Skip to content

Commit 8b58588

Browse files
authored
domain documentation and tests (#2272)
1 parent 869a825 commit 8b58588

File tree

6 files changed

+356
-7
lines changed

6 files changed

+356
-7
lines changed

.github/workflows/Documentation.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ jobs:
1515
- uses: julia-actions/setup-julia@latest
1616
with:
1717
version: '1'
18+
- run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev
1819
- name: Install dependencies
19-
run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
20+
run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
2021
- name: Build and deploy
2122
env:
2223
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token
2324
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key
24-
run: julia --project=docs/ --code-coverage=user docs/make.jl
25+
run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs/ --code-coverage=user docs/make.jl
2526
- uses: julia-actions/julia-processcoverage@v1
2627
- uses: codecov/codecov-action@v3
2728
with:

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
55
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
66
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
77
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
8+
ModelingToolkitDesigner = "23d639d0-9462-4d1e-84fe-d700424865b8"
89
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
910
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
1011
Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"

docs/pages.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ pages = [
66
"tutorials/optimization.md",
77
"tutorials/modelingtoolkitize.md",
88
"tutorials/stochastic_diffeq.md",
9-
"tutorials/parameter_identifiability.md"],
9+
"tutorials/parameter_identifiability.md",
10+
"tutorials/domain_connections.md"],
1011
"Examples" => Any["Basic Examples" => Any["examples/higher_order.md",
1112
"examples/spring_mass.md",
1213
"examples/modelingtoolkitize_index_reduction.md",
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
# [Domains](@id domains)
2+
3+
## Basics
4+
5+
A domain in ModelingToolkit.jl is a network of connected components that share properties of the medium in the network. For example, a collection of hydraulic components connected together will have a fluid medium. Using the domain feature, one only needs to define and set the fluid medium properties once, in one component, rather than at each component. The way this works in ModelingToolkit.jl is by defining a connector (with Through/Flow and Across variables) with parameters defining the medium of the domain. Then a second connector is defined, with the same parameters, and the same Through/Flow variable, which acts as the setter. For example, a hydraulic domain may have a hydraulic connector, `HydraulicPort`, that defines a fluid medium with density (`ρ`), viscosity (`μ`), and a bulk modulus (`β`), a through/flow variable mass flow (`dm`) and an across variable pressure (`p`).
6+
7+
```@example domain
8+
using ModelingToolkit
9+
10+
@parameters t
11+
D = Differential(t)
12+
13+
@connector function HydraulicPort(; p_int, name)
14+
pars = @parameters begin
15+
ρ
16+
β
17+
μ
18+
end
19+
20+
vars = @variables begin
21+
p(t) = p_int
22+
dm(t), [connect = Flow]
23+
end
24+
25+
ODESystem(Equation[], t, vars, pars; name, defaults = [dm => 0])
26+
end
27+
nothing #hide
28+
```
29+
30+
The fluid medium setter for `HydralicPort` may be defined as `HydraulicFluid` with the same parameters and through/flow variable. But now, the parameters can be set through the function keywords.
31+
32+
```@example domain
33+
@connector function HydraulicFluid(;
34+
density = 997,
35+
bulk_modulus = 2.09e9,
36+
viscosity = 0.0010016,
37+
name)
38+
pars = @parameters begin
39+
ρ = density
40+
β = bulk_modulus
41+
μ = viscosity
42+
end
43+
44+
vars = @variables begin
45+
dm(t), [connect = Flow]
46+
end
47+
48+
eqs = [
49+
dm ~ 0,
50+
]
51+
52+
ODESystem(eqs, t, vars, pars; name, defaults = [dm => 0])
53+
end
54+
nothing #hide
55+
```
56+
57+
Now, we can connect a `HydraulicFluid` component to any `HydraulicPort` connector, and the parameters of all `HydraulicPort`'s in the network will be automatically set. Let's consider a simple example, connecting a pressure source component to a volume component. Note that we don't need to define density for the volume component, it's supplied by the `HydraulicPort` (`port.ρ`).
58+
59+
```@example domain
60+
@component function FixedPressure(; p, name)
61+
pars = @parameters p = p
62+
systems = @named begin
63+
port = HydraulicPort(; p_int = p)
64+
end
65+
66+
eqs = [port.p ~ p]
67+
68+
ODESystem(eqs, t, [], pars; name, systems)
69+
end
70+
71+
@component function FixedVolume(; vol, p_int, name)
72+
pars = @parameters begin
73+
p_int = p_int
74+
vol = vol
75+
end
76+
77+
systems = @named begin
78+
port = HydraulicPort(; p_int)
79+
end
80+
81+
vars = @variables begin
82+
rho(t) = port.ρ
83+
drho(t) = 0
84+
end
85+
86+
# let
87+
dm = port.dm
88+
p = port.p
89+
90+
eqs = [D(rho) ~ drho
91+
rho ~ port.ρ * (1 + p / port.β)
92+
dm ~ drho * vol]
93+
94+
ODESystem(eqs, t, vars, pars; name, systems)
95+
end
96+
nothing #hide
97+
```
98+
99+
When the system is defined we can generate a fluid component and connect it to the system. Here `fluid` is connected to the `src.port`, but it could also be connected to `vol.port`, any connection in the network is fine. Note: we can visualize the system using `ModelingToolkitDesigner.jl`, where a dashed line is used to show the `fluid` connection to represent a domain connection that is only transporting parameters and not states.
100+
101+
```@example domain
102+
@component function System(; name)
103+
systems = @named begin
104+
src = FixedPressure(; p = 200e5)
105+
vol = FixedVolume(; vol = 0.1, p_int = 200e5)
106+
107+
fluid = HydraulicFluid(; density = 876)
108+
end
109+
110+
eqs = [connect(fluid, src.port)
111+
connect(src.port, vol.port)]
112+
113+
ODESystem(eqs, t, [], []; systems, name)
114+
end
115+
116+
@named odesys = System()
117+
nothing #hide
118+
```
119+
120+
```@setup domain
121+
# code to generate diagrams...
122+
# using ModelingToolkitDesigner
123+
# path = raw"C:\Work\Assets\ModelingToolkit.jl\domain_connections"
124+
# design = ODESystemDesign(odesys, path);
125+
126+
# using CairoMakie
127+
# CairoMakie.set_theme!(Theme(;fontsize=12))
128+
# fig = ModelingToolkitDesigner.view(design, false)
129+
# save(joinpath(path, "odesys.svg"), fig; resolution=(300,300))
130+
```
131+
132+
![odesys](https://github.com/SciML/ModelingToolkit.jl/assets/40798837/d19fbcf4-781c-4743-87b7-30bed348ff98)
133+
134+
To see how the domain works, we can examine the set parameter values for each of the ports `src.port` and `vol.port`. First we assemble the system using `structural_simplify()` and then check the default value of `vol.port.ρ`, whichs points to the setter value `fluid₊ρ`. Likewise, `src.port.ρ`, will also point to the setter value `fluid₊ρ`. Therefore, there is now only 1 defined density value `fluid₊ρ` which sets the density for the connected network.
135+
136+
```@repl domain
137+
sys = structural_simplify(odesys)
138+
ModelingToolkit.defaults(sys)[complete(odesys).vol.port.ρ]
139+
```
140+
141+
## Multiple Domain Networks
142+
143+
If we have a more complicated system, for example a hydraulic actuator, with a separated fluid on both sides of the piston, it's possible we might have 2 separate domain networks. In this case we can connect 2 separate fluids, or the same fluid, to both networks. First a simple actuator is defined with 2 ports.
144+
145+
```@example domain
146+
@component function Actuator(; p_int, mass, area, name)
147+
pars = @parameters begin
148+
p_int = p_int
149+
mass = mass
150+
area = area
151+
end
152+
153+
systems = @named begin
154+
port_a = HydraulicPort(; p_int)
155+
port_b = HydraulicPort(; p_int)
156+
end
157+
158+
vars = @variables begin
159+
x(t) = 0
160+
dx(t) = 0
161+
ddx(t) = 0
162+
end
163+
164+
eqs = [D(x) ~ dx
165+
D(dx) ~ ddx
166+
mass * ddx ~ (port_a.p - port_b.p) * area
167+
port_a.dm ~ +(port_a.ρ) * dx * area
168+
port_b.dm ~ -(port_b.ρ) * dx * area]
169+
170+
ODESystem(eqs, t, vars, pars; name, systems)
171+
end
172+
nothing #hide
173+
```
174+
175+
A system with 2 different fluids is defined and connected to each separate domain network.
176+
177+
```@example domain
178+
@component function ActuatorSystem2(; name)
179+
systems = @named begin
180+
src_a = FixedPressure(; p = 200e5)
181+
src_b = FixedPressure(; p = 200e5)
182+
act = Actuator(; p_int = 200e5, mass = 1000, area = 0.1)
183+
184+
fluid_a = HydraulicFluid(; density = 876)
185+
fluid_b = HydraulicFluid(; density = 999)
186+
end
187+
188+
eqs = [connect(fluid_a, src_a.port)
189+
connect(fluid_b, src_b.port)
190+
connect(src_a.port, act.port_a)
191+
connect(src_b.port, act.port_b)]
192+
193+
ODESystem(eqs, t, [], []; systems, name)
194+
end
195+
196+
@named actsys2 = ActuatorSystem2()
197+
nothing #hide
198+
```
199+
200+
```@setup domain
201+
# design = ODESystemDesign(actsys2, path);
202+
# fig = ModelingToolkitDesigner.view(design, false)
203+
# save(joinpath(path, "actsys2.svg"), fig; resolution=(500,300))
204+
```
205+
206+
![actsys2](https://github.com/SciML/ModelingToolkit.jl/assets/40798837/8ed50035-f6ac-48cb-a585-1ef415154a02)
207+
208+
After running `structural_simplify()` on `actsys2`, the defaults will show that `act.port_a.ρ` points to `fluid_a₊ρ` and `act.port_b.ρ` points to `fluid_b₊ρ`. This is a special case, in most cases a hydraulic system will have only 1 fluid, however this simple system has 2 separate domain networks. Therefore, we can connect a single fluid to both networks. This does not interfer with the mathmatical equations of the system, since no states are connected.
209+
210+
```@example domain
211+
@component function ActuatorSystem1(; name)
212+
systems = @named begin
213+
src_a = FixedPressure(; p = 200e5)
214+
src_b = FixedPressure(; p = 200e5)
215+
act = Actuator(; p_int = 200e5, mass = 1000, area = 0.1)
216+
217+
fluid = HydraulicFluid(; density = 876)
218+
end
219+
220+
eqs = [connect(fluid, src_a.port)
221+
connect(fluid, src_b.port)
222+
connect(src_a.port, act.port_a)
223+
connect(src_b.port, act.port_b)]
224+
225+
ODESystem(eqs, t, [], []; systems, name)
226+
end
227+
228+
@named actsys1 = ActuatorSystem1()
229+
nothing #hide
230+
```
231+
232+
```@setup domain
233+
# design = ODESystemDesign(actsys1, path);
234+
# fig = ModelingToolkitDesigner.view(design, false)
235+
# save(joinpath(path, "actsys1.svg"), fig; resolution=(500,300))
236+
```
237+
238+
![actsys1](https://github.com/SciML/ModelingToolkit.jl/assets/40798837/054404eb-dbb7-4b85-95c0-c9503d0c4d00)
239+
240+
## Special Connection Cases (`domain_connect()`)
241+
242+
In some cases a component will be defined with 2 connectors of the same domain, but they are not connected. For example the `Restrictor` defined here gives equations to define the behavior of how the 2 connectors `port_a` and `port_b` are physcially connected.
243+
244+
```@example domain
245+
@component function Restrictor(; name, p_int)
246+
pars = @parameters begin
247+
K = 0.1
248+
p_int = p_int
249+
end
250+
251+
systems = @named begin
252+
port_a = HydraulicPort(; p_int)
253+
port_b = HydraulicPort(; p_int)
254+
end
255+
256+
eqs = [port_a.dm ~ (port_a.p - port_b.p) * K
257+
0 ~ port_a.dm + port_b.dm]
258+
259+
ODESystem(eqs, t, [], pars; systems, name)
260+
end
261+
nothing #hide
262+
```
263+
264+
Adding the `Restrictor` to the original system example will cause a break in the domain network, since a `connect(port_a, port_b)` is not defined.
265+
266+
```@example domain
267+
@component function RestrictorSystem(; name)
268+
systems = @named begin
269+
src = FixedPressure(; p = 200e5)
270+
res = Restrictor(; p_int = 200e5)
271+
vol = FixedVolume(; vol = 0.1, p_int = 200e5)
272+
273+
fluid = HydraulicFluid(; density = 876)
274+
end
275+
276+
eqs = [connect(fluid, src.port)
277+
connect(src.port, res.port_a)
278+
connect(res.port_b, vol.port)]
279+
280+
ODESystem(eqs, t, [], []; systems, name)
281+
end
282+
283+
@named ressys = RestrictorSystem()
284+
sys = structural_simplify(ressys)
285+
nothing #hide
286+
```
287+
288+
```@setup domain
289+
# design = ODESystemDesign(ressys, path);
290+
# fig = ModelingToolkitDesigner.view(design, false)
291+
# save(joinpath(path, "ressys.svg"), fig; resolution=(500,300))
292+
```
293+
294+
![ressys](https://github.com/SciML/ModelingToolkit.jl/assets/40798837/3740f0e2-7324-4c1f-af8b-eba02cfece81)
295+
296+
When `structural_simplify()` is applied to this system it can be seen that the defaults are missing for `res.port_b` and `vol.port`.
297+
298+
```@repl domain
299+
ModelingToolkit.defaults(sys)[complete(ressys).res.port_a.ρ]
300+
ModelingToolkit.defaults(sys)[complete(ressys).res.port_b.ρ]
301+
ModelingToolkit.defaults(sys)[complete(ressys).vol.port.ρ]
302+
```
303+
304+
To ensure that the `Restrictor` component does not disrupt the domain network, the [`domain_connect()`](@ref) function can be used, which explicitly only connects the domain network and not the states.
305+
306+
```@example domain
307+
@component function Restrictor(; name, p_int)
308+
pars = @parameters begin
309+
K = 0.1
310+
p_int = p_int
311+
end
312+
313+
systems = @named begin
314+
port_a = HydraulicPort(; p_int)
315+
port_b = HydraulicPort(; p_int)
316+
end
317+
318+
eqs = [domain_connect(port_a, port_b) # <-- connect the domain network
319+
port_a.dm ~ (port_a.p - port_b.p) * K
320+
0 ~ port_a.dm + port_b.dm]
321+
322+
ODESystem(eqs, t, [], pars; systems, name)
323+
end
324+
325+
@named ressys = RestrictorSystem()
326+
sys = structural_simplify(ressys)
327+
nothing #hide
328+
```
329+
330+
Now that the `Restrictor` component is properly defined using `domain_connect()`, the defaults for `res.port_b` and `vol.port` are properly defined.
331+
332+
```@repl domain
333+
ModelingToolkit.defaults(sys)[complete(ressys).res.port_a.ρ]
334+
ModelingToolkit.defaults(sys)[complete(ressys).res.port_b.ρ]
335+
ModelingToolkit.defaults(sys)[complete(ressys).vol.port.ρ]
336+
```

src/systems/connectors.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
domain_connect(sys1, sys2, syss...)
3+
4+
Adds a domain only connection equation, through and across state equations are not generated.
5+
"""
16
function domain_connect(sys1, sys2, syss...)
27
syss = (sys1, sys2, syss...)
38
length(unique(nameof, syss)) == length(syss) || error("connect takes distinct systems!")

0 commit comments

Comments
 (0)