From 83b362fb24f88fe98fbd9de732a4a4d00ddeb0e5 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 19 Jun 2026 11:16:50 -0400 Subject: [PATCH 1/3] Fix master CI: JET undefined-var, CSTR init over-determination, downgrade compat floors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent master failures on SciML/ProcessSimulator.jl: QA / JET (`local variable ΔHᵣ may be undefined`): `TPControlVolume` created the reaction variables `ΔnR`/`ΔHᵣ` inside a `reactive ? append!(vars, @variables ...) : nothing` ternary, so when `reactive == false` the symbolic variables were never bound, yet the equation list still references `ΔHᵣ`/`ΔnR` (guarded by `reactive ? ... : 0.0`). JET's `report_package` flagged the possibly-undefined local. Move the `@variables` block out of the ternary so the variables are always created and only appended to the system's variable list when `reactive`. `JET.report_package` now reports 0 errors and `run_qa` passes 12/12 on Julia 1.12. Core tests (`simple_cstr.jl`, all Julia versions): `Tmax = 297.0` instead of 356.149. The test pinned `cstr.cv.c2.n` and `outlet.m` in `u0`; these are fixed by the algebraic flow balances, so pinning them over-determines MTK's initialization (4 residuals, 0 free unknowns) and the least-squares fallback returns a degenerate steady state where no reaction occurs. Supply them as initialization `guesses` instead. The peak-temperature *time* assertion is relaxed from atol 1e-2 to 0.1 because the maximum sits on a flat plateau (< 0.002 K across the bracketing steps) so the discrete argmax time shifts by one solver step (~0.02 s) across Julia versions while `Tmax` stays identical to 6 figures; the physically meaningful `Tmax` assertion stays tight at 1e-3. Verified `Pass 2/2` on Julia 1.10 and 1.12. Downgrade (Julia 1.10): `UndefVarError: AbstractNonlinearTerminationMode not defined`. The compat floors `ModelingToolkit = "9"` / `DifferentialEquations = "7"` let downgrade-compat resolve an inconsistent SciML stack (NonlinearSolve 3.14 + DiffEqBase 6.174 + SteadyStateDiffEq 2.0, and BoundaryValueDiffEqFIRK 1.6 against RecursiveArrayTools 3.50). Raise the floors to `ModelingToolkit = "9.60"` (pins NonlinearSolve 4.x) and `DifferentialEquations = "7.16"` (pins a modern BVP/SteadyState stack). Raising lower bounds only narrows resolution. Verified the downgraded resolution precompiles and `simple_cstr.jl` passes 2/2 on Julia 1.10. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 4 ++-- src/base/base_components.jl | 12 +++++------- test/simple_cstr.jl | 17 +++++++++++++++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index 068f222..9122d97 100644 --- a/Project.toml +++ b/Project.toml @@ -7,8 +7,8 @@ version = "1.0.0" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" [compat] -DifferentialEquations = "7" -ModelingToolkit = "9" +DifferentialEquations = "7.16" +ModelingToolkit = "9.60" SafeTestsets = "0.1" SciMLTesting = "1" Test = "1" diff --git a/src/base/base_components.jl b/src/base/base_components.jl index a168b8b..7aabd9f 100644 --- a/src/base/base_components.jl +++ b/src/base/base_components.jl @@ -123,13 +123,11 @@ end ΔE(t), [description = "added/removed heat or work"] #, unit=u"J/s"] V(t), [description = "volume", bounds = (0, Inf)] #, unit=u"m^3"] end - reactive ? - append!( - vars, @variables begin - ΔnR(t)[1:ms.N_c], [description = "molar holdup change by reaction"] #, unit=u"mol"]) - ΔHᵣ(t), [description = "enthalpy of reaction"] #, unit=u"J/s"] - end - ) : nothing + reaction_vars = @variables begin + ΔnR(t)[1:ms.N_c], [description = "molar holdup change by reaction"] #, unit=u"mol"]) + ΔHᵣ(t), [description = "enthalpy of reaction"] #, unit=u"J/s"] + end + reactive ? append!(vars, reaction_vars) : nothing eqs = [ ΔH ~ sum([c.h * c.n for c in mcons]), diff --git a/test/simple_cstr.jl b/test/simple_cstr.jl index c06ce5a..d0c2bf9 100644 --- a/test/simple_cstr.jl +++ b/test/simple_cstr.jl @@ -77,19 +77,32 @@ u0 = [ cstr.cv.nᵢ[1, 4] => 0.0, inlet.m => m0_inlet, cstr.cv.T => 297.0, +] + +# `cstr.cv.c2` (outlet) and `outlet.m` are fixed by the algebraic flow balances, +# not independent initial states. Pinning them in `u0` over-determines MTK's +# initialization system (4 algebraic residuals, 0 free unknowns -> InitialFailure). +# They must be supplied as initialization guesses instead. +guesses = [ cstr.cv.c2.n => 0.0, outlet.m => 0.0, ] flowsheet, idx = structural_simplify(flowsheet_, (first.(inp), [])) -prob = ODEProblem(flowsheet, u0, (0, 2) .* 3600.0, vcat(inp)) #; guesses = [outlet.m => -m0_inlet]) +prob = ODEProblem(flowsheet, u0, (0, 2) .* 3600.0, vcat(inp); guesses = guesses) sol = solve(prob, QNDF(), abstol = 1.0e-6, reltol = 1.0e-6) (Tmax, iTmax) = findmax(sol[cstr.cv.T]) @test Tmax ≈ 356.149 atol = 1.0e-3 -@test sol.t[iTmax] ≈ 2823.24 atol = 1.0e-2 +# The peak temperature sits on a flat plateau: across the ~4 s solver steps +# bracketing the maximum, T varies by < 0.002 K. The `findmax` argmax therefore +# lands on whichever sampled step is highest, and that step shifts by one solver +# step (~0.02 s) with the Julia/solver version (1.10: 2823.2427 s, 1.12: +# 2823.2227 s) even though Tmax itself is identical to 6 figures. atol = 0.1 +# (relative 3.5e-5) still pins the peak time tightly while tolerating that shift. +@test sol.t[iTmax] ≈ 2823.24 atol = 0.1 if isinteractive() # Plots From 93d1d69261e04287cb960646eb03cb42587f60f4 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 20 Jun 2026 08:53:32 -0400 Subject: [PATCH 2/3] Fix Core simple_steady_state: solve algebraic steady state as NonlinearProblem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compressor and the full Brayton-cycle flowsheet in `test/simple_steady_state.jl` are purely algebraic after `structural_simplify` (no differential states). Constructing a `SteadyStateProblem` from them makes current ModelingToolkit (9.84) assemble an over-determined *initialization* system (e.g. '6 equations for 2 unknowns'), whose least-squares fallback returns `InitialFailure`. `solve` then hands back the untouched initial guess (`u0 = 1.0`), so the assertions saw degenerate values: `comp.W = -1.56e6` (vs 2.3933999e6), `s2.T = s4.T = 1.0` (vs 755.58 / 99.89), `ε_KM = 0.02` (vs 0.504). The physically correct construction for an algebraic steady state is a `NonlinearProblem`, which solves the steady-state residual equations directly. With that change all four original assertions pass unchanged (same expected values, same tolerances). Verified locally on Julia 1.12.6 with the exact CI-resolved stack (ModelingToolkit 9.84.0, DifferentialEquations 7.17.0): the file passes 4/4 via SafeTestsets (comp.W = 2.393399947834445e6, s2.T = 755.58, s4.T = 99.89, ε_KM = 0.504; all solves return retcode Success). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- test/simple_steady_state.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/simple_steady_state.jl b/test/simple_steady_state.jl index 85b1d26..3563a2f 100644 --- a/test/simple_steady_state.jl +++ b/test/simple_steady_state.jl @@ -45,7 +45,12 @@ u0_comp = [ comp.ηᴱ => 0.8, ] -prob_comp = SteadyStateProblem(compressor, inp_comp, u0_comp) +# The compressor is purely algebraic (no differential states after simplification), +# so its steady state is the root of the algebraic equations. Building a +# `SteadyStateProblem` here makes MTK assemble an over-determined initialization +# system that fails (`InitialFailure`) and returns the untouched guess; solve the +# algebraic steady state directly as a `NonlinearProblem` instead. +prob_comp = NonlinearProblem(compressor, u0_comp, inp_comp) sol_comp = solve(prob_comp) @test sol_comp[comp.W] ≈ 2.3933999e6 rtol = 1.0e-5 @@ -104,7 +109,9 @@ flowsheet, idx = structural_simplify(flowsheet_, (first.(inp), out)) u0 = [u => 1.0 for u in unknowns(flowsheet)] -prob = SteadyStateProblem(flowsheet, inp, u0) +# As with the single compressor above, the flowsheet is fully algebraic, so its +# steady state is solved directly as a `NonlinearProblem`. +prob = NonlinearProblem(flowsheet, u0, inp) sol = solve(prob) ε_KM = abs(sol[heat_44⁺.Q]) / abs(sol[turb_34.W] + sol[comp_12.W]) From 45bad7b7b5cf7816fbd477fd08216e369b9cccb2 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 20 Jun 2026 09:37:30 -0400 Subject: [PATCH 3/3] Raise ModelingToolkit downgrade floor to 9.65 for NonlinearProblem(::ODESystem) dispatch The previous commit switched `test/simple_steady_state.jl` to build the algebraic steady states as `NonlinearProblem`s. That construction relies on MTK's `NonlinearProblem(::ODESystem, u0, p)` symbolic-problem dispatch, which does not exist on the old downgrade floor `ModelingToolkit = 9.60`: there `NonlinearProblem(sys, ...)` falls through to the bare SciMLBase constructor and errors with 'No methods were found for the model function passed to the equation solver' (it tries to build a `NonlinearFunction` directly from the ODESystem). Bisected the dispatch availability on Julia 1.10: MTK 9.60 errors, MTK 9.65/9.70 work. Raise the floor to 9.65 (raising a lower bound only narrows resolution). Verified locally on Julia 1.10 with the downgrade-pinned stack (MTK 9.65.0, DifferentialEquations 7.17.0): the env resolves and precompiles cleanly and `simple_steady_state.jl` passes 4/4 via SafeTestsets. The new stack (MTK 9.84.0, Julia 1.12) continues to pass 4/4. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9122d97..412ce2b 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,7 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" [compat] DifferentialEquations = "7.16" -ModelingToolkit = "9.60" +ModelingToolkit = "9.65" SafeTestsets = "0.1" SciMLTesting = "1" Test = "1"