Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/src/basics/Debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,24 @@ solve(dprob, Tsit5());
```@docs; canonical = false
debug_system
```

## Error message guidance

Errors raised while building a problem describe what went wrong and then, where it is
useful, how to fix it in terms of the ModelingToolkit API — which keyword argument to pass,
or where in the system to supply a value. Front ends which present a modelling language of
their own can leave that part out, since advice to call `ODEProblem` or to pass
`initialization_eqs` would send their users looking for something their language does not
have. The description of what went wrong is unaffected.

```julia
ModelingToolkit.show_api_guidance!(false)
```

!!! warning "Experimental"

`show_api_guidance!` is experimental and unsupported. It may change or be removed in
any release, without a breaking version bump. Verbosity across the SciML ecosystem is
moving to [SciMLLogging.jl](https://github.com/SciML/SciMLLogging.jl), and this setting
is expected to be replaced by an option there once ModelingToolkit adopts it. That will
be the supported way to control this.
201 changes: 200 additions & 1 deletion lib/ModelingToolkitBase/src/problems/initializationproblem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ All other keyword arguments are forwarded to the wrapped problem constructor.
pareqs, resteqs = find_all_parameter_equations(isys)
@set! isys.eqs = resteqs
if simplify_system
isys = mtkcompile(isys; fully_determined, split = is_split(sys), initsys_mtkcompile_kwargs...)
isys = mtkcompile_initialization_system(
isys, sys; fully_determined, initsys_mtkcompile_kwargs...
)
end
for i in eachindex(pareqs)
eq = pareqs[i]
Expand Down Expand Up @@ -162,6 +164,203 @@ All other keyword arguments are forwarded to the wrapped problem constructor.
)
end

"""
$TYPEDSIGNATURES

`mtkcompile` the initialization system `isys` of `sys`, rewriting structural errors so
they say that it is the initialization system which is unbalanced.

Structural analysis has no way of knowing that the system handed to it is an
initialization system, so on its own it reports the imbalance in terms which read as if
the model itself were at fault. Since this is almost always hit through a problem
constructor rather than a direct `mtkcompile` call, the message is rewritten to name the
initialization system, count the missing equations and list the ways of supplying them.
"""
function mtkcompile_initialization_system(
isys::AbstractSystem, sys::AbstractSystem; fully_determined, kwargs...
)
try
return mtkcompile(isys; fully_determined, split = is_split(sys), kwargs...)
catch err
newerr = with_initialization_context(err, isys, sys; kwargs...)
newerr === err && rethrow()
rethrow(newerr)
end
end

"""
Structural analysis exceptions which report an unbalanced or singular system. These are
defined both here and in StateSelection.jl, which ModelingToolkitBase does not depend on,
so they are identified by name to cover whichever of the two was thrown.
"""
const UNBALANCED_SYSTEM_EXCEPTIONS = (
:ExtraVariablesSystemException, :ExtraEquationsSystemException, :InvalidSystemException,
)

"""
$TYPEDSIGNATURES

Given an error `err` thrown while compiling the initialization system `isys` of `sys`,
return an equivalent error whose message identifies it as an initialization failure.
Returns `err` unchanged if it is not an error about the balance of a system.
"""
function with_initialization_context(
err, isys::AbstractSystem, sys::AbstractSystem; kwargs...
)
T = typeof(err)
kind = nameof(T)
kind in UNBALANCED_SYSTEM_EXCEPTIONS || return err
fieldcount(T) == 1 && fieldtype(T, 1) <: AbstractString || return err
msg = getfield(err, 1)
# `InvalidSystemException` also reports problems which have nothing to do with the
# balance of the system, and for which the guidance below would be wrong.
kind === :InvalidSystemException && !occursin("singular", msg) && return err
return T(
initialization_structure_error_message(
kind, msg, initialization_system_size(isys, sys; kwargs...)
)
)
end

"""
$TYPEDSIGNATURES

Return `(neqs, nunknowns)` of the compiled initialization system `isys` of `sys`, or
`nothing` if it cannot be compiled without the balance check. Used to report how many
equations an unbalanced initialization system is missing, in the same terms as
`underdetermined_initialization_message`.
"""
function initialization_system_size(isys::AbstractSystem, sys::AbstractSystem; kwargs...)
return try
compiled = mtkcompile(
isys; fully_determined = false, split = is_split(sys), kwargs...
)
(length(equations(compiled)), length(unknowns(compiled)))
catch
nothing
end
end

const INITIALIZATION_DOCS_URL = "https://docs.sciml.ai/ModelingToolkit/stable/tutorials/initialization/"

const INITIALIZATION_CONTEXT_MESSAGE = """
This is an error in the initialization system, not in the equations being integrated. \
The initialization system solves for the value of every unknown of the model, and of \
their derivatives, at the initial time. It needs as many equations as there are such \
values to solve for. Initial values given for the model become equations of this system.
"""

