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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions