Skip to content

Commit 8e183ac

Browse files
feat: add new Symbolics jacobian benchmark
1 parent 58e54e7 commit 8e183ac

File tree

2 files changed

+356
-0
lines changed

2 files changed

+356
-0
lines changed

benchmarks/Symbolics/Project.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[deps]
2+
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
3+
JuliaSimCompiler = "8391cb6b-4921-5777-4e45-fd9aab8cb88d"
4+
JuliaSimCompilerRuntime = "9cbdfd5a-25c0-4dde-8b55-206f91b28bd9"
5+
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
6+
ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739"
7+
Multibody = "e1cad5d1-98ef-44f9-a79a-9ca4547f95b9"
8+
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
9+
Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45"
10+
PreferenceTools = "ba661fbb-e901-4445-b070-854aec6bfbc5"
11+
SciMLBenchmarks = "31c91b34-3c75-11e9-0341-95557aab0344"
12+
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
13+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
14+
XSteam = "95ff35a0-be81-11e9-2ca3-5b4e338e8476"
15+
16+
[compat]
17+
CairoMakie = "0.11.11"
18+
ModelingToolkit = "9.20.0"
19+
ModelingToolkitStandardLibrary = "2"
20+
Polynomials = "4.0.8"
21+
PreferenceTools = "0.1.2"
22+
SciMLBenchmarks = "0.1.3"
23+
SymbolicUtils = "3.11"
24+
Symbolics = "6.23"

