Skip to content

Commit 3f29f9c

Browse files
authored
Merge branch 'main' into smc/test
2 parents 51f6885 + ba8d973 commit 3f29f9c

File tree

8 files changed

+119
-110
lines changed

8 files changed

+119
-110
lines changed

.github/workflows/Downgrade.yml

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,30 @@ on:
77
- 'docs/**'
88
push:
99
branches:
10-
- main
10+
- master
1111
paths-ignore:
1212
- 'docs/**'
13-
schedule:
14-
- cron: '44 4 * * 4'
1513
jobs:
1614
test:
17-
runs-on: ${{ matrix.os }}
18-
env:
19-
GROUP: ${{ matrix.group }}
15+
runs-on: ubuntu-latest
2016
strategy:
21-
fail-fast: false
2217
matrix:
2318
group:
2419
- Core
25-
version:
26-
- '1.10'
27-
os:
28-
- ubuntu-latest
29-
- macos-latest
30-
- windows-latest
20+
downgrade_mode: ['alldeps']
21+
julia-version: ['1.10']
3122
steps:
3223
- uses: actions/checkout@v4
3324
- uses: julia-actions/setup-julia@v2
3425
with:
35-
version: ${{ matrix.version }}
36-
- uses: julia-actions/julia-downgrade-compat@v1
26+
version: ${{ matrix.julia-version }}
27+
- uses: julia-actions/julia-downgrade-compat@v2
28+
# if: ${{ matrix.version == '1.6' }}
3729
with:
3830
skip: Pkg,TOML
39-
- uses: julia-actions/cache@v2
40-
with:
41-
token: ${{ secrets.GITHUB_TOKEN }}
4231
- uses: julia-actions/julia-buildpkg@v1
4332
- uses: julia-actions/julia-runtest@v1
33+
with:
34+
ALLOW_RERESOLVE: false
35+
env:
36+
GROUP: ${{ matrix.group }}

.github/workflows/Invalidations.yml

Lines changed: 0 additions & 15 deletions
This file was deleted.

.typos.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
11
[default.extend-words]
22
nin = "nin"
3+
# Additional SciML terms
4+
setp = "setp"
5+
getp = "getp"
6+
indexin = "indexin"
7+
ists = "ists"
8+
ispcs = "ispcs"
9+
eqs = "eqs"
10+
rhs = "rhs"
11+
MTK = "MTK"
12+
13+
# Julia data handling terms
14+
Missings = "Missings" # Julia's Missing data type (plural form)

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
## Breaking changes
44

5-
- The `NeuralNetworkBlock` no longer uses `RealInputArray` & `RealOutputArray`,
6-
the ports are now `inputs` and `outputs` and they are normal vector variables.
5+
- The `NeuralNetworkBlock` no longer uses `RealInputArray` & `RealOutputArray`,
6+
the ports are now `inputs` and `outputs` and they are normal vector variables.

docs/Project.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,27 @@ SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"
2020
SymbolicRegression = "8254be44-1295-4e6a-a16d-46603ac705cb"
2121
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
2222

23-
[sources]
24-
ModelingToolkitNeuralNets = {path = ".."}
23+
[sources.ModelingToolkitNeuralNets]
24+
path = ".."
2525

2626
[compat]
2727
Documenter = "1.3"
28+
LineSearches = "7"
2829
Lux = "1"
2930
ModelingToolkit = "10"
3031
ModelingToolkitNeuralNets = "2"
3132
ModelingToolkitStandardLibrary = "2.7"
3233
Optimization = "4.0"
34+
OptimizationOptimJL = "0.4"
3335
OptimizationOptimisers = "0.3"
36+
OrdinaryDiffEqTsit5 = "1"
37+
OrdinaryDiffEqVerner = "1"
3438
Plots = "1"
39+
SciMLBase = "2"
40+
SciMLSensitivity = "7"
3541
SciMLStructures = "1.1.0"
3642
StableRNGs = "1"
3743
Statistics = "1"
3844
SymbolicIndexingInterface = "0.3.15"
45+
SymbolicRegression = "1"
46+
Zygote = "0.7"

docs/src/nnblock.md

