-
-
Notifications
You must be signed in to change notification settings - Fork 260
Expand file tree
/
Copy pathdebugging.jl
More file actions
220 lines (198 loc) · 8.28 KB
/
Copy pathdebugging.jl
File metadata and controls
220 lines (198 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
struct LoggedFunctionException <: Exception
msg::String
end
struct LoggedFun{F}
f::F
args::Any
error_nonfinite::Bool
end
function LoggedFunctionException(lf::LoggedFun, args, msg)
return LoggedFunctionException(
"Function $(lf.f)($(join(lf.args, ", "))) " * msg * " with input" *
join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability
)
end
Base.showerror(io::IO, err::LoggedFunctionException) = print(io, err.msg)
Base.nameof(lf::LoggedFun) = nameof(lf.f)
SymbolicUtils.promote_symtype(f::LoggedFun, Ts::SU.TypeT...) = SU.promote_symtype(f.f, Ts...)
SU.promote_shape(f::LoggedFun, @nospecialize(shs::SU.ShapeT...)) = SU.promote_shape(f.f, shs...)
function (lf::LoggedFun)(args...)
val = try
lf.f(args...) # try to call with numerical input, as usual
catch err
throw(LoggedFunctionException(lf, args, "errors")) # Julia automatically attaches original error message
end
if lf.error_nonfinite && !isfinite(val)
throw(LoggedFunctionException(lf, args, "output non-finite value $val"))
end
return val
end
function logged_fun(f, args...; error_nonfinite = true) # remember to update error_nonfinite in debug_system() docstring
# Currently we don't really support complex numbers
return maketerm(SymbolicT, LoggedFun(f, args, error_nonfinite), args, nothing)
end
function debug_sub(eq::Equation, funcs; kw...)
return debug_sub(eq.lhs, funcs; kw...) ~ debug_sub(eq.rhs, funcs; kw...)
end
function debug_sub(ex, funcs; kw...)
iscall(ex) || return ex
f = operation(ex)
args = map(ex -> debug_sub(ex, funcs; kw...), arguments(ex))
return f in funcs ? logged_fun(f, args...; kw...) :
maketerm(typeof(ex), f, args, metadata(ex))
end
"""
$(TYPEDSIGNATURES)
A function which returns `NaN` if `condition` fails, and `0.0` otherwise.
"""
function _nan_condition(condition::Bool)
return condition ? 0.0 : NaN
end
@register_symbolic _nan_condition(condition::Bool)
"""
$(TYPEDSIGNATURES)
A function which takes a condition `expr` and returns `NaN` if it is false,
and zero if it is true. In case the condition is false and `log == true`,
`message` will be logged as an `@error`.
"""
function _debug_assertion(expr::Bool, message::String, log::Bool)
value = _nan_condition(expr)
isnan(value) || return value
log && @error message
return value
end
@register_symbolic _debug_assertion(expr::Bool, message::String, log::Bool)
"""
Boolean parameter added to models returned from `debug_system` to control logging of
assertions.
"""
const ASSERTION_LOG_VARIABLE = only(@parameters __log_assertions_ₘₜₖ::Bool = false)
"""
$(TYPEDSIGNATURES)
Get a symbolic expression for all the assertions in `sys`. The expression returns `NaN`
if any of the assertions fail, and `0.0` otherwise. If `ASSERTION_LOG_VARIABLE` is a
parameter in the system, it will control whether the message associated with each
assertion is logged when it fails.
"""
function get_assertions_expr(sys::AbstractSystem)
asserts = assertions(sys)
term = 0
if is_parameter(sys, ASSERTION_LOG_VARIABLE)
for (k, v) in asserts
term += _debug_assertion(k, "Assertion $k failed:\n$v", ASSERTION_LOG_VARIABLE)
end
else
for (k, v) in asserts
term += _nan_condition(k)
end
end
return term
end
function SciMLBase.diagnose_symbolic_instability(sys::AbstractSystem, u, uprev)
diagnosis = String[]
#check for assertion failures
unks = unknowns(sys)
curr_substitution_map = Dict{SymbolicT, SymbolicT}(zip(unks, u))
prev_substitution_map = Dict{SymbolicT, SymbolicT}(zip(unknowns(sys), uprev))
for (cond, msg) in assertions(sys)
subclauses = String[]
find_failing_subterms(cond, prev_substitution_map, curr_substitution_map, subclauses)
if !isempty(subclauses)
push!(diagnosis, "\n\nAssertion violated: $cond - \"$msg\"")
append!(diagnosis, subclauses)
end
end
#find singularity causes in equations
singularities = String[]
visited = IdDict{SymbolicT, Nothing}()
subber = SymbolicUtils.IRSubstituter{true}(get_irstructure(sys), prev_substitution_map)
for eq in full_equations(sys)
find_singular_subterms(eq, eq.rhs, subber, singularities, visited)
end
if !isempty(singularities)
push!(diagnosis, "\nSymbolic Analysis of MTK System:")
append!(diagnosis, singularities)
end
return isempty(diagnosis) ? "" : join(diagnosis, "\n")
end
function find_singular_subterms(eq, expr, sub_map, diagnosis, visited)
expr = unwrap(expr)
!SymbolicUtils.iscall(expr) && return diagnosis
op = SymbolicUtils.operation(expr)
args = SymbolicUtils.arguments(expr)
haskey(visited, expr) && return diagnosis
visited[expr] = nothing
if op === (/) #division, singular if we divide by small thing
d = Symbolics.value(sub_map(args[2]))
if d isa Number && abs(d) < 1e-10
push!(diagnosis, "in equation $eq: division by very small value $(args[2]) ≈ $(@sprintf("%.4g", d)) leads to singularity.")
end
elseif op === log #singular if we log small thing
x = Symbolics.value(sub_map(args[1]))
if x isa Number && x <= 1e-10
push!(diagnosis, "in equation $eq: log of $(args[1]) = $(@sprintf("%.4g", x)) near/at singularity (derivative blows up).")
end
elseif op === sqrt
x = Symbolics.value(sub_map(args[1]))
if x isa Number && x < 1e-10
push!(diagnosis, "in equation $eq: sqrt of $(args[1]) = $(@sprintf("%.4g", x)) near/at singularity (derivative blows up).")
end
elseif op === (^)
e = Symbolics.value(sub_map(args[2]))
b = Symbolics.value(sub_map(args[1]))
if e isa Number && b isa Number #two cases
if e < 0 && abs(b) < 1e-10
push!(diagnosis, "in equation $eq: ($(args[1])) raised to power $e with base ≈ $(@sprintf("%.4g", b)) going to 0; result diverges.")
elseif e > 0 && abs(b) > 1
push!(diagnosis, "in equation $eq: ($(args[1]) ≈ $(@sprintf("%.4g", b))) raised to power $e - base magnitude is large and being amplified.")
end
end
end
for arg in args
find_singular_subterms(eq, arg, sub_map, diagnosis, visited)
end
return diagnosis
end
function find_failing_subterms(cond, prev_map, curr_map, diagnosis)
c = Symbolics.unwrap(cond)
!SymbolicUtils.iscall(c) && return diagnosis
op = SymbolicUtils.operation(c)
args = SymbolicUtils.arguments(c)
if (op === (<) || op === (>) || op === (<=) || op === (>=)) && length(args) == 2
#compare using previous non-nan values to find violating subclauses, then output current values
lhs = Symbolics.value(Symbolics.substitute(args[1], prev_map))
rhs = Symbolics.value(Symbolics.substitute(args[2], prev_map))
if lhs isa Number && rhs isa Number
# small margin -> violated
margin = (op === (<) || op === (<=)) ? rhs - lhs : lhs - rhs
if margin <= 1e-6
push!(diagnosis, " subclause `$c` violated: $(clause_values(c, curr_map))")
end
end
elseif op === (!=) && length(args) == 2
lhs = Symbolics.value(Symbolics.substitute(args[1], prev_map))
rhs = Symbolics.value(Symbolics.substitute(args[2], prev_map))
if lhs isa Number && rhs isa Number && abs(lhs - rhs) <= 1e-6
push!(diagnosis, " subclause `$c` violated: $(clause_values(c, curr_map))")
end
elseif op === (==) && length(args) == 2
lhs = Symbolics.value(Symbolics.substitute(args[1], prev_map))
rhs = Symbolics.value(Symbolics.substitute(args[2], prev_map))
if lhs isa Number && rhs isa Number && abs(lhs - rhs) > 1e-6
push!(diagnosis, " subclause `$c` violated: $(clause_values(c, curr_map))")
end
else #recurse
for arg in args
find_failing_subterms(arg, prev_map, curr_map, diagnosis)
end
end
return diagnosis
end
function clause_values(c, curr_map)
parts = String[]
for v in Symbolics.get_variables(c)
val = Symbolics.value(Symbolics.substitute(v, curr_map))
push!(parts, val isa Number ? "$v = $(@sprintf("%.4g", val))" : "$v = $val")
end
return join(parts, ", ")
end