|
| 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