const INITIALIZATION_UNDERDETERMINED_MESSAGE = """
Each missing equation is supplied by giving one more initial value, or one more equation \
relating initial values. Guesses are starting values for the initialization solve rather \
than constraints on it, so adding a guess does not supply one.
"""

const INITIALIZATION_UNDERDETERMINED_API_MESSAGE = """
In ModelingToolkit, any of the following supplies one missing equation:

* Give an initial value in the problem constructor, as in
`ODEProblem(sys, [x => 1.0], tspan)`.
* Give one in the model, via `initial_conditions` or `bindings`.
* Add an equation relating initial values, via the `initialization_eqs` keyword argument \
of the system or of the problem constructor.

To solve an underdetermined initialization in a least squares sense instead, pass \
`fully_determined = false` to the problem constructor; the guesses then decide which of \
the possible initial states is found.
"""

const INITIALIZATION_OVERDETERMINED_MESSAGE = """
Remove initial values or equations relating them until as many remain as there are \
unknowns to solve for.
"""

const INITIALIZATION_OVERDETERMINED_API_MESSAGE = """
In ModelingToolkit, these come from `initial_conditions`, `bindings` and \
`initialization_eqs`, and from the operating point passed to the problem constructor. To \
solve an overdetermined initialization in a least squares sense instead, pass \
`fully_determined = false` to the problem constructor. That only finds an initial state \
if the extra equations are consistent with the rest.
"""

const INITIALIZATION_SINGULAR_MESSAGE = """
There are as many equations as unknowns, but they do not determine a unique initial state: \
at least one equation is redundant given the others, leaving some unknown undetermined. \
This usually means two of the given initial conditions are related by an equation of the \
model, so that one of them carries no new information. Giving an initial condition for a \
different variable, or for a derivative, in place of the redundant one resolves it.
"""

const INITIALIZATION_NOTATION_MESSAGE = """
Note that variables named with a `ˍt` suffix are derivatives at the initial time: `xˍt` \
is `D(x)`.
"""

"""
$TYPEDSIGNATURES

Build the message of a structural error from the initialization system. `kind` is the name
of the exception type thrown by structural analysis, `msg` its original message, and `size`
the `(neqs, nunknowns)` of the initialization system, if known.
"""
function initialization_structure_error_message(
kind::Symbol, msg::AbstractString, size::Union{Nothing, Tuple{Int, Int}}
)
io = IOBuffer()
if kind === :ExtraVariablesSystemException
headline = "Initialization system is underdetermined."
remedy = INITIALIZATION_UNDERDETERMINED_MESSAGE
api_remedy = INITIALIZATION_UNDERDETERMINED_API_MESSAGE
elseif kind === :ExtraEquationsSystemException
headline = "Initialization system is overdetermined."
remedy = INITIALIZATION_OVERDETERMINED_MESSAGE
api_remedy = INITIALIZATION_OVERDETERMINED_API_MESSAGE
else
headline = "Initialization system is structurally singular."
remedy = INITIALIZATION_SINGULAR_MESSAGE
api_remedy = nothing
end
println(io, headline, '\n')
println(io, INITIALIZATION_CONTEXT_MESSAGE)
println(io, msg, '\n')
deficit = initialization_deficit_message(kind, size)
deficit === nothing || println(io, deficit)
println(io, remedy)
if show_api_guidance()
api_remedy === nothing || println(io, api_remedy)
println(io, "See $INITIALIZATION_DOCS_URL for more information.")
end
print(io, INITIALIZATION_NOTATION_MESSAGE)
return String(take!(io))
end

"""
$TYPEDSIGNATURES

Return a sentence stating by how many equations an initialization system of size
`size = (neqs, nunknowns)` is out of balance, or `nothing` if `size` is unknown or does
not corroborate the imbalance that structural analysis reported.

Only the difference is reported, not the counts themselves: `size` is measured on the
system compiled without the balance check, which simplifies further than the compilation
that failed, so its counts do not line up with the ones structural analysis reports.
"""
function initialization_deficit_message(kind::Symbol, size::Union{Nothing, Tuple{Int, Int}})
size === nothing && return nothing
neqs, nunknown = size
if kind === :ExtraVariablesSystemException && nunknown > neqs
n = nunknown - neqs
verb = n == 1 ? "is" : "are"
return "$n more $(pluralize(n, "equation")) $verb needed to determine the initial state.\n"
elseif kind === :ExtraEquationsSystemException && neqs > nunknown
n = neqs - nunknown
return "There $(n == 1 ? "is" : "are") $n $(pluralize(n, "equation")) too many.\n"
end
return nothing
end

pluralize(n::Integer, word::AbstractString) = n == 1 ? word : word * "s"

function overdetermined_initialization_message(neqs::Integer, nunknown::Integer, extra::AbstractString)
return """
Initialization system is overdetermined. $neqs equations for $nunknown unknowns. \
Expand Down
43 changes: 43 additions & 0 deletions lib/ModelingToolkitBase/src/utils.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
get_iv(D::Differential) = D.x

const SHOW_API_GUIDANCE = Ref(true)

const EXPERIMENTAL_API_GUIDANCE_NOTE = """
!!! warning "Experimental"

