Skip to content

Commit 47e80d0

Browse files
Merge pull request #89 from JuliaComputing/as/clock-inf-nested-ops
fix: handle nested clock operators
2 parents 319675b + 80d119c commit 47e80d0

3 files changed

Lines changed: 356 additions & 129 deletions

File tree

lib/ModelingToolkitTearing/src/clock_inference/clock_inference.jl

Lines changed: 212 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ struct NotInferredTimeDomain end
8181
struct InferEquationClosure
8282
varsbuf::OrderedSet{SymbolicT}
8383
# variables in each argument to an operator
84-
arg_varsbuf::Set{SymbolicT}
84+
arg_varsbuf::OrderedSet{SymbolicT}
8585
# hyperedge for each equation
8686
hyperedge::Set{ClockVertex.Type}
8787
# hyperedge for each argument to an operator
@@ -95,51 +95,87 @@ struct InferEquationClosure
9595
end
9696

9797
function InferEquationClosure(var_to_idx, inference_graph)
98-
InferEquationClosure(OrderedSet{SymbolicT}(), Set{SymbolicT}(), Set{ClockVertex.Type}(),
98+
InferEquationClosure(OrderedSet{SymbolicT}(), OrderedSet{SymbolicT}(), Set{ClockVertex.Type}(),
9999
Set{ClockVertex.Type}(), Dict{Int, Set{ClockVertex.Type}}(),
100100
Set{ClockVertex.Type}(), var_to_idx, inference_graph)
101101
end
102102

103-
function (iec::InferEquationClosure)(ieq::Int, eq::Equation, is_initialization_equation::Bool)
104-
(; varsbuf, arg_varsbuf, hyperedge, arg_hyperedge, relative_hyperedges) = iec
105-
(; must_be_discrete, var_to_idx, inference_graph) = iec
106-
empty!(varsbuf)
107-
empty!(hyperedge)
108-
# get variables in equation
109-
SU.search_variables!(varsbuf, eq)
110-
# add the equation to the hyperedge
111-
eq_node = if is_initialization_equation
112-
ClockVertex.InitEquation(ieq)
113-
else
114-
ClockVertex.Equation(ieq)
115-
end
116-
push!(hyperedge, eq_node)
117-
for var in varsbuf
118-
idx = get(var_to_idx, var, nothing)
119-
# if this is just a single variable, add it to the hyperedge
120-
if idx isa Int
121-
push!(hyperedge, ClockVertex.Variable(idx))
122-
d = get_time_domain(var)
123-
if is_concrete_time_domain(d)
124-
push!(hyperedge, ClockVertex.Clock(d))
125-
elseif d isa InferredTimeDomain
126-
@match d begin
127-
InferredClock.Inferred(id) => push!(hyperedge, ClockVertex.InferredClock(id))
128-
_ => nothing
129-
end
130-
elseif d isa MTKBase.IntegerSequence
131-
push!(hyperedge, ClockVertex.IntegerSequence())
132-
end
133-
134-
# we don't immediately `continue` here because this variable might be a
135-
# `Sample` or similar and we want the clock information from it if it is.
136-
end
103+
struct InferVariableClosure
104+
# The hyperedge that this variable exists in. Could be the hyperedge for an equation,
105+
# or one for the argument of a clock operator. The parent is responsible for
106+
# adding this hyperedge to the inference graph.
107+
parent_hyperedge::Set{ClockVertex.Type}
108+
# The list of variables to be explored in the parent. This allows
109+
# the variable to queue up additional variables to be searched
110+
# in the same context.
111+
vars_in_parent::OrderedSet{SymbolicT}
112+
# Mapping from `i` in `InferredDiscrete(i)` to the vertices in that inferred partition.
113+
relative_hyperedges::Dict{Int, Set{ClockVertex.Type}}
114+
# Variables in each argument to an operator.
115+
arg_varsbuf::OrderedSet{SymbolicT}
116+
# Hyperedge for each argument to an operator. This closure is responsible for
117+
# adding this hyperedge to the inference graph.
118+
arg_hyperedge::Set{ClockVertex.Type}
119+
var_to_idx::Dict{SymbolicT, Int}
120+
must_be_discrete::Set{ClockVertex.Type}
121+
inference_graph::HyperGraph{ClockVertex.Type}
122+
end
123+
124+
# When searching a variable/operator application present directly in an equation
125+
function InferVariableClosure(iec::InferEquationClosure)
126+
return InferVariableClosure(
127+
# This exists in the equation's hyperedge.
128+
iec.hyperedge,
129+
# Use the equation's queue for variables to explore.
130+
iec.varsbuf,
131+
# The next three are basically just preallocated buffers. Theoretically,
132+
# these could be omitted from both structs. However, it's very common to
133+
# have at least one clock operator in an expression, and unlikely to nest.
134+
# So this allows saving unnecessary allocations in each call to the
135+
# `InferVariableClosure`. It also allows the
136+
# `InferVariableClosure(::InferVariableClosure)` constructor to be simpler.
137+
iec.relative_hyperedges,
138+
iec.arg_varsbuf,
139+
iec.arg_hyperedge,
140+
# Shared information propagated across the hierarchy.
141+
iec.var_to_idx,
142+
iec.must_be_discrete,
143+
iec.inference_graph,
144+
)
145+
end
146+
147+
# When searching a variable/operator application present in the argument of
148+
# a clock operator. The returned `InferVariableClosure` will be called recursively
149+
# from the `ivc` passed in.
150+
function InferVariableClosure(ivc::InferVariableClosure)
151+
return InferVariableClosure(
152+
# This exists in the clock operator argument's hyperedge.
153+
ivc.arg_hyperedge,
154+
# Use the operator argument's queue.
155+
ivc.arg_varsbuf,
156+
# Cannot reuse buffers, allocate new ones.
157+
Dict{Int, Set{ClockVertex.Type}}(),
158+
OrderedSet{SymbolicT}(),
159+
Set{ClockVertex.Type}(),
160+
# Same shared information.
161+
ivc.var_to_idx,
162+
ivc.must_be_discrete,
163+
ivc.inference_graph,
164+
)
165+
end
166+
167+
function (ivc::InferVariableClosure)(var::SymbolicT)
168+
# NOTE: Never add `hyperedge` to the graph. The parent is responsible for doing so.
169+
(; var_to_idx, parent_hyperedge, vars_in_parent, relative_hyperedges) = ivc
170+
(; arg_varsbuf, arg_hyperedge, must_be_discrete, inference_graph) = ivc
171+
hyperedge = parent_hyperedge
172+
173+
idx = get(var_to_idx, var, nothing)
174+
# if this is just a single variable, add it to the hyperedge
175+
if idx isa Int
176+
push!(hyperedge, ClockVertex.Variable(idx))
137177
d = get_time_domain(var)
138-
if d === nothing
139-
arrv, isarr = MTKBase.split_indexed_var(var)
140-
d = get_time_domain(arrv)
141-
end
142-
if is_concrete_time_domain(d)::Bool
178+
if is_concrete_time_domain(d)
143179
push!(hyperedge, ClockVertex.Clock(d))
144180
elseif d isa InferredTimeDomain
145181
@match d begin
@@ -150,109 +186,158 @@ function (iec::InferEquationClosure)(ieq::Int, eq::Equation, is_initialization_e
150186
push!(hyperedge, ClockVertex.IntegerSequence())
151187
end
152188

153-
@match var begin
154-
BSImpl.Term(; f, args) && if f isa Shift end => begin
155-
d = get_time_domain(args[1])
156-
if is_concrete_time_domain(d)::Bool
157-
push!(hyperedge, ClockVertex.Clock(d))
158-
elseif d isa InferredTimeDomain
159-
@match d begin
160-
InferredClock.Inferred(id) => push!(hyperedge, ClockVertex.InferredClock(id))
161-
_ => nothing
162-
end
163-
elseif d isa MTKBase.IntegerSequence
164-
push!(hyperedge, ClockVertex.IntegerSequence())
165-
end
166-
end
189+
# we don't immediately `return` here because this variable might be a
190+
# `Sample` or similar and we want the clock information from it if it is.
191+
end
192+
193+
# Leverage concrete clock information about this variable/operator application
194+
d = get_time_domain(var)
195+
if d === nothing
196+
arrv, isarr = MTKBase.split_indexed_var(var)
197+
d = get_time_domain(arrv)
198+
end
199+
if is_concrete_time_domain(d)::Bool
200+
push!(hyperedge, ClockVertex.Clock(d))
201+
elseif d isa InferredTimeDomain
202+
@match d begin
203+
InferredClock.Inferred(id) => push!(hyperedge, ClockVertex.InferredClock(id))
167204
_ => nothing
168205
end
206+
elseif d isa MTKBase.IntegerSequence
207+
push!(hyperedge, ClockVertex.IntegerSequence())
208+
end
169209

170-
# now we only care about synchronous operators
171-
op, args = @match var begin
172-
BSImpl.Term(; f, args) && if is_timevarying_operator(f)::Bool end => (f, args)
173-
if SU.is_array_shape(SU.shape(var)) end => begin
174-
for i in SU.stable_eachindex(var)
175-
push!(varsbuf, var[i])
210+
@match var begin
211+
# `Shift` sets the clock metadata of its input, so use that
212+
BSImpl.Term(; f, args) && if f isa Shift end => begin
213+
d = get_time_domain(args[1])
214+
if is_concrete_time_domain(d)::Bool
215+
push!(hyperedge, ClockVertex.Clock(d))
216+
elseif d isa InferredTimeDomain
217+
@match d begin
218+
InferredClock.Inferred(id) => push!(hyperedge, ClockVertex.InferredClock(id))
219+
_ => nothing
176220
end
177-
continue
221+
elseif d isa MTKBase.IntegerSequence
222+
push!(hyperedge, ClockVertex.IntegerSequence())
178223
end
179-
_ => continue
180224
end
225+
_ => nothing
226+
end
181227

182-
# arguments and corresponding time domains
183-
tdomains = input_timedomain(op, args)::Vector{InputTimeDomainElT}
184-
nargs = length(args)
185-
ndoms = length(tdomains)
186-
if nargs != ndoms
187-
throw(ArgumentError("""
188-
Operator $op applied to $nargs arguments $args but only returns $ndoms \
189-
domains $tdomains from `input_timedomain`.
190-
"""))
228+
# now we only care about synchronous operators
229+
op, args = @match var begin
230+
BSImpl.Term(; f, args) && if is_timevarying_operator(f)::Bool end => (f, args)
231+
# If we have a symbolic array variable, process each of its elements
232+
if SU.is_array_shape(SU.shape(var)) end => begin
233+
for i in SU.stable_eachindex(var)
234+
push!(vars_in_parent, var[i])
235+
end
236+
return
191237
end
238+
_ => return
239+
end
192240

193-
# each relative clock mapping is only valid per operator application
194-
empty!(relative_hyperedges)
195-
for (arg, domain) in zip(args, tdomains)
196-
empty!(arg_varsbuf)
197-
empty!(arg_hyperedge)
198-
# get variables in argument
199-
SU.search_variables!(arg_varsbuf, arg; is_atomic = MTKBase.OperatorIsAtomic{Union{Differential, MTKBase.Shift}}())
200-
# get hyperedge for involved variables
201-
for v in arg_varsbuf
202-
vidx = get(var_to_idx, v, nothing)
203-
vidx === nothing && continue
204-
push!(arg_hyperedge, ClockVertex.Variable(vidx))
205-
end
241+
# arguments and corresponding time domains
242+
tdomains = input_timedomain(op, args)::Vector{InputTimeDomainElT}
243+
nargs = length(args)
244+
ndoms = length(tdomains)
245+
if nargs != ndoms
246+
throw(ArgumentError("""
247+
Operator $op applied to $nargs arguments $args but only returns $ndoms \
248+
domains $tdomains from `input_timedomain`.
249+
"""))
250+
end
206251

207-
@match domain begin
208-
# If the time domain for this argument is a clock, then all variables in this edge have that clock.
209-
x::SciMLBase.AbstractClock => begin
210-
# add the clock to the edge
211-
push!(arg_hyperedge, ClockVertex.Clock(x))
212-
# add the edge to the graph
213-
add_edge!(inference_graph, arg_hyperedge)
214-
end
215-
x::MTKBase.IntegerSequence => begin
216-
push!(arg_hyperedge, ClockVertex.IntegerSequence())
217-
add_edge!(inference_graph, arg_hyperedge)
218-
end
219-
# We only know that this time domain is inferred. Treat it as a unique domain, all we know is that the
220-
# involved variables have the same clock.
221-
InferredClock.Inferred() => add_edge!(inference_graph, arg_hyperedge)
222-
# All `InferredDiscrete` with the same `i` have the same clock (including output domain) so we don't
223-
# add the edge, and instead add this to the `relative_hyperedges` mapping.
224-
InferredClock.InferredDiscrete(i) => begin
225-
relative_edge = get!(Set{ClockVertex.Type}, relative_hyperedges, i)
226-
union!(relative_edge, arg_hyperedge)
227-
# Ensure that this clock partition will be discrete.
228-
union!(must_be_discrete, arg_hyperedge)
229-
add_edge!(inference_graph, arg_hyperedge)
230-
end
252+
nested_ivc = InferVariableClosure(ivc)
253+
# each relative clock mapping is only valid per operator application
254+
empty!(relative_hyperedges)
255+
for (arg, domain) in zip(args, tdomains)
256+
empty!(arg_varsbuf)
257+
empty!(arg_hyperedge)
258+
# get variables in argument
259+
SU.search_variables!(arg_varsbuf, arg)
260+
# get hyperedge for involved variables
261+
for v in arg_varsbuf
262+
nested_ivc(v)
263+
end
264+
265+
# NOTE: Ensure all branches here add `arg_hyperedge` to the inference graph. It
266+
# is the parent hyperedge for `nested_ivc`, so this closure is responsible for
267+
# adding it to the graph.
268+
@match domain begin
269+
# If the time domain for this argument is a clock, then all variables in this edge have that clock.
270+
x::SciMLBase.AbstractClock => begin
271+
# add the clock to the edge
272+
push!(arg_hyperedge, ClockVertex.Clock(x))
273+
# add the edge to the graph
274+
add_edge!(inference_graph, arg_hyperedge)
275+
end
276+
x::MTKBase.IntegerSequence => begin
277+
push!(arg_hyperedge, ClockVertex.IntegerSequence())
278+
add_edge!(inference_graph, arg_hyperedge)
279+
end
280+
# We only know that this time domain is inferred. Treat it as a unique domain, all we know is that the
281+
# involved variables have the same clock.
282+
InferredClock.Inferred() => add_edge!(inference_graph, arg_hyperedge)
283+
# All `InferredDiscrete` with the same `i` have the same clock (including output domain) so we don't
284+
# add the edge, and instead add this to the `relative_hyperedges` mapping.
285+
InferredClock.InferredDiscrete(i) => begin
286+
relative_edge = get!(Set{ClockVertex.Type}, relative_hyperedges, i)
287+
union!(relative_edge, arg_hyperedge)
288+
# Ensure that this clock partition will be discrete.
289+
union!(must_be_discrete, arg_hyperedge)
290+
add_edge!(inference_graph, arg_hyperedge)
231291
end
232292
end
293+
end
233294

234-
outdomain = output_timedomain(op, args)
235-
if outdomain isa SciMLBase.AbstractClock
236-
push!(hyperedge, ClockVertex.Clock(outdomain))
237-
elseif outdomain isa InferredTimeDomain
238-
@match outdomain begin
239-
InferredClock.Inferred() => nothing
240-
InferredClock.InferredDiscrete(i) => begin
241-
buffer = get(relative_hyperedges, i, nothing)
242-
if buffer !== nothing
243-
union!(hyperedge, buffer)
244-
delete!(relative_hyperedges, i)
245-
end
246-
union!(must_be_discrete, hyperedge)
295+
# Handle the output timedomain of this clock operator application.
296+
outdomain = output_timedomain(op, args)
297+
if outdomain isa SciMLBase.AbstractClock
298+
# If it is a clock we know, simply add it to the parent edge.
299+
push!(hyperedge, ClockVertex.Clock(outdomain))
300+
elseif outdomain isa InferredTimeDomain
301+
@match outdomain begin
302+
InferredClock.Inferred() => nothing
303+
InferredClock.InferredDiscrete(i) => begin
304+
# If we've encountered this relative inferreddiscrete index before,
305+
# we know that that relative hyperedge is the output. So include it
306+
# in the parent hyperedge.
307+
buffer = get(relative_hyperedges, i, nothing)
308+
if buffer !== nothing
309+
union!(hyperedge, buffer)
310+
delete!(relative_hyperedges, i)
247311
end
312+
# Everything here must be discrete.
313+
union!(must_be_discrete, hyperedge)
248314
end
249-
else
250-
error("Unreachable reached!")
251315
end
316+
else
317+
error("Unreachable reached!")
318+
end
252319

253-
for (_, relative_edge) in relative_hyperedges
254-
add_edge!(inference_graph, relative_edge)
255-
end
320+
for (_, relative_edge) in relative_hyperedges
321+
add_edge!(inference_graph, relative_edge)
322+
end
323+
end
324+
325+
function (iec::InferEquationClosure)(ieq::Int, eq::Equation, is_initialization_equation::Bool)
326+
(; varsbuf, hyperedge, inference_graph) = iec
327+
empty!(varsbuf)
328+
empty!(hyperedge)
329+
# get variables in equation
330+
SU.search_variables!(varsbuf, eq)
331+
# add the equation to the hyperedge
332+
eq_node = if is_initialization_equation
333+
ClockVertex.InitEquation(ieq)
334+
else
335+
ClockVertex.Equation(ieq)
336+
end
337+
push!(hyperedge, eq_node)
338+
ivc = InferVariableClosure(iec)
339+
for var in varsbuf
340+
ivc(var)
256341
end
257342

258343
add_edge!(inference_graph, hyperedge)

0 commit comments

Comments
 (0)