Skip to content

CasADi dynamic opt: TypeError: non-boolean (CasADi.MX) used in boolean context in SymbolicUtils hashconsing during substitute (Parameter estimation test, master) #4706

Description

@ChrisRackauckas-Claude

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

  1. 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]).
  2. add_cost_function! then calls substitute(jcosts, rules; fold = Val(true), ...) with those MX objects as substitution values.
  3. During folding, SymbolicUtils' combine_fold computes a constant result that is a CasADi.MX and wraps it in Const{SymReal}(::CasADi.MX).
  4. The Const constructor goes through hashconsing: hashconsWeakCacheSets.getkey! compares the new Const against a cached entry in the same hash bucket via isequal_bsimplisequal_somescalar(::CasADi.MX, ::Float64).
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions