Skip to content

Commit e9b4d6e

Browse files
Merge branch 'master' into test-SDEFunctionExpr
2 parents fe704ed + 5ede0a7 commit e9b4d6e

24 files changed

+1029
-314
lines changed

Project.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelingToolkit"
22
uuid = "961ee093-0014-501f-94e3-6117800e7a78"
33
authors = ["Yingbo Ma <[email protected]>", "Chris Rackauckas <[email protected]> and contributors"]
4-
version = "9.60.0"
4+
version = "9.61.0"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -123,7 +123,7 @@ NonlinearSolve = "4.3"
123123
OffsetArrays = "1"
124124
OrderedCollections = "1"
125125
OrdinaryDiffEq = "6.82.0"
126-
OrdinaryDiffEqCore = "1.13.0"
126+
OrdinaryDiffEqCore = "1.15.0"
127127
OrdinaryDiffEqDefault = "1.2"
128128
OrdinaryDiffEqNonlinearSolve = "1.3.0"
129129
PrecompileTools = "1"
@@ -132,7 +132,7 @@ RecursiveArrayTools = "3.26"
132132
Reexport = "0.2, 1"
133133
RuntimeGeneratedFunctions = "0.5.9"
134134
SCCNonlinearSolve = "1.0.0"
135-
SciMLBase = "2.68.1"
135+
SciMLBase = "2.71.1"
136136
SciMLStructures = "1.0"
137137
Serialization = "1"
138138
Setfield = "0.7, 0.8, 1"
@@ -143,7 +143,7 @@ StaticArrays = "0.10, 0.11, 0.12, 1.0"
143143
StochasticDiffEq = "6.72.1"
144144
StochasticDelayDiffEq = "1.8.1"
145145
SymbolicIndexingInterface = "0.3.36"
146-
SymbolicUtils = "3.7"
146+
SymbolicUtils = "3.10"
147147
Symbolics = "6.22.1"
148148
URIs = "1"
149149
UnPack = "0.1, 1.0"

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pages = [
3232
"basics/InputOutput.md",
3333
"basics/MTKLanguage.md",
3434
"basics/Validation.md",
35+
"basics/Debugging.md",
3536
"basics/DependencyGraphs.md",
3637
"basics/Precompilation.md",
3738
"basics/FAQ.md"],

docs/src/basics/Debugging.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Debugging
2+
3+
Every (mortal) modeler writes models that contain mistakes or are susceptible to numerical errors in their hunt for the perfect model.
4+
Debugging such errors is part of the modeling process, and ModelingToolkit includes some functionality that helps with this.
5+
6+
For example, consider an ODE model with "dangerous" functions (here ``):
7+
8+
```@example debug
9+
using ModelingToolkit, OrdinaryDiffEq
10+
using ModelingToolkit: t_nounits as t, D_nounits as D
11+
12+
@variables u1(t) u2(t) u3(t)
13+
eqs = [D(u1) ~ -√(u1), D(u2) ~ -√(u2), D(u3) ~ -√(u3)]
14+
defaults = [u1 => 1.0, u2 => 2.0, u3 => 3.0]
15+
@named sys = ODESystem(eqs, t; defaults)
16+
sys = structural_simplify(sys)
17+
```
18+
19+
This problem causes the ODE solver to crash:
20+
21+
```@repl debug
22+
prob = ODEProblem(sys, [], (0.0, 10.0), []);
23+
sol = solve(prob, Tsit5());
24+
```
25+
26+
This suggests *that* something went wrong, but not exactly *what* went wrong and *where* it did.
27+
In such situations, the `debug_system` function is helpful:
28+
29+
```@repl debug
30+
dsys = debug_system(sys; functions = [sqrt]);
31+
dprob = ODEProblem(dsys, [], (0.0, 10.0), []);
32+
dsol = solve(dprob, Tsit5());
33+
```
34+
35+
Now we see that it crashed because `u1` decreased so much that it became negative and outside the domain of the `` function.
36+
We could have figured that out ourselves, but it is not always so obvious for more complex models.
37+
38+
```@docs
39+
debug_system
40+
```

src/ModelingToolkit.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ import SCCNonlinearSolve
5454
using Reexport
5555
using RecursiveArrayTools
5656
import Graphs: SimpleDiGraph, add_edge!, incidence_matrix
57-
import BlockArrays: BlockedArray, Block, blocksize, blocksizes
57+
import BlockArrays: BlockArray, BlockedArray, Block, blocksize, blocksizes, blockpush!,
58+
undef_blocks, blocks
5859
import CommonSolve
5960
import EnumX
6061

