Olá!
Nesse post, eu mostrarei como podemo utilizar um Data Array para estender as possibilidade do pacote OrdinaryDiffEq.jl da linguagem julia para simular sistemas dinâmicos.
Experiência necessária
Esse tutorial provê informações adicionais para resolver o problema introduzido no meu último tutorial , então é muito importante que você o leia antes.
Definição do problema
No meu último post , eu mostrei como o pacote OrdinaryDiffEq.jl da linguagem julia pode ser utilizado para simular sistema de controle que possuem partes discretas e contínuas. No entanto, existiam dois grande problemas:
- Eu precisei utilizar uma variável global para passar a informação da parte discreta para a função dinâmica (naquele caso, a informação era a aceleração);
- Eu teria que utilizar alguns hacks muito ruins para plotar a aceleração comandada (criar um vetor global para guardar os valores computados).
Como mencionei anteriormente, a solução ótima para contornar esses problemas
é era de alguma forma difícil de se obter. Felizmente,
Chris
Rackauckas
, que é o mantenedor do pacote
OrdinaryDiffEq.jl, introduziu o conceito de Data Arrays, que simplifica
muito a solução para aqueles problemas. Note que você precisará da v1.4.0 ou
superior do pacote OrdinaryDiffEq para ter acesso à nova interface Data Array.
Nesse tutorial, o problema que queremos resolver é: como criar parâmetros customizados em que a parte discreta do sistema modifica e que influenciará na função dinâmica?
Uma pequena história
Quando eu comecei a usar o OrdinaryDiffEq.jl para simular o subsistema de controle e atitude do satélite Amazonia-1, eu verifiquei que eu precisaria de dois tipos de variáveis:
- Aquelas que compõe o vetor de estados e que serão integradas pelo solver; e
- Alguns parâmetros que o loop de controle pode modificar e que irá influenciar na solução dinâmica, como, por exemplo, o torque comandado para as rodas de reação e magneto-torquers.
Entretanto, naquela época, não existia um meio fácil de se conseguir isso. O
único método é criar uma estrutura (type) customizada em julia para guardar os
parâmetros que são externos ao vetor de estado. O problema é que você precisava
implementar muitos métodos para que essa nova estrutura fosse compatível com a
interface do OrdinaryDiffEq.jl. A seguir, você poderá ver o que deveria ser
feito para que se definisse um parâmetro chamado f1
. Esse código foi utilizado
na issue aberta por mim no GitHub:
https://github.com/JuliaDiffEq/DifferentialEquations.jl/issues/117
Método antigo para se criar um tipo customizado no OrdinaryDiffEq.jl
importall Base
import RecursiveArrayTools.recursivecopy!
using DifferentialEquations
type SimType{T} <: AbstractArray{T,1}
x::Array{T,1}
f1::T
end
function SimType{T}(::Type{T}, length::Int64)
return SimType{T}(zeros(length), T(0))
end
function SimType{T}(S::SimType{T})
return SimType(T, length(S.x))
end
function SimType{T}(S::SimType{T}, dims::Dims)
return SimType(T, prod(dims))
end
similar{T}(A::SimType{T}) = begin
R = SimType(A)
R.f1 = A.f1
R
end
similar{T}(A::SimType{T}, ::Type{T}, dims::Dims) = begin
R = SimType(A, dims)
R.f1 = A.f1
R
end
done(A::SimType, i::Int64) = done(A.x,i)
eachindex(A::SimType) = eachindex(A.x)
next(A::SimType, i::Int64) = next(A.x,i)
start(A::SimType) = start(A.x)
length(A::SimType) = length(A.x)
ndims(A::SimType) = ndims(A.x)
size(A::SimType) = size(A.x)
recursivecopy!(B::SimType, A::SimType) = begin
recursivecopy!(B.x, A.x)
B.f1 = A.f1
end
getindex( A::SimType, i::Int...) = (A.x[i...])
setindex!(A::SimType, x, i::Int...) = (A.x[i...] = x)
Então, Chris Rackauckas decidiu criar uma nova interface dentro do pacote OrdinaryDiffEq.jl para que você possa facilmente construir suas próprias estruturas customizadas para serem utilizadas nos solvers.
Nota: Na verdade, a definição da nova interface foi colocada no pacote DiffEqBase , que é uma dependência do OrdinaryDiffEq.jl.
Interface Data Type
Essa interface permite que você defina suas próprias estruturas a serem
utilizadas pelo solver. Para fazer isso, você simplesmente precisa definir um
novo tipo em julia (type) que herda o tipo DEDataVector
:
mutable struct MyDataArray{T} <: DEDataVector{T}
x::Vector{T}
a::T
b::Symbol
end
onde deve existir um array x
que define o vetor de estados e qualquer
outro parâmetro que você irá necessitar na sua simulação. Dado isso, você poderá
acessar quaisquer parâmetros dentro do solver e das funções de callback
utilizando u.a
, u.b
, etc. Você poderá também acessar o i-ésimo elemento do
vetor de estados utilizando u[i]
. Em outras palavras, você não precisará da
notação pesada u.x[i]
.
Isso praticamente substitui todo aquele código que eu mostrei anteriormente 👍.
Resolvendo o nosso problema
Então, agora nós podemos remover a variável global a
(🎉🎊🎉🎊🎉🎊) definindo
o seguinte tipo:
mutable struct SimWorkspace{T} <: DEDataVector{T}
x::Vector{T}
a::T
end
Dentro de nossa função dinâmica, nós podemos agora obter a aceleração comandada
utilizando u.a
:
# Dynamic equation.
function dyn_eq!(du, u, t, p)
du[1] = u[2]
du[2] = u.a
return nothing
end
Agora, nós precisamos apenas modificar a função efeito do nosso callback para escrever nessa variável:
# Affect function.
function control_loop!(integrator)
p = integrator.u[1]
v = integrator.u[2]
a = k*(r-p) + d*(0.0-v)
for c in full_cache(integrator)
c.a = a
end
end
Você deverá ter notado um for
bem estranho aqui. Esse problema é criado pela
estrutura interna do solucionadores de equações diferenciais ordinárias. Por
exemplo, o
Algoritmo Runge-Kutta de 4ª
ordem
necessita
quatro caches chamados k_1
, k_2
, k_3
e k_4
, que são basicamente a função
dinâmica computada em diferentes pontos do espaço de estados, sendo utilizados
para obter a solução no próximo instante de amostragem. Nesse caso, o parâmetro
a
deve ser definido em todos esses caches para fornecer a solução correta.
Isso é precisamente o que aquele for
está fazendo. Ele está passando por todos
os caches no solver e definindocorretamente o parâmetro a
com a aceleração
computada pela função efeito desse callback. Note que o vetor de estado atual
integrator.u
está dentro da lista full_cache(integrator)
e,
consequentemente, é também definido dentro daquele loop. O lado bom de se
utilizar esse for
é que você não precisa se preocupar com quantos caches o
solver selecionado possui.
Finalmente, nós precisamos apenas definir nosso estado inicial utilizando o tipo
SimWorkspace
:
u0 = SimWorkspace(zeros(2), 0.0)
onde o primeiro parâmetro está inicializando o vetor de estados com zeros
(posição e velocidade) e o segundo parâmetro está inicializando a aceleração.
Após a execução do programa, nós podemos acessar a aceleração comandada em cada
instante de amostragem que foi salvo utilizando sol[i].a
.
Se você estiver utilizando o PyPlot , então o vetor de estado e a aceleração podem ser plotados através de:
plot(sol.t, sol.u, sol.t, map(x->x.a, sol.u))
que deverá fornecer o seguinte resultado:
Se você quiser alterar o estado inicial, então você deverá apenas modificar a
variável u0
antes de chamar o solver:
u0 = SimWorkspace([5.0; -1.0], 0.0)
ou
u0 = SimWorkspace(zeros(2), 0.0)
u0[1] = 5.0
u0[2] = -1.0
que deverá fornecer o seguinte resultado:
Conclusões
Nesse tutorial, eu mostrei como você pode utilizar a interface Data Array do pacote OrdinaryDiffEq.jl para criar espaços de trabalho customizados para suas simulaçãoes. Portanto, agora você sabe como criar parâmetros para serem utilizados no solver que não pertencem ao vetor de estado.
Eu forneci um exemplo através da extensão da solução apresentada em um tutorial anterior , onde nós precisamos de uma variável global para lidar com esse tipo de parâmetro. Note que, embora o exemplo apresentado aqui seja muito simples, esse conceito pode ser facilmente estendido para lhe fornecer uma solução simples e elegante para simular sistemas muito mais complexos.
Se não ficou claro, por favor, me avise nos comentários. Eu irei utilizar o seu conselho para melhorar esse e meus futuros tutoriais!
A seguir, você encontrará o código fonte desse tutorial.
using DiffEqBase
using OrdinaryDiffEq
###############################################################################
# Structures
###############################################################################
mutable struct SimWorkspace{T} <: DEDataVector{T}
x::Vector{T}
a::T
end
###############################################################################
# Variables
###############################################################################
# Parameters.
r = 10.0 # Reference position.
k = 0.3 # Proportional gain.
d = 0.8 # Derivative gain.
vl = 2.0 # Velocity limit.
# Configuration of the simulation.
tf = 30.0 # Final simulation time.
tstops = collect(0:1:tf) # Instants that the control loop will be computed.
# Initial state of the simulation.
# |-- State Vector --| |-- Parameter a -- |
u0 = SimWorkspace( zeros(2), 0.0 )
###############################################################################
# Functions
###############################################################################
# Dynamic equation.
function dyn_eq(du, u, t, p)
du[1] = u[2]
du[2] = u.a
return nothing
end
# CALLBACK: Control loop.
# =======================
# Condition function.
function condition_control_loop(u, t, integrator)
(t in tstops)
end
# Affect function.
function control_loop!(integrator)
p = integrator.u[1]
v = integrator.u[2]
a = k*(r-p) + d*(0.0-v)
for c in full_cache(integrator)
c.a = a
end
end
cb = DiscreteCallback(condition_control_loop, control_loop!)
###############################################################################
# Solver
###############################################################################
prob = ODEProblem(dyn_eq, u0, (0.0, tf))
sol = solve(prob, Tsit5(), callback = cb, tstops = tstops)