Skip to content

Commit de17b87

Browse files
feat: validate causality of analysis points
1 parent ddceb6b commit de17b87

File tree

2 files changed

+55
-9
lines changed

2 files changed

+55
-9
lines changed

src/systems/analysis_points.jl

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,36 @@ struct AnalysisPoint
7272
these are all `RealInput` connectors.
7373
"""
7474
outputs::Union{Nothing, Vector{Any}}
75+
76+
function AnalysisPoint(input, name::Symbol, outputs; verbose = true)
77+
# input to analysis point should be an output variable
78+
if verbose && input !== nothing
79+
var = ap_var(input)
80+
isoutput(var) || ap_warning(1, name, true)
81+
end
82+
# outputs of analysis points should be input variables
83+
if verbose && outputs !== nothing
84+
for (i, output) in enumerate(outputs)
85+
var = ap_var(output)
86+
isinput(var) || ap_warning(2 + i, name, false)
87+
end
88+
end
89+
90+
return new(input, name, outputs)
91+
end
92+
end
93+
94+
function ap_warning(arg::Int, name::Symbol, should_be_output)
95+
causality = should_be_output ? "output" : "input"
96+
@warn """
97+
The $(arg)-th argument to analysis point $(name) was not a $causality. This is supported in \
98+
order to handle inverse models, but may not be what you intended.
99+
100+
If you are building a forward mode (causal), you may want to swap this argument with \
101+
one on the opposite side of the name of the analysis point provided to `connect`. \
102+
Learn more about the causality of analysis points in the docstring for `AnalysisPoint`. \
103+
Silence this message using `connect(out, :name, in...; warn = false)`.
104+
"""
75105
end
76106

77107
AnalysisPoint() = AnalysisPoint(nothing, Symbol(), nothing)
@@ -133,8 +163,8 @@ function renamespace(sys, ap::AnalysisPoint)
133163
end
134164

135165
# create analysis points via `connect`
136-
function Symbolics.connect(in, ap::AnalysisPoint, outs...)
137-
return AnalysisPoint() ~ AnalysisPoint(in, ap.name, collect(outs))
166+
function Symbolics.connect(in, ap::AnalysisPoint, outs...; verbose = true)
167+
return AnalysisPoint() ~ AnalysisPoint(in, ap.name, collect(outs); verbose)
138168
end
139169

140170
"""
@@ -161,15 +191,21 @@ connect(P.input, :plant_input, C.output)
161191
162192
typically is not (unless the model is an inverse model).
163193
164-
# Arguments:
194+
# Arguments
195+
196+
- `output_connector`: An output connector
197+
- `input_connector`: An input connector
198+
- `ap`: An explicitly created [`AnalysisPoint`](@ref)
199+
- `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be
200+
created automatically.
201+
202+
# Keyword arguments
165203
166-
- `output_connector`: An output connector
167-
- `input_connector`: An input connector
168-
- `ap`: An explicitly created [`AnalysisPoint`](@ref)
169-
- `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be created automatically.
204+
- `verbose`: Warn if an input is connected to an output (reverse causality). Silence this
205+
warning if you are analyzing an inverse model.
170206
"""
171-
function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...)
172-
return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)])
207+
function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...; verbose = true)
208+
return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose)
173209
end
174210

175211
"""

test/analysis_points.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ using Symbolics: NAMESPACE_SEPARATOR
2424
@test isequal(sys_ap2, sys_normal2)
2525
end
2626

27+
@testset "Inverse causality throws a warning" begin
28+
@named P = FirstOrder(k = 1, T = 1)
29+
@named C = Gain(; k = -1)
30+
31+
ap = AnalysisPoint(:plant_input)
32+
@test_warn ["1-th argument", "plant_input", "not a output"] connect(
33+
P.input, ap, C.output)
34+
@test_nowarn connect(P.input, ap, C.output; verbose = false)
35+
end
36+
2737
# also tests `connect(input, name::Symbol, outputs...)` syntax
2838
@testset "AnalysisPoint is accessible via `getproperty`" begin
2939
@named P = FirstOrder(k = 1, T = 1)

0 commit comments

Comments
 (0)