Skip to content

Commit 542afe6

Browse files
committed
Merge branch 'master' into kf/state_selection
2 parents d2880c9 + 72a3978 commit 542afe6

31 files changed

+608
-276
lines changed

NEWS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# ModelingToolkit v8 Release Notes
2+
3+
4+
### Upgrade guide
5+
6+
- `connect` should not be overloaded by users anymore. `[connect = Flow]`
7+
informs ModelingToolkit that particular variable in a connector ought to sum
8+
to zero, and by default, variables are equal in a connection. Please check out
9+
[acausal components tutorial](https://mtk.sciml.ai/dev/tutorials/acausal_components/)
10+
for examples.

Project.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelingToolkit"
22
uuid = "961ee093-0014-501f-94e3-6117800e7a78"
33
authors = ["Chris Rackauckas <[email protected]>"]
4-
version = "8.0.0"
4+
version = "8.3.1"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -45,10 +45,10 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
4545

4646
[compat]
4747
AbstractTrees = "0.3"
48-
ArrayInterface = "3.1.39"
48+
ArrayInterface = "3.1.39, 4"
4949
ConstructionBase = "1"
5050
DataStructures = "0.17, 0.18"
51-
DiffEqBase = "6.54.0"
51+
DiffEqBase = "6.81.0"
5252
DiffEqCallbacks = "2.16"
5353
DiffEqJump = "7.0, 8"
5454
DiffRules = "0.1, 1.0"
@@ -57,7 +57,7 @@ DocStringExtensions = "0.7, 0.8"
5757
DomainSets = "0.5"
5858
Graphs = "1.4"
5959
IfElse = "0.1"
60-
JuliaFormatter = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.20"
60+
JuliaFormatter = "0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.20, 0.21"
6161
LabelledArrays = "1.3"
6262
Latexify = "0.11, 0.12, 0.13, 0.14, 0.15"
6363
MacroTools = "0.5"

docs/src/basics/Composition.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,17 @@ before numerically solving. The `structural_simplify` function removes
167167
these trivial equality relationships and trivial singularity equations,
168168
i.e. equations which result in `0~0` expressions, in over-specified systems.
169169

170-
## Inheritance and Combine (TODO)
170+
## Inheritance and Combine
171171

172-
Model inheritance can be done in two ways: explicitly or implicitly.
173-
The explicit way is to shadow variables with equality expressions.
174-
For example, let's assume we have three separate systems which we
175-
want to compose to a single one. This is how one could explicitly
176-
forward all states and parameters to the higher level system:
172+
Model inheritance can be done in two ways: implicitly or explicitly. First, one
173+
can use the `extend` function to extend a base model with another set of
174+
equations, states, and parameters. An example can be found in the
175+
[acausal components tutorial](@ref acausal).
176+
177+
The explicit way is to shadow variables with equality expressions. For example,
178+
let's assume we have three separate systems which we want to compose to a single
179+
one. This is how one could explicitly forward all states and parameters to the
180+
higher level system:
177181

178182
```julia
179183
using ModelingToolkit, OrdinaryDiffEq, Plots
@@ -259,7 +263,7 @@ ODESystem(eqs, ...; continuous_events::Pair{Vector{Equation}, Vector{Equation}})
259263
```
260264
where equations can be added that evaluate to 0 at discontinuities.
261265

262-
To model events that have an affect on the state, provide `events::Pair{Vector{Equation}, Vector{Equation}}` where the first entry in the pair is a vector of equations describing event conditions, and the second vector of equations describe the affect on the state. The affect equations must be on the form
266+
To model events that have an effect on the state, provide `events::Pair{Vector{Equation}, Vector{Equation}}` where the first entry in the pair is a vector of equations describing event conditions, and the second vector of equations describe the effect on the state. The effect equations must be of the form
263267
```
264268
single_state_variable ~ expression_involving_any_variables
265269
```
@@ -275,7 +279,7 @@ function UnitMassWithFriction(k; name)
275279
D(x) ~ v
276280
D(v) ~ sin(t) - k*sign(v) # f = ma, sinusoidal force acting on the mass, and Coulomb friction opposing the movement
277281
]
278-
ODESystem(eqs, t, continuous_events=[v ~ 0], name=name) # when v = 0 there is a discontinuity
282+
ODESystem(eqs, t; continuous_events=[v ~ 0], name) # when v = 0 there is a discontinuity
279283
end
280284
@named m = UnitMassWithFriction(0.7)
281285
prob = ODEProblem(m, Pair[], (0, 10pi))
@@ -296,7 +300,7 @@ affect = [v ~ -v] # the effect is that the velocity changes sign
296300
@named ball = ODESystem([
297301
D(x) ~ v
298302
D(v) ~ -9.8
299-
], t, continuous_events = root_eqs => affect) # equation => affect
303+
], t; continuous_events = root_eqs => affect) # equation => affect
300304

