Skip to content

Commit 41da735

Browse files
authored
Named state space sets and other MTK related fixes and improvements (#235)
* Informative error on nonexisting symbolic parameter * mention init cond as parameters * correct typo * add a new function for named current parameters * organize MTK tests better * add reinit_dae = false See SciML/ModelingToolkit.jl#3451 * remove `named_parametersd` Needs MTK to be defined * Named state space sets * add tests for trajectory and named variables * update tests to MTK v10 * some typos * fix jacobian
1 parent f7fc41d commit 41da735

File tree

8 files changed

+88
-65
lines changed

8 files changed

+88
-65
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# v3.15
2+
3+
- New function `named_variables(ds)` for getting the variable names of an MTK-generated dynamical system.
4+
- `trajectory` function now automatically makes a named state space set if the dynamical system is MTK-generated.
5+
16
# v3.14.0
27

38
- `set_parameter!` and `current_parameter` will now throw an informative error message explicitly naming the parameter if the user tries to get/set a symbolic parameter that does not exist in the MTK-generated dynamical system.

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DynamicalSystemsBase"
22
uuid = "6e36e845-645a-534a-86f2-f5d4aa5a06b4"
33
repo = "https://github.com/JuliaDynamics/DynamicalSystemsBase.jl.git"
4-
version = "3.14.0"
4+
version = "3.15.0"
55

66
[deps]
77
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
@@ -27,7 +27,7 @@ OrdinaryDiffEqTsit5 = "1.1"
2727
Reexport = "1"
2828
Roots = "1, 2"
2929
SciMLBase = "1.19.5, 2"
30-
StateSpaceSets = "2"
30+
StateSpaceSets = "2.5"
3131
Statistics = "1"
3232
StochasticDiffEq = "6.66.0"
3333
SymbolicIndexingInterface = "0.3.4"

docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ initial_time
3030
isinplace(::DynamicalSystem)
3131
successful_step
3232
referrenced_sciml_model
33+
named_variables
3334
```
3435

3536
```@docs

src/core/dynamicalsystem_interface.jl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ from an experimental measurement of a dynamical system with an unknown dynamic r
4444
4545
See also the DynamicalSystems.jl tutorial online for examples making dynamical systems.
4646
47-
## Integration with ModelingToolkit.jl
47+
## Integration with ModelingToolkit.jl (MTK)
4848
4949
Dynamical systems that have been constructed from `DEProblem`s that themselves
5050
have been constructed from ModelingToolkit.jl keep a reference to the symbolic
@@ -93,6 +93,7 @@ unless when developing new algorithm implementations that use dynamical systems.
9393
- [`isinplace`](@ref)
9494
- [`successful_step`](@ref)
9595
- [`referrenced_sciml_model`](@ref)
96+
- [`named_variables`](@ref)
9697
9798
### API - alter status
9899
@@ -126,7 +127,7 @@ errormsg(ds) = error("Not yet implemented for dynamical system of type $(nameof(
126127

127128
export current_state, initial_state, current_parameters, current_parameter, initial_parameters, isinplace,
128129
current_time, initial_time, successful_step, isdeterministic, isdiscretetime, dynamic_rule,
129-
reinit!, set_state!, set_parameter!, set_parameters!, step!, observe_state, referrenced_sciml_model
130+
reinit!, set_state!, set_parameter!, set_parameters!, step!, observe_state, referrenced_sciml_model, named_variables
130131

131132
###########################################################################################
132133
# Symbolic support
@@ -148,10 +149,27 @@ referrenced_sciml_model(::Nothing) = nothing
148149

149150
# return true if there is an actual referrenced system
150151
has_referrenced_model(prob::SciMLBase.DEProblem) = has_referrenced_model(referrenced_sciml_model(prob))
152+
has_referrenced_model(prob::DynamicalSystem) = has_referrenced_model(referrenced_sciml_model(prob))
151153
has_referrenced_model(::Nothing) = false
152154
has_referrenced_model(::SymbolicIndexingInterface.SymbolCache{Nothing, Nothing, Nothing}) = false
153155
has_referrenced_model(sys) = true
154156

157+
"""
158+
named_variables(ds::DynamicalSystem)
159+
160+
If `ds` is constructed via MTK, return a vector of the variable names (as symbols).
161+
Otherwise return `nothing`. If `X` is a `StateSpaceSet`, you can always do
162+
```julia
163+
X = StateSpaceSet(X; names = named_variables(ds))
164+
```
165+
in downstream functions to name a set coming from `ds` (if possible).
166+
"""
167+
function named_variables(ds::DynamicalSystem)
168+
mtk = referrenced_sciml_model(ds)
169+
isnothing(mtk) && return nothing
170+
return SymbolicIndexingInterface.getname.(SymbolicIndexingInterface.variable_symbols(mtk))
171+
end
172+
155173
###########################################################################################
156174
# API - obtaining information from the system
157175
###########################################################################################

src/core/trajectory.jl

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,15 @@ export trajectory
55
66
Evolve `ds` for a total time of `T` and return its trajectory `X`, sampled at equal time
77
intervals, and corresponding time vector. `X` is a `StateSpaceSet`.
8-
Optionally provide a starting state `u0` which is `current_state(ds)` by default.
9-
108
The returned time vector is `t = (t0+Ttr):Δt:(t0+Ttr+T)`.
119
12-
If time evolution diverged, or in general failed, before `T`,
10+
Optionally provide a starting state `u0` which is `current_state(ds)` by default.
11+
12+
If time evolution diverged or in general failed before `T`,
1313
the remaining of the trajectory is set to the last valid point.
1414
15-
`trajectory` is a very simple function provided for convenience.
16-
For continuous time systems, it doesn't play well with callbacks,
17-
use `DifferentialEquations.solve` if you want a trajectory/timeseries
18-
that works with callbacks, or in general you want more flexibility in the generated
19-
trajectory (but remember to convert the output of `solve` to a `StateSpaceSet`).
15+
The dimensions of `X` are automatically named if `ds` referrences an MTK model
16+
and if `save_idxs` remains at its default value.
2017
2118
## Keyword arguments
2219
@@ -27,22 +24,34 @@ trajectory (but remember to convert the output of `solve` to a `StateSpaceSet`).
2724
* `t0 = initial_time(ds)`: Starting time.
2825
* `container = SVector`: Type of vector that will represent the state space points
2926
that will be included in the `StateSpaceSet` output. See `StateSpaceSet` for valid options.
30-
* `save_idxs::AbstractVector`: Which variables to output in `X`. It can be
31-
any type of index that can be given to [`observe_state`](@ref).
32-
Defaults to `1:dimension(ds)` (all dynamic variables).
27+
* `save_idxs`: Which variables to output in `X`. By default it is `nothing` (all variables).
28+
It can be a vector of any type of index that can be given to [`observe_state`](@ref).
3329
Note: if you mix integer and symbolic indexing be sure to initialize the array
3430
as `Any` so that integers `1, 2, ...` are not converted to symbolic expressions.
31+
32+
## Description
33+
34+
`trajectory` is a very simple function provided for convenience.
35+
For continuous time systems, it doesn't play well with callbacks,
36+
use `DifferentialEquations.solve` if you want a trajectory
37+
that works with callbacks, or in general you want more flexibility in the generated
38+
trajectory (but remember to convert the output of `solve` to a `StateSpaceSet`).
3539
"""
3640
function trajectory(ds::DynamicalSystem, T, u0 = current_state(ds);
3741
save_idxs = nothing, t0 = initial_time(ds), kwargs...
3842
)
3943
accessor = svector_access(save_idxs)
4044
reinit!(ds, u0; t0)
4145
if isdiscretetime(ds)
42-
trajectory_discrete(ds, T; accessor, kwargs...)
46+
X, tvec = trajectory_discrete(ds, T; accessor, kwargs...)
4347
else
44-
trajectory_continuous(ds, T; accessor, kwargs...)
48+
X, tvec = trajectory_continuous(ds, T; accessor, kwargs...)
49+
end
50+
# name automatically if possible
51+
if isnothing(save_idxs)
52+
X = StateSpaceSet(X; names = named_variables(ds))
4553
end
54+
return X, tvec
4655
end
4756

4857
function trajectory_discrete(ds, T;

test/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
1010
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1111

1212
[compat]
13-
ModelingToolkit = "9"
13+
ModelingToolkit = "10"

test/jacobian.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,23 @@ end
3030

3131
@testset "MTK Jacobian" begin
3232
using ModelingToolkit
33-
using ModelingToolkit: Num, GeneratedFunctionWrapper
33+
using ModelingToolkit: GeneratedFunctionWrapper
3434
using DynamicalSystemsBase: SciMLBase
3535
@independent_variables t
3636
@variables u(t)[1:2]
3737
D = Differential(t)
3838

3939
eqs = [D(u[1]) ~ 3.0 * u[1],
4040
D(u[2]) ~ -3.0 * u[2]]
41-
@named sys = ODESystem(eqs, t)
42-
sys = structural_simplify(sys)
41+
@named sys = System(eqs, t)
42+
sys = mtkcompile(sys)
4343

4444
prob = ODEProblem(sys, [1.0, 1.0], (0.0, 1.0); jac=true)
4545
ds = CoupledODEs(prob)
4646

4747
jac = jacobian(ds)
4848
@test jac isa GeneratedFunctionWrapper
49-
@test jac([1.0, 1.0], [], 0.0) == [3 0;0 -3]
49+
@test jac([1.0, 1.0], [], 0.0) == [-3 0;0 3]
5050

5151
@testset "CoupledSDEs" begin
5252
# just to check if MTK @brownian does not give any problems
@@ -61,6 +61,6 @@ end
6161

6262
jac = jacobian(sde)
6363
@test jac isa GeneratedFunctionWrapper
64-
@test jac([1.0, 1.0], [], 0.0) == [3 0; 0 -3]
64+
@test jac([1.0, 1.0], [], 0.0) == [-3 0; 0 3]
6565
end
6666
end

test/mtk_integration.jl

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function fol_factory(separate = false; name)
1313
D(x) ~ RHS] :
1414
D(x) ~ (f - x) / τ
1515

16-
ODESystem(eqs, t; name)
16+
System(eqs, t; name)
1717
end
1818

1919
@named fol_1 = fol_factory()
@@ -22,97 +22,86 @@ end
2222
connections = [fol_1.f ~ 1.5,
2323
fol_2.f ~ fol_1.x]
2424

25-
connected = compose(ODESystem(connections, t; name = :connected), fol_1, fol_2)
25+
connected = compose(System(connections, t; name = :connected), fol_1, fol_2)
2626

27-
sys = structural_simplify(connected; split = false)
27+
sys = mtkcompile(connected; split = false)
2828

2929
u0 = [fol_1.x => -0.5,
3030
fol_2.x => 1.0]
3131

3232
p = [fol_1.τ => 2.0,
3333
fol_2.τ => 4.0]
3434

35-
prob = ODEProblem(sys, u0, (0.0, 10.0), p)
35+
initials = vcat(u0, p)
36+
37+
prob = ODEProblem(sys, initials, (0.0, 10.0))
3638
ds = CoupledODEs(prob)
3739

3840
@testset "parameter get/set" begin
39-
@test current_parameter(ds, 1) == 2.0
4041
@test current_parameter(ds, fol_1.τ) == 2.0
41-
@test current_parameter(ds, 2) == 4.0
4242
@test current_parameter(ds, fol_2.τ) == 4.0
43-
44-
set_parameter!(ds, 1, 3.0)
45-
@test current_parameter(ds, 1) == 3.0
46-
@test current_parameter(ds, fol_1.τ) == 3.0
47-
48-
set_parameter!(ds, fol_1.τ, 2.0)
49-
@test current_parameter(ds, 1) == 2.0
50-
@test current_parameter(ds, fol_1.τ) == 2.0
43+
set_parameter!(ds, fol_1.τ, 4.0)
44+
@test current_parameter(ds, fol_1.τ) == 4.0
5145
end
5246

5347
# pure parameter container
5448
@testset "pure parameter container" begin
5549
pp = deepcopy(current_parameters(ds))
56-
set_parameter!(ds, fol_1.τ, 4.0, pp)
57-
@test current_parameter(ds, fol_1.τ, pp) == 4.0
50+
set_parameter!(ds, fol_1.τ, 2.0, pp)
51+
@test current_parameter(ds, fol_1.τ, pp) == 2.0
5852
end
5953

6054
@testset "states and observed variables" begin
61-
@test observe_state(ds, 1) == -0.5
6255
@test observe_state(ds, fol_1.x) == -0.5
6356
@test observe_state(ds, fol_2.RHS) == -0.375
6457

65-
set_state!(ds, 1.5, 1)
66-
@test observe_state(ds, 1) == 1.5
58+
x0 = observe_state(ds, 1)
59+
set_state!(ds, x0, 1)
60+
@test observe_state(ds, 1) == x0
6761
set_state!(ds, -0.5, fol_1.x)
68-
@test observe_state(ds, 1) == -0.5
62+
@test observe_state(ds, fol_1.x) == -0.5
6963
end
7064

7165
@testset "derivative dynamical systems" begin
7266
u1 = current_state(ds)
7367
pds = ParallelDynamicalSystem(ds, [u1, copy(u1)])
7468

7569
set_parameter!(pds, fol_1.τ, 4.0)
76-
@test current_parameter(pds, 1) == 4.0
7770
@test current_parameter(pds, fol_1.τ) == 4.0
7871
@test observe_state(pds, fol_1.x) == -0.5
7972
@test observe_state(pds, fol_2.RHS) == -0.375
8073

8174
sds = StroboscopicMap(ds, 1.0)
8275
set_parameter!(sds, fol_1.τ, 2.0)
83-
@test current_parameter(sds, 1) == 2.0
8476
@test current_parameter(sds, fol_1.τ) == 2.0
8577
@test observe_state(sds, fol_1.x) == -0.5
8678
@test observe_state(sds, fol_2.RHS) == -0.375
8779

8880
prods = ProjectedDynamicalSystem(ds, [1], [0.0])
8981
set_parameter!(prods, fol_1.τ, 3.0)
90-
@test current_parameter(prods, 1) == 3.0
9182
@test current_parameter(prods, fol_1.τ) == 3.0
9283
@test observe_state(prods, fol_1.x) == -0.5
9384
@test observe_state(prods, fol_2.RHS) == -0.375
9485

9586
# notice this evolves the dynamical system!!!
96-
pmap = PoincareMap(ds, (1, 0.0))
87+
pmap = PoincareMap(ds, (2, 0.0))
9788
set_parameter!(pmap, fol_1.τ, 4.0)
98-
@test current_parameter(pmap, 1) == 4.0
9989
@test current_parameter(pmap, fol_1.τ) == 4.0
10090
@test observe_state(pmap, fol_1.x) 0 atol = 1e-3 rtol = 0
10191
end
10292

93+
@testset "trajectory naming" begin
94+
vars = named_variables(ds)
95+
@test length(vars) == 2
96+
@test sort!(string.(vars)) == ["fol_1₊x", "fol_2₊x"]
97+
X, tvec = trajectory(ds, 10)
98+
@test X.names == vars
99+
end
103100

104101
@testset "split = true" begin
105-
sys = structural_simplify(connected; split = true)
106-
107-
u0 = [fol_1.x => -0.5,
108-
fol_2.x => 1.0]
109-
110-
p = [fol_1.τ => 2.0,
111-
fol_2.τ => 4.0]
112-
113-
prob = ODEProblem(sys, u0, (0.0, 10.0), p)
102+
sys = mtkcompile(connected; split = true)
103+
prob = ODEProblem(sys, initials, (0.0, 10.0))
114104
ds = CoupledODEs(prob)
115-
116105
@test current_parameter(ds, fol_1.τ) == 2.0
117106
set_parameter!(ds, fol_1.τ, 3.0)
118107
@test current_parameter(ds, fol_1.τ) == 3.0
@@ -123,6 +112,7 @@ end
123112
du[1] = p[1] * (u[2] - u[1])
124113
du[2] = u[1] * (28.0 - u[3]) - u[2]
125114
du[3] = u[1] * u[2] - (8 / 3) * u[3]
115+
return nothing
126116
end
127117
u0 = [1.0; 0.0; 0.0]
128118
tspan = (0.0, 100.0)
@@ -136,7 +126,7 @@ end
136126

137127
@test observe_state(ds, 1) == 1.0
138128

139-
@test_throws ErrorException observe_state(ds, fol_1.f)
129+
@test_throws ArgumentError observe_state(ds, fol_1.f)
140130
end
141131

142132
# Test that remake works also without anything initial
@@ -163,11 +153,11 @@ D = Differential(t)
163153
end
164154
end
165155

166-
@mtkbuild roessler_model = Roessler()
156+
@mtkcompile roessler_model = Roessler()
167157

168158
@testset "type: $(iip)" for iip in (true, false)
169159
if iip
170-
prob = ODEProblem(roessler_model)
160+
prob = ODEProblem(roessler_model, nothing, (0.0, Inf))
171161
else
172162
prob = ODEProblem{false}(roessler_model, nothing, (0.0, Inf); u0_constructor = x->SVector(x...))
173163
end
@@ -209,7 +199,7 @@ end
209199
end
210200

211201
@testset "informative errors" begin
212-
prob = ODEProblem(roessler_model)
202+
prob = prob = ODEProblem(roessler_model, nothing, (0.0, Inf))
213203
ds = CoupledODEs(prob)
214204
@parameters XOXO = 0.5
215205
@test_throws "XOXO" current_parameter(ds, :XOXO)
@@ -231,10 +221,10 @@ Differential(t)(DS) ~ η2 - η3*DS - abs(DT - DS)*DS,
231221
η1 ~ η1_0 + r_η*t, # this symbolic variable has its own equation!
232222
]
233223

234-
sys = ODESystem(eqs, t; name = :stommel)
235-
sys = structural_simplify(sys; split = false)
224+
sys = System(eqs, t; name = :stommel)
225+
sys = mtkcompile(sys; split = false)
236226

237-
prob = ODEProblem(sys)
227+
prob = ODEProblem(sys, nothing, (0.0, Inf))
238228
ds = CoupledODEs(prob)
239229

240230
X, tvec = trajectory(ds, 10.0; Δt = 0.1, save_idxs = Any[1, 2, η1])

0 commit comments

Comments
 (0)