@@ -145,8 +146,8 @@ include("systems/index_cache.jl")
145146
include("systems/parameter_buffer.jl")
146147
include("systems/abstractsystem.jl")
147148
include("systems/model_parsing.jl")
148-
include("systems/analysis_points.jl")
149149
include("systems/connectors.jl")
150+
include("systems/analysis_points.jl")
150151
include("systems/imperative_affect.jl")
151152
include("systems/callbacks.jl")
152153
include("systems/problem_utils.jl")

src/debugging.jl

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,44 @@
1-
const LOGGED_FUN = Set([log, sqrt, (^), /, inv])
2-
is_legal(::typeof(/), a, b) = is_legal(inv, b)
3-
is_legal(::typeof(inv), a) = !iszero(a)
4-
is_legal(::Union{typeof(log), typeof(sqrt)}, a) = a isa Complex || a >= zero(a)
5-
is_legal(::typeof(^), a, b) = a isa Complex || b isa Complex || isinteger(b) || a >= zero(a)
6-
1+
struct LoggedFunctionException <: Exception
2+
msg::String
3+
end
74
struct LoggedFun{F}
85
f::F
96
args::Any
7+
error_nonfinite::Bool
8+
end
9+
function LoggedFunctionException(lf::LoggedFun, args, msg)
10+
LoggedFunctionException(
11+
"Function $(lf.f)($(join(lf.args, ", "))) " * msg * " with input" *
12+
join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability
13+
)
1014
end
15+
Base.showerror(io::IO, err::LoggedFunctionException) = print(io, err.msg)
1116
Base.nameof(lf::LoggedFun) = nameof(lf.f)
1217
SymbolicUtils.promote_symtype(::LoggedFun, Ts...) = Real
1318
function (lf::LoggedFun)(args...)
14-
f = lf.f
15-
symbolic_args = lf.args
16-
if is_legal(f, args...)
17-
f(args...)
18-
else
19-
args_str = join(string.(symbolic_args .=> args), ", ", ", and ")
20-
throw(DomainError(args, "$(lf.f) errors with input(s): $args_str"))
19+
val = try
20+
lf.f(args...) # try to call with numerical input, as usual
21+
catch err
22+
throw(LoggedFunctionException(lf, args, "errors")) # Julia automatically attaches original error message
2123
end
24+
if lf.error_nonfinite && !isfinite(val)
25+
throw(LoggedFunctionException(lf, args, "output non-finite value $val"))
26+
end
27+
return val
2228
end
2329

24-
function logged_fun(f, args...)
30+
function logged_fun(f, args...; error_nonfinite = true) # remember to update error_nonfinite in debug_system() docstring
2531
# Currently we don't really support complex numbers
26-
term(LoggedFun(f, args), args..., type = Real)
32+
term(LoggedFun(f, args, error_nonfinite), args..., type = Real)
2733
end
2834

29-
debug_sub(eq::Equation) = debug_sub(eq.lhs) ~ debug_sub(eq.rhs)
30-
function debug_sub(ex)
35+
function debug_sub(eq::Equation, funcs; kw...)
36+
debug_sub(eq.lhs, funcs; kw...) ~ debug_sub(eq.rhs, funcs; kw...)
37+
end
38+
function debug_sub(ex, funcs; kw...)
3139
iscall(ex) || return ex
3240
f = operation(ex)
33-
args = map(debug_sub, arguments(ex))
34-
f in LOGGED_FUN ? logged_fun(f, args...) :
41+
args = map(ex -> debug_sub(ex, funcs; kw...), arguments(ex))
42+
f in funcs ? logged_fun(f, args...; kw...) :
3543
maketerm(typeof(ex), f, args, metadata(ex))
3644
end