301305
ball = structural_simplify(ball)
302306

@@ -323,7 +327,7 @@ continuous_events = [ # This time we have a vector of pairs
323327
D(y) ~ vy,
324328
D(vx) ~ -9.8-0.1vx, # gravity + some small air resistance
325329
D(vy) ~ -0.1vy,
326-
], t, continuous_events = continuous_events)
330+
], t; continuous_events)
327331

328332

329333
ball = structural_simplify(ball)
@@ -340,4 +344,4 @@ tv = sort([LinRange(0, 10, 200); sol.t])
340344
plot(sol(tv)[y], sol(tv)[x], line_z=tv)
341345
vline!([-1.5, 1.5], l=(:black, 5), primary=false)
342346
hline!([0], l=(:black, 5), primary=false)
343-
```
347+
```

docs/src/tutorials/acausal_components.md

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,10 @@ using ModelingToolkit, Plots, DifferentialEquations
1616

1717
@variables t
1818
@connector function Pin(;name)
19-
sts = @variables v(t)=1.0 i(t)=1.0
19+
sts = @variables v(t)=1.0 i(t)=1.0 [connect = Flow]
2020
ODESystem(Equation[], t, sts, []; name=name)
2121
end
2222

23-
function ModelingToolkit.connect(::Type{Pin}, ps...)
24-
eqs = [
25-
0 ~ sum(p->p.i, ps) # KCL
26-
]
27-
# KVL
28-
for i in 1:length(ps)-1
29-
push!(eqs, ps[i].v ~ ps[i+1].v)
30-
end
31-
32-
return eqs
33-
end
34-
3523
function Ground(;name)
3624
@named g = Pin()
3725
eqs = [g.v ~ 0]
@@ -92,7 +80,8 @@ V = 1.0
9280
rc_eqs = [
9381
connect(source.p, resistor.p)
9482
connect(resistor.n, capacitor.p)
95-
connect(capacitor.n, source.n, ground.g)
83+
connect(capacitor.n, source.n)
84+
connect(capacitor.n, ground.g)
9685
]
9786