This is experimental and unsupported. It may change or be removed in any release,
without a breaking version bump. Verbosity across the SciML ecosystem is moving to
SciMLLogging.jl, and this setting is expected to be replaced by an option there once
ModelingToolkit adopts it.
"""

"""
$(TYPEDSIGNATURES)

Whether error messages include guidance phrased in terms of the ModelingToolkit API. See
[`show_api_guidance!`](@ref).

$EXPERIMENTAL_API_GUIDANCE_NOTE
"""
show_api_guidance() = SHOW_API_GUIDANCE[]

"""
$(TYPEDSIGNATURES)

Set whether error messages include guidance phrased in terms of the ModelingToolkit API,
such as which keyword argument of a problem constructor to pass, and return the previous
setting.

This is meant for front ends which present a modelling language of their own, in which
advice to call `ODEProblem` or to pass `initialization_eqs` would send users looking for
something their language does not have. Such a front end can call
`show_api_guidance!(false)` once when it loads, and add guidance of its own. What went
wrong is still described in full; only the part of the message which names ModelingToolkit
functions and keyword arguments is left out.

$EXPERIMENTAL_API_GUIDANCE_NOTE
"""
function show_api_guidance!(show::Bool)
old = SHOW_API_GUIDANCE[]
SHOW_API_GUIDANCE[] = show
return old
end

"""
$(TYPEDSIGNATURES)

Expand Down
79 changes: 79 additions & 0 deletions lib/ModelingToolkitBase/test/initializationsystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,72 @@ sol = solve(prob, Rodas5P())
fully_determined = true
)

@testset "Unbalanced initialization error names the initialization system" begin
err = try
ODEProblem(
pend, [x => 1, g => 1], (0.0, 1.5),
guesses = [x => 1, y => 0.2, λ => 0.0],
fully_determined = true
)
nothing
catch e
e
end
@test err isa ERRMOD.ExtraVariablesSystemException
msg = sprint(showerror, err)
@test occursin("Initialization system is underdetermined", msg)
@test occursin("initialization_eqs", msg)
# The message from structural analysis is retained
@test occursin("The system is unbalanced.", msg)
end

@testset "Initialization error messages" begin
unbalanced = "The system is unbalanced."
msg = ModelingToolkitBase.initialization_structure_error_message(
:ExtraVariablesSystemException, unbalanced, (1, 3)
)
@test occursin("Initialization system is underdetermined", msg)
@test occursin(unbalanced, msg)
@test occursin("2 more equations are needed", msg)

msg = ModelingToolkitBase.initialization_structure_error_message(
:ExtraEquationsSystemException, unbalanced, (3, 2)
)
@test occursin("Initialization system is overdetermined", msg)
@test occursin("There is 1 equation too many", msg)

msg = ModelingToolkitBase.initialization_structure_error_message(
:InvalidSystemException, "structurally singular", nothing
)
@test occursin("Initialization system is structurally singular", msg)

old = ModelingToolkitBase.show_api_guidance!(false)
try
msg = ModelingToolkitBase.initialization_structure_error_message(
:ExtraVariablesSystemException, unbalanced, (1, 3)
)
# What went wrong is still described in full
@test occursin("Initialization system is underdetermined", msg)
@test occursin(unbalanced, msg)
@test occursin("2 more equations are needed", msg)
# Only the guidance naming ModelingToolkit functions is left out
@test !occursin("ODEProblem", msg)
@test !occursin("initialization_eqs", msg)
@test !occursin("fully_determined", msg)
finally
ModelingToolkitBase.show_api_guidance!(old)
end
@test ModelingToolkitBase.show_api_guidance()
# Counts of a system whose imbalance is unknown, or does not corroborate the reported
# one, are left out rather than guessed at.
@test ModelingToolkitBase.initialization_deficit_message(
:ExtraVariablesSystemException, nothing
) === nothing
@test ModelingToolkitBase.initialization_deficit_message(
:ExtraVariablesSystemException, (3, 3)
) === nothing
end

@connector function Port(; name, p = nothing, dm = 0)
vars = @variables begin
p(t) = p
Expand Down Expand Up @@ -367,6 +433,19 @@ end
sys, [], (0, 0.1), fully_determined = true
)

@testset "Overdetermined initialization error names the initialization system" begin
err = try
ODEProblem(sys, [], (0, 0.1), fully_determined = true)
nothing
catch e
e
end
@test err isa ERRMOD.ExtraEquationsSystemException
msg = sprint(showerror, err)
@test occursin("Initialization system is overdetermined", msg)
@test occursin("fully_determined = false", msg)
end

if @isdefined(ModelingToolkit)
prob = ODEProblem(sys, [], (0, 0.1), check = false)
sol = solve(prob, Rodas5P())
Expand Down
Loading