src/systems/abstractsystem.jl

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,6 +1875,13 @@ Equivalent to `length(equations(expand_connections(sys))) - length(filter(eq ->
18751875
function n_expanded_connection_equations(sys::AbstractSystem)
18761876
# TODO: what about inputs?
18771877
isconnector(sys) && return length(get_unknowns(sys))
1878+
sys = remove_analysis_points(sys)
1879+
n_variable_connect_eqs = 0
1880+
for eq in equations(sys)
1881+
is_causal_variable_connection(eq.rhs) || continue
1882+
n_variable_connect_eqs += length(get_systems(eq.rhs)) - 1
1883+
end
1884+
18781885
sys, (csets, _) = generate_connection_set(sys)
18791886
ceqs, instream_csets = generate_connection_equations_and_stream_connections(csets)
18801887
n_outer_stream_variables = 0
@@ -1897,7 +1904,7 @@ function n_expanded_connection_equations(sys::AbstractSystem)
18971904
# n_toplevel_unused_flows += count(x->get_connection_type(x) === Flow && !(x in toplevel_flows), get_unknowns(m))
18981905
#end
18991906

1900-
nextras = n_outer_stream_variables + length(ceqs)
1907+
nextras = n_outer_stream_variables + length(ceqs) + n_variable_connect_eqs
19011908
end
19021909

19031910
function Base.show(
@@ -1919,7 +1926,7 @@ function Base.show(
19191926
nrows > 0 && hint && print(io, " see hierarchy($name)")
19201927
for i in 1:nrows
19211928
sub = subs[i]
1922-
name = String(nameof(sub))
1929+
local name = String(nameof(sub))
19231930
print(io, "\n ", name)
19241931
desc = description(sub)
19251932
if !isempty(desc)
@@ -2274,37 +2281,37 @@ macro mtkbuild(exprs...)
22742281
end
22752282

22762283
"""
2277-
$(SIGNATURES)
2284+
debug_system(sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], error_nonfinite = true)
22782285
2279-
Replace functions with singularities with a function that errors with symbolic
2280-
information. E.g.
2286+
Wrap `functions` in `sys` so any error thrown in them shows helpful symbolic-numeric
2287+
information about its input. If `error_nonfinite`, functions that output nonfinite
2288+
values (like `Inf` or `NaN`) also display errors, even though the raw function itself
2289+
does not throw an exception (like `1/0`). For example:
22812290
22822291
```julia-repl
2283-
julia> sys = debug_system(sys);
2292+
julia> sys = debug_system(complete(sys))
22842293
2285-
julia> sys = complete(sys);
2294+
julia> prob = ODEProblem(sys, [0.0, 2.0], (0.0, 1.0))
22862295
2287-
julia> prob = ODEProblem(sys, [], (0, 1.0));
2288-
2289-
julia> du = zero(prob.u0);
2290-
2291-
julia> prob.f(du, prob.u0, prob.p, 0.0)
2292-
ERROR: DomainError with (-1.0,):
2293-
log errors with input(s): -cos(Q(t)) => -1.0
2294-
Stacktrace:
2295-
[1] (::ModelingToolkit.LoggedFun{typeof(log)})(args::Float64)
2296-
...
2296+
julia> prob.f(prob.u0, prob.p, 0.0)
2297+
ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input
2298+
1 => 1
2299+
sin(P(t)) => 0.0
22972300
```
22982301
"""
2299-
function debug_system(sys::AbstractSystem)
2302+
function debug_system(
2303+
sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], kw...)
2304+
if !(functions isa Set)
2305+
functions = Set(functions) # more efficient "in" lookup
2306+
end
23002307
if has_systems(sys) && !isempty(get_systems(sys))
2301-
error("debug_system only works on systems with no sub-systems!")
2308+
error("debug_system(sys) only works on systems with no sub-systems! Consider flattening it with flatten(sys) or structural_simplify(sys) first.")
23022309
end
23032310
if has_eqs(sys)
2304-
@set! sys.eqs = debug_sub.(equations(sys))
2311+
@set! sys.eqs = debug_sub.(equations(sys), Ref(functions); kw...)
23052312
end
23062313
if has_observed(sys)
2307-
@set! sys.observed = debug_sub.(observed(sys))
2314+
@set! sys.observed = debug_sub.(observed(sys), Ref(functions); kw...)
23082315
end
23092316
return sys
23102317
end

src/systems/analysis_points.jl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...; verbo
208208
return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose)
209209
end
210210

211+
function Symbolics.connect(
212+
in::ConnectableSymbolicT, name::Symbol, out::ConnectableSymbolicT,
213+
outs::ConnectableSymbolicT...; verbose = true)
214+
allvars = (in, out, outs...)
215+
validate_causal_variables_connection(allvars)
216+
return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose)
217+
end
218+
211219
"""
212220
$(TYPEDSIGNATURES)
213221
@@ -240,7 +248,7 @@ connection. This is the variable named `u` if present, and otherwise the only
240248
variable in the system. If the system does not have a variable named `u` and
241249
contains multiple variables, throw an error.
242250
"""
243-
function ap_var(sys)
251+
function ap_var(sys::AbstractSystem)
244252
if hasproperty(sys, :u)
245253
return sys.u
246254
end
@@ -249,6 +257,15 @@ function ap_var(sys)
249257
error("Could not determine the analysis-point variable in system $(nameof(sys)). To use an analysis point, apply it to a connection between causal blocks which have a variable named `u` or a single unknown of the same size.")
250258
end
251259

260+
"""
261+
$(TYPEDSIGNATURES)
262+
263+
For an `AnalysisPoint` involving causal variables. Simply return the variable.
264+
"""
265+
function ap_var(var::ConnectableSymbolicT)
266+
return var
267+
end
268+
252269
"""
253270
$(TYPEDEF)
254271

0 commit comments

Comments
 (0)