benchmarks/Symbolics/ThermalFluid.jmd

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
---
2+
title: Symbolic Manipulation - Large Jacobians
3+
author: Aayush Sabharwal, Bowen Zhu, Chris Rackauckas
4+
---
5+
6+
This benchmark utilizes a thermal fluid ODE setup to benchmark the scaling of
7+
symbolic jacobian computation with and without hashconsing.
8+
9+
```julia
10+
using Pkg
11+
# Rev fixes precompilation https://github.com/hzgzh/XSteam.jl/pull/2
12+
Pkg.add(Pkg.PackageSpec(;name="XSteam", rev="f2a1c589054cfd6bba307985a3a534b6f5a1863b"))
13+
14+
using ModelingToolkit, JuliaSimCompiler, Symbolics, SymbolicUtils, XSteam, Polynomials, BenchmarkTools, OrdinaryDiffEq
15+
16+
using ModelingToolkit: t_nounits as t, D_nounits as D
17+
```
18+
19+
## Setup Julia Code
20+
21+
```julia
22+
# o o o o o o o < heat capacitors
23+
# | | | | | | | < heat conductors
24+
# o o o o o o o
25+
# | | | | | | |
26+
#Source -> o--o--o--o--o--o--o -> Sink
27+
# advection diff source PDE
28+
29+
m_flow_source(t) = 2.75
30+
T_source(t) = (t > 12 * 3600) * 56.0 + 12.0
31+
@register_symbolic m_flow_source(t)
32+
@register_symbolic T_source(t)
33+
34+
#build polynomial liquid-water property only dependent on Temperature
35+
p_l = 5 #bar
36+
T_vec = collect(1:1:150);
37+
@generated kin_visc_T(t) = :(Base.evalpoly(t, $(fit(T_vec, my_pT.(p_l, T_vec) ./ rho_pT.(p_l, T_vec), 5).coeffs...,)))
38+
@generated lambda_T(t) = :(Base.evalpoly(t, $(fit(T_vec, tc_pT.(p_l, T_vec), 3).coeffs...,)))
39+
@generated Pr_T(t) = :(Base.evalpoly(t, $(fit(T_vec, 1e3 * Cp_pT.(p_l, T_vec) .* my_pT.(p_l, T_vec) ./ tc_pT.(p_l, T_vec), 5).coeffs...,)))
40+
@generated rho_T(t) = :(Base.evalpoly(t, $(fit(T_vec, rho_pT.(p_l, T_vec), 4).coeffs...,)))
41+
@generated rhocp_T(t) = :(Base.evalpoly(t, $(fit(T_vec, 1000 * rho_pT.(p_l, T_vec) .* Cp_pT.(p_l, T_vec), 5).coeffs...,)))
42+
@register_symbolic kin_visc_T(t)
43+
@register_symbolic lambda_T(t)
44+
@register_symbolic Pr_T(t)
45+
@register_symbolic rho_T(t)
46+
@register_symbolic rhocp_T(t)
47+
48+
@connector function FluidPort(; name, p=101325.0, m=0.0, T=0.0)
49+
sts = @variables p(t) = p m(t) = m [connect = Flow] T(t) = T [connect = Stream]
50+
ODESystem(Equation[], t, sts, []; name=name)
51+
end
52+
53+
@connector function VectorHeatPort(; name, N=100, T0=0.0, Q0=0.0)
54+
sts = @variables (T(t))[1:N] = T0 (Q(t))[1:N] = Q0 [connect = Flow]
55+
ODESystem(Equation[], t, [T; Q], []; name=name)
56+
end
57+
58+
@register_symbolic Dxx_coeff(u, d, T)
59+
#Taylor-aris dispersion model
60+
function Dxx_coeff(u, d, T)
61+
Re = abs(u) * d / kin_visc_T(T) + 0.1
62+
if Re < 1000.0
63+
(d^2 / 4) * u^2 / 48 / 0.14e-6
64+
else
65+
d * u * (1.17e9 * Re^(-2.5) + 0.41)
66+
end
67+
end
68+
69+
@register_symbolic Nusselt(Re, Pr, f)
70+
#Nusselt number model
71+
function Nusselt(Re, Pr, f)
72+
if Re <= 2300.0
73+
3.66
74+
elseif Re <= 3100.0
75+
3.5239 * (Re / 1000)^4 - 45.158 * (Re / 1000)^3 + 212.13 * (Re / 1000)^2 - 427.45 * (Re / 1000) + 316.08
76+
else
77+
f / 8 * ((Re - 1000) * Pr) / (1 + 12.7 * (f / 8)^(1 / 2) * (Pr^(2 / 3) - 1))
78+
end
79+
end
80+
81+
@register_symbolic Churchill_f(Re, epsilon, d)
82+
#Darcy weisbach friction factor
83+
function Churchill_f(Re, epsilon, d)
84+
theta_1 = (-2.457 * log(((7 / Re)^0.9) + (0.27 * (epsilon / d))))^16
85+
theta_2 = (37530 / Re)^16
86+
8 * ((((8 / Re)^12) + (1 / ((theta_1 + theta_2)^1.5)))^(1 / 12))
87+
end
88+
89+
function FluidRegion(; name, L=1.0, dn=0.05, N=100, T0=0.0,
90+
lumped_T=50, diffusion=true, e=1e-4)
91+
@named inlet = FluidPort()
92+
@named outlet = FluidPort()
93+
@named heatport = VectorHeatPort(N=N)
94+
95+
dx = L / N
96+
c = [-1 / 8, -3 / 8, -3 / 8] # advection stencil coefficients
97+
A = pi * dn^2 / 4
98+
99+
p = @parameters C_shift = 0.0 Rw = 0.0 # stuff for latter
100+
@variables begin
101+
(T(t))[1:N] = fill(T0, N)
102+
Twall(t)[1:N] = fill(T0, N)
103+
(S(t))[1:N] = fill(T0, N)
104+
(C(t))[1:N] = fill(1.0, N)
105+
u(t) = 1e-6
106+
Re(t) = 1000.0
107+
Dxx(t) = 0.0
108+
Pr(t) = 1.0
109+
alpha(t) = 1.0
110+
f(t) = 1.0
111+
end
112+
113+
sts = vcat(T, Twall, S, C, Num[u], Num[Re], Num[Dxx], Num[Pr], Num[alpha], Num[f])
114+
115+
eqs = Equation[
116+
Re ~ 0.1 + dn * abs(u) / kin_visc_T(lumped_T)
117+
Pr ~ Pr_T(lumped_T)
118+
f ~ Churchill_f(Re, e, dn) #Darcy-weisbach
119+
alpha ~ Nusselt(Re, Pr, f) * lambda_T(lumped_T) / dn
120+
Dxx ~ diffusion * Dxx_coeff(u, dn, lumped_T)
121+
inlet.m ~ -outlet.m
122+
inlet.p ~ outlet.p
123+
inlet.T ~ instream(inlet.T)
124+
outlet.T ~ T[N]
125+
u ~ inlet.m / rho_T(inlet.T) / A
126+
[C[i] ~ dx * A * rhocp_T(T[i]) for i in 1:N]
127+
[S[i] ~ heatport.Q[i] for i in 1:N]
128+
[Twall[i] ~ heatport.T[i] for i in 1:N]
129+
130+
#source term
131+
[S[i] ~ (1 / (1 / (alpha * dn * pi * dx) + abs(Rw / 1000))) * (Twall[i] - T[i]) for i in 1:N]
132+
133+
#second order upwind + diffusion + source
134+
D(T[1]) ~ u / dx * (inlet.T - T[1]) + Dxx * (T[2] - T[1]) / dx^2 + S[1] / (C[1] - C_shift)
135+
D(T[2]) ~ u / dx * (c[1] * inlet.T - sum(c) * T[1] + c[2] * T[2] + c[3] * T[3]) + Dxx * (T[1] - 2 * T[2] + T[3]) / dx^2 + S[2] / (C[2] - C_shift)
136+
[D(T[i]) ~ u / dx * (c[1] * T[i-2] - sum(c) * T[i-1] + c[2] * T[i] + c[3] * T[i+1]) + Dxx * (T[i-1] - 2 * T[i] + T[i+1]) / dx^2 + S[i] / (C[i] - C_shift) for i in 3:N-1]
137+
D(T[N]) ~ u / dx * (T[N-1] - T[N]) + Dxx * (T[N-1] - T[N]) / dx^2 + S[N] / (C[N] - C_shift)
138+
]
139+
140+
ODESystem(eqs, t, sts, p; systems=[inlet, outlet, heatport], name=name)
141+
end
142+
143+
@register_symbolic Cn_circular_wall_inner(d, D, cp, ρ)
144+
function Cn_circular_wall_inner(d, D, cp, ρ)
145+
C = pi / 4 * (D^2 - d^2) * cp * ρ
146+
return C / 2
147+
end
148+
149+
@register_symbolic Cn_circular_wall_outer(d, D, cp, ρ)
150+
function Cn_circular_wall_outer(d, D, cp, ρ)
151+
C = pi / 4 * (D^2 - d^2) * cp * ρ
152+
return C / 2
153+
end
154+
155+
@register_symbolic Ke_circular_wall(d, D, λ)
156+
function Ke_circular_wall(d, D, λ)
157+
2 * pi * λ / log(D / d)
158+
end
159+
160+
function CircularWallFEM(; name, L=100, N=10, d=0.05, t_layer=[0.002],
161+
λ=[50], cp=[500], ρ=[7850], T0=0.0)
162+
@named inner_heatport = VectorHeatPort(N=N)
163+
@named outer_heatport = VectorHeatPort(N=N)
164+
dx = L / N
165+
Ne = length(t_layer)
166+
Nn = Ne + 1
167+
dn = vcat(d, d .+ 2.0 .* cumsum(t_layer))
168+
Cn = zeros(Nn)
169+
Cn[1:Ne] += Cn_circular_wall_inner.(dn[1:Ne], dn[2:Nn], cp, ρ) .* dx
170+
Cn[2:Nn] += Cn_circular_wall_outer.(dn[1:Ne], dn[2:Nn], cp, ρ) .* dx
171+
p = @parameters C_shift = 0.0
172+
Ke = Ke_circular_wall.(dn[1:Ne], dn[2:Nn], λ) .* dx
173+
@variables begin
174+
(Tn(t))[1:N, 1:Nn] = fill(T0, N, Nn)
175+
(Qe(t))[1:N, 1:Ne] = fill(T0, N, Ne)
176+
end
177+
sts = [vec(Tn); vec(Qe)]
178+
e0 = Equation[inner_heatport.T[i] ~ Tn[i, 1] for i in 1:N]
179+
e1 = Equation[outer_heatport.T[i] ~ Tn[i, Nn] for i in 1:N]
180+
e2 = Equation[Qe[i, j] ~ Ke[j] * (-Tn[i, j+1] + Tn[i, j]) for i in 1:N for j in 1:Ne]
181+
e3 = Equation[D(Tn[i, 1]) * (Cn[1] + C_shift) ~ inner_heatport.Q[i] - Qe[i, 1] for i in 1:N]
182+
e4 = Equation[D(Tn[i, j]) * Cn[j] ~ Qe[i, j-1] - Qe[i, j] for i in 1:N for j in 2:Nn-1]
183+
e5 = Equation[D(Tn[i, Nn]) * Cn[Nn] ~ Qe[i, Ne] + outer_heatport.Q[i] for i in 1:N]
184+
eqs = vcat(e0, e1, e2, e3, e4, e5)
185+
ODESystem(eqs, t, sts, p; systems=[inner_heatport, outer_heatport], name=name)
186+
end
187+
188+
function CylindricalSurfaceConvection(; name, L=100, N=100, d=1.0, α=5.0)
189+
dx = L / N
190+
S = pi * d * dx
191+
@named heatport = VectorHeatPort(N=N)
192+
sts = @variables Tenv(t) = 0.0
193+
eqs = [
194+
Tenv ~ 18.0
195+
[heatport.Q[i] ~ α * S * (heatport.T[i] - Tenv) for i in 1:N]
196+
]
197+
198+
ODESystem(eqs, t, sts, []; systems=[heatport], name=name)
199+
end
200+
201+
function PreinsulatedPipe(; name, L=100.0, N=100.0, dn=0.05, T0=0.0, t_layer=[0.004, 0.013],
202+
λ=[50, 0.04], cp=[500, 1200], ρ=[7800, 40], α=5.0,
203+
e=1e-4, lumped_T=50, diffusion=true)
204+
@named inlet = FluidPort()
205+
@named outlet = FluidPort()
206+
@named fluid_region = FluidRegion(L=L, N=N, dn=dn, e=e, lumped_T=lumped_T, diffusion=diffusion)
207+
@named shell = CircularWallFEM(L=L, N=N, d=dn, t_layer=t_layer, λ=λ, cp=cp, ρ=ρ)
208+
@named surfconv = CylindricalSurfaceConvection(L=L, N=N, d=dn + 2.0 * sum(t_layer), α=α)
209+
systems = [fluid_region, shell, inlet, outlet, surfconv]
210+
eqs = [
211+
connect(fluid_region.inlet, inlet)
212+
connect(fluid_region.outlet, outlet)
213+
connect(fluid_region.heatport, shell.inner_heatport)
214+
connect(shell.outer_heatport, surfconv.heatport)
215+
]
216+
ODESystem(eqs, t, [], []; systems=systems, name=name)
217+
end
218+
219+
function Source(; name, p_feed=100000)
220+
@named outlet = FluidPort()
221+
sts = @variables m_flow(t) = 1e-6
222+
eqs = [
223+
m_flow ~ m_flow_source(t)
224+
outlet.m ~ -m_flow
225+
outlet.p ~ p_feed
226+
outlet.T ~ T_source(t)
227+
]
228+
compose(ODESystem(eqs, t, sts, []; name=name), [outlet])
229+
end
230+
231+
function Sink(; name)
232+
@named inlet = FluidPort()
233+
eqs = [
234+
inlet.T ~ instream(inlet.T)
235+
]
236+
compose(ODESystem(eqs, t, [], []; name=name), [inlet])
237+
end
238+
239+
function TestBenchPreinsulated(; name, L=1.0, dn=0.05, t_layer=[0.0056, 0.013], N=100, diffusion=true, lumped_T=20)
240+
@named pipe = PreinsulatedPipe(L=L, dn=dn, N=N, diffusion=diffusion, t_layer=t_layer, lumped_T=lumped_T)
241+
@named source = Source()
242+
@named sink = Sink()
243+
subs = [source, pipe, sink]
244+
eqs = [
245+
connect(source.outlet, pipe.inlet)
246+
connect(pipe.outlet, sink.inlet)
247+
]
248+
compose(ODESystem(eqs, t, [], []; name=name), subs)
249+
end
250+
```
251+
252+
To circumvent scaling issues with ModelingToolkit.jl, we will simplify and compile the system
253+
using JuliaSimCompiler.jl and trace through the resultant function to obtain the expression
254+
whose jacobian will be benchmarked.
255+
256+
```julia
257+
function build_all_problems(N)
258+
return map(N) do n
259+
@named testbench = TestBenchPreinsulated(L=470, N=n, dn=0.3127, t_layer=[0.0056, 0.058])
260+
sys_jsir = structural_simplify(IRSystem(testbench), loop=false)
261+
return ODEProblem(sys_jsir, [], (0.0, 1.0))
262+
end
263+
end
264+
265+
function trace_and_jacobian!(trace_times, jac_times, prob, i; xname, pname, tname)
266+
t = only(@independent_variables $tname)
267+
n_states = length(prob.u0)
268+
x = only(@variables $xname(t)[1:n_states])
269+
n_params = length(prob.p)
270+
p = only(@parameters $pname[1:n_params])
271+
xvec = collect(Symbolics.unwrap(x))
272+
273+
expr = zeros(Num, n_states)
274+
trace_times[i] = @elapsed prob.f.f(expr, x, p, t)
275+
276+
expr = Vector{Union{SymbolicUtils.BasicSymbolic{Real}, Float64}}(map(Symbolics.unwrap, expr))
277+
jac_times[i] = @elapsed jac = Symbolics.jacobian(expr, xvec)
278+
279+
return jac
280+
end
281+
```
282+
283+
```julia
284+
N = [5, 10, 20, 40, 60, 80, 160];
285+
problems = build_all_problems(N)
286+
287+
hashconsing_jac_times = fill(NaN, length(N))
288+
hashconsing_trace_times = fill(NaN, length(N))
289+
no_hashconsing_jac_times = fill(NaN, length(N))
290+
no_hashconsing_trace_times = fill(NaN, length(N))
291+
```
292+
293+
# Timings
294+
295+
```julia
296+
for (i, prob) in enumerate(problems)
297+
# to prevent cached hashconsing entries from previously run jacobians
298+
# affecting the timing of this jacobian, the names of the symbolic variables
299+
# are gensym-ed
300+
xname = gensym(:x)
301+
pname = gensym(:p)
302+
tname = gensym(:t)
303+
jac1 = trace_and_jacobian!(hashconsing_trace_times, hashconsing_jac_times, prob, i; xname, pname, tname)
304+
SymbolicUtils.ENABLE_HASHCONSING[] = false
305+
jac2 = trace_and_jacobian!(no_hashconsing_trace_times, no_hashconsing_jac_times, prob, i; xname, pname, tname)
306+
SymbolicUtils.ENABLE_HASHCONSING[] = true
307+
@assert isequal(jac1, jac2)
308+
end
309+
```
310+
311+
# Generate plots
312+
313+
```julia
314+
f = Figure(size=(800,1200));
315+
names = ["Without hashconsing", "With hashconsing"]
316+
let ax = Axis(f[1, 1]; yscale = log10, xscale = log10, title = "Tracing time")
317+
_lines = [lines!(N, no_hashconsing_trace_times), lines!(N, hashconsing_trace_times)]
318+
Legend(f[1, 2], _lines, names)
319+
end
320+
let ax = Axis(f[2, 1]; yscale = log10, xscale = log10, title = "Jacobian time")
321+
_lines = [lines!(N, no_hashconsing_jac_times), lines!(N, hashconsing_jac_times)]
322+
Legend(f[2, 2], _lines, names)
323+
end
324+
f
325+
```
326+
327+
## Appendix
328+
329+
```julia, echo = false
330+
using SciMLBenchmarks
331+
SciMLBenchmarks.bench_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])
332+
```

0 commit comments

Comments
 (0)