diff --git a/lib/ModelingToolkitBase/src/systems/problem_utils.jl b/lib/ModelingToolkitBase/src/systems/problem_utils.jl index 72c2ee86d2..291e8e2e44 100644 --- a/lib/ModelingToolkitBase/src/systems/problem_utils.jl +++ b/lib/ModelingToolkitBase/src/systems/problem_utils.jl @@ -1901,6 +1901,32 @@ function build_operating_point(sys::AbstractSystem, op; fast_path = false) return op end +struct MissingNecessaryInitialConditionsError <: Exception + missing_ics::NecessaryInitialConditionsT +end + +const MISSING_NECESSARY_ICS_ERR_PRELUDE = """ +Missing necessary initial conditions for some variables. The missing variables are listed \ +below, along with the reason why the initial condition is required: +""" + +function Base.showerror(io::IO, err::MissingNecessaryInitialConditionsError) + println(io, MISSING_NECESSARY_ICS_ERR_PRELUDE) + for (k, v) in err.missing_ics + printstyled(io, k; bold = true) + println(io, ": ", v) + end + return +end + +function check_necessary_initial_conditions(sys::AbstractSystem, op::SymmapT) + ics = get_necessary_initial_conditions(sys) + isempty(ics) && return + missing_ics = filter(!Base.Fix1(has_possibly_indexed_key, op) ∘ first, ics) + isempty(missing_ics) || throw(MissingNecessaryInitialConditionsError(missing_ics)) + return +end + """ $(TYPEDSIGNATURES) @@ -1972,6 +1998,7 @@ function __process_SciMLProblem( check_inputmap_keys(sys, op) op = getmetadata(sys, ProblemConstructionHook, identity)(op)::SymmapT + check_necessary_initial_conditions(sys, op) kwargs = NamedTuple(kwargs) diff --git a/lib/ModelingToolkitBase/src/systems/system.jl b/lib/ModelingToolkitBase/src/systems/system.jl index 81ec01e3aa..a4acd868ca 100644 --- a/lib/ModelingToolkitBase/src/systems/system.jl +++ b/lib/ModelingToolkitBase/src/systems/system.jl @@ -1798,3 +1798,42 @@ to `sys` for which [`reverse_transformation_during_initialization`](@ref) is `tr function reverse_transformations_for_initialization(sys::System) return __reverse_transformations_helper(reverse_transformation_during_initialization, sys) end + +# Necessary Initial Conditions API + +# Metadata key +abstract type NecessaryInitialConditions end +const NecessaryInitialConditionsT = OrderedDict{SymbolicT, Union{String, LazyString}} + +""" + $TYPEDSIGNATURES + +Get a map from variables in `sys` whose initial conditions are required to a `String` +reason for why they are required. This can be mutated to change the necessary +initial conditions. After mutation, [`set_necessary_initial_conditions`](@ref) MUST +be called with the mutated map. +""" +function get_necessary_initial_conditions(sys::System) + if !isempty(get_systems(sys)) + throw(ArgumentError("This is only valid for flattened systems!")) + end + return @something( + getmetadata( + sys, NecessaryInitialConditions, nothing + )::Union{NecessaryInitialConditionsT, Nothing}, + NecessaryInitialConditionsT() + ) +end + +""" + $TYPEDSIGNATURES + +See [`get_necessary_initial_conditions`](@ref). +""" +function set_necessary_initial_conditions(sys::System, ics) + if !isempty(get_systems(sys)) + throw(ArgumentError("This is only valid for flattened systems!")) + end + get_necessary_initial_conditions(sys) === ics && return sys + return setmetadata(sys, NecessaryInitialConditions, ics)::System +end diff --git a/lib/ModelingToolkitBase/test/system_building.jl b/lib/ModelingToolkitBase/test/system_building.jl index 7910344a72..0113084523 100644 --- a/lib/ModelingToolkitBase/test/system_building.jl +++ b/lib/ModelingToolkitBase/test/system_building.jl @@ -53,3 +53,12 @@ end @named sys = System([D(x) ~ x'], t) @test_nowarn mtkcompile(sys) end + +@testset "Necessary initial conditions" begin + @variables x(t) y(t) + @mtkcomplete sys = System([D(x) ~ t, x ~ y], t) + ics = ModelingToolkitBase.get_necessary_initial_conditions(sys) + ics[y] = "Because I want to" + sys = ModelingToolkitBase.set_necessary_initial_conditions(sys, ics) + @test_throws ModelingToolkitBase.MissingNecessaryInitialConditionsError ODEProblem(sys, [x => 1], (0.0, 1.0)) +end