Lines changed: 78 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
[`ModelingToolkitNeuralNets`](https://github.com/SciML/ModelingToolkitNeuralNets.jl) provides 2 main interfaces for representing neural networks symbolically:
44

5-
* The [`NeuralNetworkBlock`](@ref), which represents the neural network as a block component
6-
* The [`SymbolicNeuralNetwork`](@ref), which represents the neural network via callable parameters
5+
- The [`NeuralNetworkBlock`](@ref), which represents the neural network as a block component
6+
- The [`SymbolicNeuralNetwork`](@ref), which represents the neural network via callable parameters
77

88
This tutorial will introduce the [`NeuralNetworkBlock`](@ref). This representation is useful in the context of hierarchical acausal component-based model.
99

1010
For such models we have a component representation that is converted to a a differential-algebraic equation (DAE) system, where the algebraic equations are given by the constraints and equalities between different component variables.
11-
The process of going from the component representation to the full DAE system at the end is reffered to as [structural simplification](https://docs.sciml.ai/ModelingToolkit/stable/API/model_building/#System-simplification).
12-
In order to formulate Universal Differential Equations (UDEs) in this context, we could operate eiter operate before the structural simplification step or after that, on the
11+
The process of going from the component representation to the full DAE system at the end is referred to as [structural simplification](https://docs.sciml.ai/ModelingToolkit/stable/API/model_building/#System-simplification).
12+
In order to formulate Universal Differential Equations (UDEs) in this context, we could operate either operate before the structural simplification step or after that, on the
1313
resulting DAE system. We call these the component UDE formulation and the system UDE formulation.
1414

1515
The advantage of the component UDE formulation is that it allows us to represent the model
@@ -46,13 +46,13 @@ input_f(t) = (1+sin(0.005 * t^2))/2
4646
C2 = 15
4747
end
4848
@components begin
49-
input = Blocks.TimeVaryingFunction(f=input_f)
50-
source = PrescribedHeatFlow(T_ref=373.15)
51-
plate = HeatCapacitor(C=C1, T=273.15)
52-
pot = HeatCapacitor(C=C2, T=273.15)
53-
conduction = ThermalConductor(G=1)
54-
air = ThermalConductor(G=0.1)
55-
env = FixedTemperature(T=293.15)
49+
input = Blocks.TimeVaryingFunction(f = input_f)
50+
source = PrescribedHeatFlow(T_ref = 373.15)
51+
plate = HeatCapacitor(C = C1, T = 273.15)
52+
pot = HeatCapacitor(C = C2, T = 273.15)
53+
conduction = ThermalConductor(G = 1)
54+
air = ThermalConductor(G = 0.1)
55+
env = FixedTemperature(T = 293.15)
5656
Tsensor = TemperatureSensor()
5757
end
5858
@equations begin
@@ -70,11 +70,11 @@ end
7070
C2 = 15
7171
end
7272
@components begin
73-
input = Blocks.TimeVaryingFunction(f=input_f)
74-
source = PrescribedHeatFlow(T_ref=373.15)
75-
pot = HeatCapacitor(C=C2, T=273.15)
76-
air = ThermalConductor(G=0.1)
77-
env = FixedTemperature(T=293.15)
73+
input = Blocks.TimeVaryingFunction(f = input_f)
74+
source = PrescribedHeatFlow(T_ref = 373.15)
75+
pot = HeatCapacitor(C = C2, T = 273.15)
76+
air = ThermalConductor(G = 0.1)
77+
env = FixedTemperature(T = 293.15)
7878
Tsensor = TemperatureSensor()
7979
end
8080
@equations begin
@@ -91,11 +91,11 @@ end
9191
## solve and plot the temperature of the pot in the 2 systems
9292
9393
prob1 = ODEProblem(sys1, Pair[], (0, 100.0))
94-
sol1 = solve(prob1, Tsit5(), reltol=1e-6)
94+
sol1 = solve(prob1, Tsit5(), reltol = 1e-6)
9595
prob2 = ODEProblem(sys2, Pair[], (0, 100.0))
96-
sol2 = solve(prob2, Tsit5(), reltol=1e-6)
97-
plot(sol1, idxs=sys1.pot.T, label="pot.T in original system")
98-
plot!(sol2, idxs=sys1.pot.T, label="pot.T in simplified system")
96+
sol2 = solve(prob2, Tsit5(), reltol = 1e-6)
97+
plot(sol1, idxs = sys1.pot.T, label = "pot.T in original system")
98+
plot!(sol2, idxs = sys1.pot.T, label = "pot.T in simplified system")
9999
```
100100

101101
If we take a closer look at the 2 models, the original system has 2 unknowns,
@@ -105,6 +105,7 @@ unknowns(sys1)
105105
```
106106

107107
while the simplified system only has 1 unknown
108+
108109
```@example potplate
109110
unknowns(sys2)
110111
```
@@ -127,12 +128,13 @@ always output positive numbers for positive inputs, so this also makes physical
127128
begin
128129
n_input = 2
129130
n_output = 1
130-
chain = multi_layer_feed_forward(; n_input, n_output, depth=1, width=4, activation=Lux.swish)
131+
chain = multi_layer_feed_forward(;
132+
n_input, n_output, depth = 1, width = 4, activation = Lux.swish)
131133
end
132134
@components begin
133135
port_a = HeatPort()
134136
port_b = HeatPort()
135-
nn = NeuralNetworkBlock(; n_input, n_output, chain, rng=StableRNG(1337))
137+
nn = NeuralNetworkBlock(; n_input, n_output, chain, rng = StableRNG(1337))
136138
end
137139
@parameters begin
138140
T0 = 273.15
@@ -160,11 +162,11 @@ end
160162
C2 = 15
161163
end
162164
@components begin
163-
input = Blocks.TimeVaryingFunction(f=input_f)
164-
source = PrescribedHeatFlow(T_ref=373.15)
165-
pot = HeatCapacitor(C=C2, T=273.15)
166-
air = ThermalConductor(G=0.1)
167-
env = FixedTemperature(T=293.15)
165+
input = Blocks.TimeVaryingFunction(f = input_f)
166+
source = PrescribedHeatFlow(T_ref = 373.15)
167+
pot = HeatCapacitor(C = C2, T = 273.15)
168+
air = ThermalConductor(G = 0.1)
169+
env = FixedTemperature(T = 293.15)
168170
Tsensor = TemperatureSensor()
169171
thermal_nn = ThermalNN()
170172
end
@@ -181,19 +183,19 @@ end
181183
@named model = NeuralPot()
182184
sys3 = mtkcompile(model)
183185
184-
# Let's check that we can succesfully simulate the system in the
186+
# Let's check that we can successfully simulate the system in the
185187
# initial state
186188
prob3 = ODEProblem(sys3, Pair[], (0, 100.0))
187-
sol3 = solve(prob3, Tsit5(), abstol=1e-6, reltol=1e-6)
189+
sol3 = solve(prob3, Tsit5(), abstol = 1e-6, reltol = 1e-6)
188190
@assert SciMLBase.successful_retcode(sol3)
189191
```
190192

191193
Now that we have the system with the embedded neural network, we can start training the network.
192194
The training will be formulated as an optimization problem where we will minimize the mean absolute squared distance
193195
between the predictions of the new system and the data obtained from the original system.
194196
In order to gain some insight into the training process we will also add a callback that will plot various quantities
195-
in the system versus their equivalents in the original system. In a more realistic scenarion we would not have access
196-
to the original system, but we could still monitor how well we fit the traning data and the system predictions.
197+
in the system versus their equivalents in the original system. In a more realistic scenario we would not have access
198+
to the original system, but we could still monitor how well we fit the training data and the system predictions.
197199

198200
```@example potplate
199201
using SymbolicIndexingInterface
@@ -209,23 +211,28 @@ x0 = prob3.ps[tp]
209211
210212
oop_update = setsym_oop(prob3, tp);
211213
212-
plot_cb = (opt_state, loss) -> begin
214+
plot_cb = (opt_state,
215+
loss) -> begin
213216
opt_state.iter % 1000 ≠ 0 && return false
214217
@info "step $(opt_state.iter), loss: $loss"
215218
216219
(new_u0, new_p) = oop_update(prob3, opt_state.u)
217-
new_prob = remake(prob3, u0=new_u0, p=new_p)
218-
sol = solve(new_prob, Tsit5(), abstol=1e-8, reltol=1e-8)
219-
220-
plt = plot(sol, layout=(2,3), idxs=[
221-
sys3.thermal_nn.nn.inputs[1], sys3.thermal_nn.x,
222-
sys3.thermal_nn.nn.outputs[1], sys3.thermal_nn.port_b.T,
223-
sys3.pot.T, sys3.pot.port.Q_flow],
224-
size=(950,800))
225-
plot!(plt, sol1, idxs=[
226-
(sys1.conduction.port_a.T-273.15)/10, sys1.conduction.port_a.T,
227-
sys1.conduction.port_a.Q_flow, sys1.conduction.port_b.T,
228-
sys1.pot.T, sys1.pot.port.Q_flow])
220+
new_prob = remake(prob3, u0 = new_u0, p = new_p)
221+
sol = solve(new_prob, Tsit5(), abstol = 1e-8, reltol = 1e-8)
222+
223+
plt = plot(sol,
224+
layout = (2, 3),
225+
idxs = [
226+
sys3.thermal_nn.nn.inputs[1], sys3.thermal_nn.x,
227+
sys3.thermal_nn.nn.outputs[1], sys3.thermal_nn.port_b.T,
228+
sys3.pot.T, sys3.pot.port.Q_flow],
229+
size = (950, 800))
230+
plot!(plt,
231+
sol1,
232+
idxs = [
233+
(sys1.conduction.port_a.T-273.15)/10, sys1.conduction.port_a.T,
234+
sys1.conduction.port_a.Q_flow, sys1.conduction.port_b.T,
235+
sys1.pot.T, sys1.pot.port.Q_flow])
229236
display(plt)
230237
false
231238
end
@@ -236,7 +243,8 @@ function cost(x, opt_ps)
236243
u0, p = oop_update(prob, x)
237244
new_prob = remake(prob; u0, p)
238245
239-
new_sol = solve(new_prob, Tsit5(), saveat=ts, abstol=1e-8, reltol=1e-8, verbose=false, sensealg=GaussAdjoint())
246+
new_sol = solve(new_prob, Tsit5(), saveat = ts, abstol = 1e-8,
247+
reltol = 1e-8, verbose = false, sensealg = GaussAdjoint())
240248
241249
!SciMLBase.successful_retcode(new_sol) && return Inf
242250
@@ -249,39 +257,41 @@ data = sol1[sys1.pot.T]
249257
get_T = getsym(prob3, sys3.pot.T)
250258
opt_ps = (prob3, oop_update, data, sol1.t, get_T);
251259
252-
op = OptimizationProblem(of, x0, opt_ps,)
260+
op = OptimizationProblem(of, x0, opt_ps)
253261
254-
res = solve(op, Adam(); maxiters=10_000, callback=plot_cb)
262+
res = solve(op, Adam(); maxiters = 10_000, callback = plot_cb)
255263
op2 = OptimizationProblem(of, res.u, opt_ps)
256-
res2 = solve(op2, LBFGS(linesearch=BackTracking()); maxiters=2000, callback=plot_cb)
264+
res2 = solve(op2, LBFGS(linesearch = BackTracking()); maxiters = 2000, callback = plot_cb)
257265
258266
(new_u0, new_p) = oop_update(prob3, res2.u)
259-
new_prob1 = remake(prob3, u0=new_u0, p=new_p)
260-
new_sol1 = solve(new_prob1, Tsit5(), abstol=1e-6, reltol=1e-6)
261-
262-
plt = plot(new_sol1, layout=(2,3), idxs=[
263-
sys3.thermal_nn.nn.inputs[1], sys3.thermal_nn.x,
264-
sys3.thermal_nn.nn.outputs[1], sys3.thermal_nn.port_b.T,
265-
sys3.pot.T, sys3.pot.port.Q_flow],
266-
size=(950,800))
267-
plot!(plt, sol1, idxs=[
268-
(sys1.conduction.port_a.T-273.15)/10, sys1.conduction.port_a.T,
269-
sys1.conduction.port_a.Q_flow, sys1.conduction.port_b.T,
270-
sys1.pot.T, sys1.pot.port.Q_flow], ls=:dash)
267+
new_prob1 = remake(prob3, u0 = new_u0, p = new_p)
268+
new_sol1 = solve(new_prob1, Tsit5(), abstol = 1e-6, reltol = 1e-6)
269+
270+
plt = plot(new_sol1,
271+
layout = (2, 3),
272+
idxs = [
273+
sys3.thermal_nn.nn.inputs[1], sys3.thermal_nn.x,
274+
sys3.thermal_nn.nn.outputs[1], sys3.thermal_nn.port_b.T,
275+
sys3.pot.T, sys3.pot.port.Q_flow],
276+
size = (950, 800))
277+
plot!(plt,
278+
sol1,
279+
idxs = [
280+
(sys1.conduction.port_a.T-273.15)/10, sys1.conduction.port_a.T,
281+
sys1.conduction.port_a.Q_flow, sys1.conduction.port_b.T,
282+
sys1.pot.T, sys1.pot.port.Q_flow],
283+
ls = :dash)
271284
```
272285

273286
As we can see from the final plot, the neural network fits very well and not only the training data fits, but also the rest of the
274287
predictions of the system match the original system. Let us also compare against the predictions of the incomplete system:
275288

276289
```@example potplate
277-
(new_u0, new_p) = oop_update(prob3, res2.u)
278-
new_prob1 = remake(prob3, u0=new_u0, p=new_p)
279-
new_sol1 = solve(new_prob1, Tsit5(), abstol=1e-6, reltol=1e-6)
280-
281-
plot(sol1, label=["original sys: pot T" "original sys: plate T"], lw=3)
282-
plot!(sol3; idxs=[sys3.pot.T], label="untrained UDE", lw=2.5)
283-
plot!(sol2; idxs=[sys2.pot.T], label="incomplete sys: pot T", lw=2.5)
284-
plot!(new_sol1; idxs=[sys3.pot.T, sys3.thermal_nn.x], label="trained UDE", ls=:dash, lw=2.5)
290+
plot(sol1, label = ["original sys: pot T" "original sys: plate T"], lw = 3)
291+
plot!(sol3; idxs = [sys3.pot.T], label = "untrained UDE", lw = 2.5)
292+
plot!(sol2; idxs = [sys2.pot.T], label = "incomplete sys: pot T", lw = 2.5)
293+
plot!(new_sol1; idxs = [sys3.pot.T, sys3.thermal_nn.x],
294+
label = "trained UDE", ls = :dash, lw = 2.5)
285295
```
286296

287297
Now that our neural network is trained, we can go a step further and use [`SymbolicRegression.jl`](https://github.com/MilesCranmer/SymbolicRegression.jl) to find

src/utils.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
depth::Int = 1, activation = tanh, use_bias = true, initial_scaling_factor = 1e-8)
44
55
Create a Lux.jl `Chain` for use in [`NeuralNetworkBlock`](@ref)s. The weights of the last layer
6-
are multipled by the `initial_scaling_factor` in order to make the initial contribution
7-
of the network small and thus help with acheiving a stable starting position for the training.
6+
are multiplied by the `initial_scaling_factor` in order to make the initial contribution
7+
of the network small and thus help with achieving a stable starting position for the training.
88
"""
99
function multi_layer_feed_forward(; n_input, n_output, width::Int = 4,
1010
depth::Int = 1, activation = tanh, use_bias = true, initial_scaling_factor = 1e-8)
1111
Lux.Chain(
1212
Lux.Dense(n_input, width, activation; use_bias),
1313
[Lux.Dense(width, width, activation; use_bias) for _ in 1:(depth)]...,
1414
Lux.Dense(width, n_output;
15-
init_weight = (rng, a...) -> initial_scaling_factor *
16-
Lux.kaiming_uniform(rng, a...), use_bias)
15+
init_weight = (
16+
rng, a...) -> initial_scaling_factor *
17+
Lux.kaiming_uniform(rng, a...), use_bias)
1718
)
1819
end
1920

0 commit comments

Comments
 (0)