CasADi dynamic opt: TypeError: non-boolean (CasADi.MX) used in boolean context in SymbolicUtils hashconsing during substitute (Parameter estimation test, master)
The Parameter estimation testset in lib/ModelingToolkitBase/test/optimization/dynamic_optimization.jl:607 errors on master when constructing a CasADiDynamicOptProblem with tune_parameters = true:
TypeError: non-boolean (CasADi.MX) used in boolean context
This fails on ModelingToolkit.jl's own master CI (tests / Optimization (julia 1), commit 77047c8, 2026-07-03):
https://github.com/SciML/ModelingToolkit.jl/actions/runs/28641054022/job/84937147611
and it also breaks NonlinearSolve.jl's downstream CI on master (ModelingToolkit.jl/Initialization downstream job, which runs the MTK Optimization group):
https://github.com/SciML/NonlinearSolve.jl/actions/jobs/84972377349
Versions in the failing runs: ModelingToolkitBase v1.50.0 (master checkout), SymbolicUtils v4.38.1, CasADi v1.2.0, Julia 1.12.6.
Stacktrace (excerpt, from the NonlinearSolve downstream log)
Parameter estimation: Error During Test at .../lib/ModelingToolkitBase/test/optimization/dynamic_optimization.jl:607
Got exception outside of a @test
TypeError: non-boolean (CasADi.MX) used in boolean context
Stacktrace:
[1] signequal(x::CasADi.MX, y::Float64)
@ Base ./operators.jl:180
[2] isequal(x::CasADi.MX, y::Float64)
@ Base ./operators.jl:184
[3] isequal_somescalar(a::Any, b::Any)
@ SymbolicUtils .../SymbolicUtils/u8OUn/src/hashconsing.jl:37
[4] isequal_bsimpl(a::BasicSymbolicImpl{SymReal}, b::BasicSymbolicImpl{SymReal}, full::Bool)
@ SymbolicUtils .../src/hashconsing.jl:190
[5] isequal(a::BasicSymbolicImpl{SymReal}, b::BasicSymbolicImpl{SymReal})
@ SymbolicUtils .../src/hashconsing.jl:218
[6] ht_keyindex2_shorthash!(h::WeakCacheSets.WeakCacheSet{...}, key::...)
@ WeakCacheSets .../src/WeakCacheSets.jl:146
[7] getkey!(h::WeakCacheSets.WeakCacheSet{...}, key::...)
@ WeakCacheSets .../src/WeakCacheSets.jl:201
[11] hashcons(s::BasicSymbolicImpl{SymReal}, reregister::Bool)
@ SymbolicUtils .../src/hashconsing.jl:596
[13] SymbolicUtils.BasicSymbolicImpl.Const{SymReal}(val::Any; unsafe::Bool)
@ SymbolicUtils .../src/inner_ctors.jl:337
[16] combine_fold(::Type{SymReal}, op::Any, args::..., meta::..., can_fold::Bool)
@ SymbolicUtils .../src/substitute.jl:213
[17] (::SymbolicUtils.DefaultSubstituter{true, Dict{Any, Any}, Returns{Bool}, Nothing})(ex::...)
@ SymbolicUtils .../src/substitute.jl:196
[19] #substitute#452 @ .../src/substitute.jl:273
[21] add_cost_function!(model::MTKCasADiDynamicOptExt.CasADiModel{Vector{CasADi.MX}}, sys::System, tspan::Tuple{Float64, Float64}, pmap::Dict{Any, Any})
@ ModelingToolkitBase .../src/systems/optimal_control_interface.jl:471
[22] process_DynamicOptProblem(prob_type::Type{MTKCasADiDynamicOptExt.CasADiDynamicOptProblem}, ...; dt, steps, tune_parameters::Bool, ...)
@ ModelingToolkitBase .../src/systems/optimal_control_interface.jl:362
[24] #CasADiDynamicOptProblem#6
@ .../ext/MTKCasADiDynamicOptExt.jl:107
Mechanism
- With
tune_parameters = true, process_DynamicOptProblem does merge!(pmap, Dict(tunable_params .=> P_syms)) where, for the CasADi backend, P_syms are raw CasADi.MX accessors (MTK.get_param_for_pmap(::Opti, P, i) = P[i]).
add_cost_function! then calls substitute(jcosts, rules; fold = Val(true), ...) with those MX objects as substitution values.
- During folding, SymbolicUtils'
combine_fold computes a constant result that is a CasADi.MX and wraps it in Const{SymReal}(::CasADi.MX).
- The
Const constructor goes through hashconsing: hashcons → WeakCacheSets.getkey! compares the new Const against a cached entry in the same hash bucket via isequal_bsimpl → isequal_somescalar(::CasADi.MX, ::Float64).
isequal_somescalar's fallback is isequal(a, b)::Bool. CasADi.MX (MX <: CasadiSymbolicObject) has symbolic comparison operators (== returns an MX, not a Bool), so Base's generic isequal path ends up using an MX in a boolean context and throws TypeError: non-boolean (CasADi.MX) used in boolean context.
So this is a contract clash: SymbolicUtils hashconsing assumes isequal on Const payloads returns Bool, while CasADi.MX (like other symbolic scalar types) has non-Bool isequal/comparisons. It only triggers when the new Const(::MX) lands in the same WeakCacheSet bucket as a cached numeric Const, which is why it can look version-/run-dependent.
Minimal reproduction (verified locally, Julia 1.12.4, CasADi v1.2.0, SymbolicUtils v4.38.1)
using CasADi, SymbolicUtils
isequal(MX(2.0), 1.0) # TypeError: typeassert Bool got MX
SymbolicUtils.isequal_somescalar(MX(2.0), 1.0) # TypeError — the exact hashconsing comparison that fails in CI
Note: the same MTK master run shows sublibrary-ci / lib/ModelingToolkitBase [Optimization] / Julia 1 passing while root tests / Optimization (julia 1) fails; the difference appears to be which environments actually exercise the CasADi path.
Possible fixes (non-exclusive)
- MTKCasADiDynamicOptExt / optimal_control_interface: avoid substituting raw backend objects (
MX) as scalar values into SymbolicUtils expressions with fold = Val(true) — e.g. wrap them so folding cannot produce a bare Const(::MX), or disable folding for the backend-variable substitution pass (the JuMP/InfiniteOpt backends presumably get away with it because their variable types have Bool isequal).
- SymbolicUtils: make
isequal_somescalar's fallback robust to payload types with non-Bool isequal (e.g. typeof(a) === typeof(b) || return false before the generic isequal, and/or catching the non-Bool case). This would harden hashconsing against any symbolic-scalar payload, though Const(::MX) vs Const(::MX) comparison would still need isequal to return Bool.
- CasADi.jl:
Base.isequal(::MX, ::Number) returning a non-Bool violates the documented isequal contract; a structural (Bool-returning) isequal in CasADi.jl would fix the whole class of problems (out of SciML's hands).
Marking here since the consumer code and the failing test are in ModelingToolkitBase; happy to move/split to SymbolicUtils.jl if that's the preferred fix location.
CasADi dynamic opt:
TypeError: non-boolean (CasADi.MX) used in boolean contextin SymbolicUtils hashconsing duringsubstitute(Parameter estimation test, master)The
Parameter estimationtestset inlib/ModelingToolkitBase/test/optimization/dynamic_optimization.jl:607errors on master when constructing aCasADiDynamicOptProblemwithtune_parameters = true:This fails on ModelingToolkit.jl's own master CI (
tests / Optimization (julia 1), commit 77047c8, 2026-07-03):https://github.com/SciML/ModelingToolkit.jl/actions/runs/28641054022/job/84937147611
and it also breaks NonlinearSolve.jl's downstream CI on master (ModelingToolkit.jl/Initialization downstream job, which runs the MTK Optimization group):
https://github.com/SciML/NonlinearSolve.jl/actions/jobs/84972377349
Versions in the failing runs: ModelingToolkitBase v1.50.0 (master checkout), SymbolicUtils v4.38.1, CasADi v1.2.0, Julia 1.12.6.
Stacktrace (excerpt, from the NonlinearSolve downstream log)
Mechanism
tune_parameters = true,process_DynamicOptProblemdoesmerge!(pmap, Dict(tunable_params .=> P_syms))where, for the CasADi backend,P_symsare rawCasADi.MXaccessors (MTK.get_param_for_pmap(::Opti, P, i) = P[i]).add_cost_function!then callssubstitute(jcosts, rules; fold = Val(true), ...)with thoseMXobjects as substitution values.combine_foldcomputes a constant result that is aCasADi.MXand wraps it inConst{SymReal}(::CasADi.MX).Constconstructor goes through hashconsing:hashcons→WeakCacheSets.getkey!compares the newConstagainst a cached entry in the same hash bucket viaisequal_bsimpl→isequal_somescalar(::CasADi.MX, ::Float64).isequal_somescalar's fallback isisequal(a, b)::Bool.CasADi.MX(MX <: CasadiSymbolicObject) has symbolic comparison operators (==returns anMX, not aBool), so Base's genericisequalpath ends up using anMXin a boolean context and throwsTypeError: non-boolean (CasADi.MX) used in boolean context.So this is a contract clash: SymbolicUtils hashconsing assumes
isequalonConstpayloads returnsBool, whileCasADi.MX(like other symbolic scalar types) has non-Boolisequal/comparisons. It only triggers when the newConst(::MX)lands in the sameWeakCacheSetbucket as a cached numericConst, which is why it can look version-/run-dependent.Minimal reproduction (verified locally, Julia 1.12.4, CasADi v1.2.0, SymbolicUtils v4.38.1)
Note: the same MTK master run shows
sublibrary-ci / lib/ModelingToolkitBase [Optimization] / Julia 1passing while roottests / Optimization (julia 1)fails; the difference appears to be which environments actually exercise the CasADi path.Possible fixes (non-exclusive)
MX) as scalar values into SymbolicUtils expressions withfold = Val(true)— e.g. wrap them so folding cannot produce a bareConst(::MX), or disable folding for the backend-variable substitution pass (the JuMP/InfiniteOpt backends presumably get away with it because their variable types have Boolisequal).isequal_somescalar's fallback robust to payload types with non-Boolisequal(e.g.typeof(a) === typeof(b) || return falsebefore the genericisequal, and/or catching the non-Bool case). This would harden hashconsing against any symbolic-scalar payload, thoughConst(::MX)vsConst(::MX)comparison would still needisequalto returnBool.Base.isequal(::MX, ::Number)returning a non-Boolviolates the documentedisequalcontract; a structural (Bool-returning)isequalin CasADi.jl would fix the whole class of problems (out of SciML's hands).Marking here since the consumer code and the failing test are in ModelingToolkitBase; happy to move/split to SymbolicUtils.jl if that's the preferred fix location.