9887
@named _rc_model = ODESystem(rc_eqs, t)
@@ -117,11 +106,15 @@ For each of our components we use a Julia function which emits an `ODESystem`.
117106
At the top we start with defining the fundamental qualities of an electrical
118107
circuit component. At every input and output pin a circuit component has
119108
two values: the current at the pin and the voltage. Thus we define the `Pin`
120-
component (connector) to simply be the values there:
109+
component (connector) to simply be the values there. Whenever two `Pin`s in a
110+
circuit are connected together, the system satisfies [Kirchoff's laws](https: //en.wikipedia.org/wiki/Kirchhoff%27s_circuit_laws),
111+
i.e. that currents sum to zero and voltages across the pins are equal.
112+
`[connect = Flow]` informs MTK that currents ought to sum to zero, and by
113+
default, variables are equal in a connection.
121114

122115
```julia
123116
@connector function Pin(;name)
124-
sts = @variables v(t)=1.0 i(t)=1.0
117+
sts = @variables v(t)=1.0 i(t)=1.0 [connect = Flow]
125118
ODESystem(Equation[], t, sts, []; name=name)
126119
end
127120
```
@@ -253,26 +246,6 @@ V = 1.0
253246
@named ground = Ground()
254247
```
255248

256-
Next we have to define how we connect the circuit. Whenever two `Pin`s in a
257-
circuit are connected together, the system satisfies
258-
[Kirchoff's laws](https://en.wikipedia.org/wiki/Kirchhoff%27s_circuit_laws),
259-
i.e. that currents sum to zero and voltages across the pins are equal. Thus
260-
we will build a helper function `connect_pins` which implements these rules:
261-
262-
```julia
263-
function ModelingToolkit.connect(::Type{Pin}, ps...)
264-
eqs = [
265-
0 ~ sum(p->p.i, ps) # KCL
266-
]
267-
# KVL
268-
for i in 1:length(ps)-1
269-
push!(eqs, ps[i].v ~ ps[i+1].v)
270-
end
271-
272-
return eqs
273-
end
274-
```
275-
276249
Finally we will connect the pieces of our circuit together. Let's connect the
277250
positive pin of the resistor to the source, the negative pin of the resistor
278251
to the capacitor, and the negative pin of the capacitor to a junction between
@@ -282,7 +255,8 @@ the source and the ground. This would mean our connection equations are:
282255
rc_eqs = [
283256
connect(source.p, resistor.p)
284257
connect(resistor.n, capacitor.p)
285-
connect(capacitor.n, source.n, ground.g)
258+
connect(capacitor.n, source.n)
259+
connect(capacitor.n, ground.g)
286260
]
287261
```
288262

docs/src/tutorials/spring_mass.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ In this tutorial we will build a simple component-based model of a spring-mass s
66

77
```julia
88
using ModelingToolkit, Plots, DifferentialEquations, LinearAlgebra
9+
using Symbolics: scalarize
910

1011
@variables t
1112
D = Differential(t)
1213

1314
function Mass(; name, m = 1.0, xy = [0., 0.], u = [0., 0.])
1415
ps = @parameters m=m
1516
sts = @variables pos[1:2](t)=xy v[1:2](t)=u
16-
eqs = collect(D.(pos) .~ v)
17+
eqs = scalarize(D.(pos) .~ v)
1718
ODESystem(eqs, t, [pos..., v...], ps; name)
1819
end
1920

@@ -25,12 +26,12 @@ end
2526

2627
function connect_spring(spring, a, b)
2728
[
28-
spring.x ~ norm(collect(a .- b))
29-
collect(spring.dir .~ collect(a .- b))
29+
spring.x ~ norm(scalarize(a .- b))
30+
scalarize(spring.dir .~ scalarize(a .- b))
3031
]
3132
end
3233

33-
spring_force(spring) = -spring.k .* collect(spring.dir) .* (spring.x - spring.l) ./ spring.x
34+
spring_force(spring) = -spring.k .* scalarize(spring.dir) .* (spring.x - spring.l) ./ spring.x
3435

3536
m = 1.0
3637
xy = [1., -1.]
@@ -43,7 +44,7 @@ g = [0., -9.81]
4344

4445
eqs = [
4546
connect_spring(spring, mass.pos, center)
46-
collect(D.(mass.v) .~ spring_force(spring) / mass.m .+ g)
47+
scalarize(D.(mass.v) .~ spring_force(spring) / mass.m .+ g)
4748
]
4849

4950
@named _model = ODESystem(eqs, t)
@@ -65,7 +66,7 @@ For each component we use a Julia function that returns an `ODESystem`. At the t
6566
function Mass(; name, m = 1.0, xy = [0., 0.], u = [0., 0.])
6667
ps = @parameters m=m
6768
sts = @variables pos[1:2](t)=xy v[1:2](t)=u
68-
eqs = collect(D.(pos) .~ v)
69+
eqs = scalarize(D.(pos) .~ v)
6970
ODESystem(eqs, t, [pos..., v...], ps; name)
7071
end
7172
```
@@ -97,16 +98,16 @@ We now define functions that help construct the equations for a mass-spring syst
9798
```julia
9899
function connect_spring(spring, a, b)
99100
[
100-
spring.x ~ norm(collect(a .- b))
101-
collect(spring.dir .~ collect(a .- b))
101+
spring.x ~ norm(scalarize(a .- b))
102+
scalarize(spring.dir .~ scalarize(a .- b))
102103
]
103104
end
104105
```
105106

106107
Lastly, we define the `spring_force` function that takes a `spring` and returns the force exerted by this spring.
107108

108109
```julia
109-
spring_force(spring) = -spring.k .* collect(spring.dir) .* (spring.x - spring.l) ./ spring.x
110+
spring_force(spring) = -spring.k .* scalarize(spring.dir) .* (spring.x - spring.l) ./ spring.x
110111
```
111112

112113
To create our system, we will first create the components: a mass and a spring. This is done as follows:
@@ -127,7 +128,7 @@ We can now create the equations describing this system, by connecting `spring` t
127128
```julia
128129
eqs = [
129130
connect_spring(spring, mass.pos, center)
130-
collect(D.(mass.v) .~ spring_force(spring) / mass.m .+ g)
131+
scalarize(D.(mass.v) .~ spring_force(spring) / mass.m .+ g)
131132
]
132133
```
133134

@@ -230,4 +231,4 @@ We can also plot the path of the mass:
230231
plot(sol, vars = (mass.pos[1], mass.pos[2]))
231232
```
232233

233-
![plotpos](https://user-images.githubusercontent.com/23384717/130322197-cff35eb7-0739-471d-a3d9-af83d87f1cc7.png)
234+
![plotpos](https://user-images.githubusercontent.com/23384717/130322197-cff35eb7-0739-471d-a3d9-af83d87f1cc7.png)

docs/src/tutorials/tearing_parallelism.md

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,11 @@ electrical circuits:
1212
```julia
1313
using ModelingToolkit, OrdinaryDiffEq
1414

15-
function connect_pin(ps...)
16-
eqs = [
17-
0 ~ sum(p->p.i, ps) # KCL
18-
]
19-
# KVL
20-
for i in 1:length(ps)-1
21-
push!(eqs, ps[i].v ~ ps[i+1].v)
22-
end
23-
24-
return eqs
25-
end
26-
27-
function connect_heat(ps...)
28-
eqs = [
29-
0 ~ sum(p->p.Q_flow, ps)
30-
]
31-
for i in 1:length(ps)-1
32-
push!(eqs, ps[i].T ~ ps[i+1].T)
33-
end
34-
35-
return eqs
36-
end
37-
3815
# Basic electric components
3916
@variables t
4017
const D = Differential(t)
41-
function Pin(;name)
42-
@variables v(t)=1.0 i(t)=1.0
18+
@connector function Pin(;name)
19+
@variables v(t)=1.0 i(t)=1.0 [connect = Flow]
4320
ODESystem(Equation[], t, [v, i], [], name=name)
4421
end
4522

@@ -61,8 +38,8 @@ function ConstantVoltage(;name, V = 1.0)
6138
compose(ODESystem(eqs, t, [], [V], name=name), p, n)
6239
end
6340

64-
function HeatPort(;name)
65-
@variables T(t)=293.15 Q_flow(t)=0.0
41+
@connector function HeatPort(;name)
42+
@variables T(t)=293.15 Q_flow(t)=0.0 [connect = Flow]
6643
ODESystem(Equation[], t, [T, Q_flow], [], name=name)
6744
end
6845

@@ -120,10 +97,10 @@ function parallel_rc_model(i; name, source, ground, R, C)
12097
heat_capacitor = HeatCapacitor(name=Symbol(:heat_capacitor, i))
12198

12299
rc_eqs = [
123-
connect_pin(source.p, resistor.p)
124-
connect_pin(resistor.n, capacitor.p)
125-
connect_pin(capacitor.n, source.n, ground.g)
126-
connect_heat(resistor.h, heat_capacitor.h)
100+
connect(source.p, resistor.p)
101+
connect(resistor.n, capacitor.p)
102+
connect(capacitor.n, source.n, ground.g)
103+
connect(resistor.h, heat_capacitor.h)
127104
]
128105

129106
compose(ODESystem(rc_eqs, t, name=Symbol(name, i)),
@@ -139,8 +116,8 @@ we can connect a bunch of RC components as follows:
139116

140117
```julia
141118
V = 2.0
142-
source = ConstantVoltage(name=:source, V=V)
143-
ground = Ground(name=:ground)
119+
@named source = ConstantVoltage(V=V)
120+
@named ground = Ground()
144121
N = 50
145122
Rs = 10 .^range(0, stop=-4, length=N)
146123
Cs = 10 .^range(-3, stop=0, length=N)
@@ -170,19 +147,9 @@ Yes, that's a good question! Let's investigate a little bit more what had happen
170147
If you look at the system we defined:
171148

172149
```julia
173-
equations(big_rc)
174-
175-
1051-element Vector{Equation}:
176-
Differential(t)(E(t)) ~ rc10₊resistor10₊h₊Q_flow(t) + rc11₊resistor11₊h₊Q_flow(t) + rc12₊resistor12₊h₊Q_flow(t) + rc13₊resistor13₊h₊Q_flow(t) + rc14₊resistor14₊h₊Q_flow(t) + rc15₊resistor15₊h₊Q_flow(t) + rc16₊resistor16₊h₊Q_flow(t) + rc17₊resistor17₊h₊Q_flow(t) + rc18₊resistor18₊h₊Q_flow(t) + rc19₊resistor19₊h₊Q_flow(t) + rc1₊resistor1₊h₊Q_flow(t) + rc20₊resistor20₊h₊Q_flow(t) + rc21₊resistor21₊h₊Q_flow(t) + rc22₊resistor22₊h₊Q_flow(t) + rc23₊resistor23₊h₊Q_flow(t) + rc24₊resistor24₊h₊Q_flow(t) + rc25₊resistor25₊h₊Q_flow(t) + rc26₊resistor26₊h₊Q_flow(t) + rc27₊resistor27₊h₊Q_flow(t) + rc28₊resistor28₊h₊Q_flow(t) + rc29₊resistor29₊h₊Q_flow(t) + rc2₊resistor2₊h₊Q_flow(t) + rc30₊resistor30₊h₊Q_flow(t) + rc31₊resistor31₊h₊Q_flow(t) + rc32₊resistor32₊h₊Q_flow(t) + rc33₊resistor33₊h₊Q_flow(t) + rc34₊resistor34₊h₊Q_flow(t) + rc35₊resistor35₊h₊Q_flow(t) + rc36₊resistor36₊h₊Q_flow(t) + rc37₊resistor37₊h₊Q_flow(t) + rc38₊resistor38₊h₊Q_flow(t) + rc39₊resistor39₊h₊Q_flow(t) + rc3₊resistor3₊h₊Q_flow(t) + rc40₊resistor40₊h₊Q_flow(t) + rc41₊resistor41₊h₊Q_flow(t) + rc42₊resistor42₊h₊Q_flow(t) + rc43₊resistor43₊h₊Q_flow(t) + rc44₊resistor44₊h₊Q_flow(t) + rc45₊resistor45₊h₊Q_flow(t) + rc46₊resistor46₊h₊Q_flow(t) + rc47₊resistor47₊h₊Q_flow(t) + rc48₊resistor48₊h₊Q_flow(t) + rc49₊resistor49₊h₊Q_flow(t) + rc4₊resistor4₊h₊Q_flow(t) + rc50₊resistor50₊h₊Q_flow(t) + rc5₊resistor5₊h₊Q_flow(t) + rc6₊resistor6₊h₊Q_flow(t) + rc7₊resistor7₊h₊Q_flow(t) + rc8₊resistor8₊h₊Q_flow(t) + rc9₊resistor9₊h₊Q_flow(t)
177-
0 ~ rc1₊resistor1₊p₊i(t) + rc1₊source₊p₊i(t)
178-
rc1₊source₊p₊v(t) ~ rc1₊resistor1₊p₊v(t)
179-
0 ~ rc1₊capacitor1₊p₊i(t) + rc1₊resistor1₊n₊i(t)
180-
rc1₊resistor1₊n₊v(t) ~ rc1₊capacitor1₊p₊v(t)
181-
182-
rc50₊source₊V ~ rc50₊source₊p₊v(t) - rc50₊source₊n₊v(t)
183-
0 ~ rc50₊source₊n₊i(t) + rc50₊source₊p₊i(t)
184-
rc50₊ground₊g₊v(t) ~ 0
185-
Differential(t)(rc50₊heat_capacitor50₊h₊T(t)) ~ rc50₊heat_capacitor50₊h₊Q_flow(t)*(rc50₊heat_capacitor50₊V^-1)*(rc50₊heat_capacitor50₊cp^-1)*(rc50₊heat_capacitor50₊rho^-1)
150+
length(equations(big_rc))
151+
152+
801
186153
```
187154

188155
You see it started as a massive 1051 set of equations. However, after eliminating
@@ -211,7 +178,7 @@ investigate what this means:
211178

212179
```julia
213180
using ModelingToolkit.BipartiteGraphs
214-
ts = TearingState(big_rc)
181+
ts = TearingState(expand_connections(big_rc))
215182
inc_org = BipartiteGraphs.incidence_matrix(ts.graph)
216183
blt_org = StructuralTransformations.sorted_incidence_matrix(big_rc, only_algeqs=true, only_algvars=true)
217184
blt_reduced = StructuralTransformations.sorted_incidence_matrix(sys, only_algeqs=true, only_algvars=true)
@@ -223,7 +190,7 @@ The figure on the left is the original incidence matrix of the algebraic equatio
223190
Notice that the original formulation of the model has dependencies between different
224191
equations, and so the full set of equations must be solved together. That exposes
225192
no parallelism. However, the Block Lower Triangular (BLT) transformation exposes
226-
independent blocks. This is then further impoved by the tearing process, which
193+
independent blocks. This is then further improved by the tearing process, which
227194
removes 90% of the equations and transforms the nonlinear equations into 50
228195
independent blocks *which can now all be solved in parallel*. The conclusion
229196
is that, your attempts to parallelize are neigh: performing parallelism after

0 commit comments

Comments
 (0)