@@ -259,12 +259,135 @@ function _scale_reachability_from_mtg(mtg)
259259 return scale_reachability
260260end
261261
262- function _input_candidates_for_var (model_specs, consumer_scale:: String , consumer_process:: Symbol , input_var:: Symbol ; scale_reachability= nothing )
262+ function _effective_timestep_spec (spec:: ModelSpec )
263+ ts = timestep (spec)
264+ return isnothing (ts) ? timespec (model_ (spec)) : ts
265+ end
266+
267+ function _timestep_signature (ts)
268+ if ts isa ClockSpec
269+ return (:clock , float (ts. dt), float (ts. phase))
270+ elseif ts isa Real
271+ return (:step , float (ts), 0.0 )
272+ elseif ts isa Dates. FixedPeriod
273+ return (:period , _seconds_from_period (ts), 0.0 )
274+ end
275+ return nothing
276+ end
277+
278+ function _same_timestep_signature (sig_a, sig_b)
279+ isnothing (sig_a) && return false
280+ isnothing (sig_b) && return false
281+
282+ if sig_a[1 ] == :period || sig_b[1 ] == :period
283+ return sig_a[1 ] == :period &&
284+ sig_b[1 ] == :period &&
285+ isapprox (sig_a[2 ], sig_b[2 ]; atol= 1.0e-9 , rtol= 0.0 )
286+ end
287+
288+ phase_a = sig_a[1 ] == :step ? 0.0 : sig_a[3 ]
289+ phase_b = sig_b[1 ] == :step ? 0.0 : sig_b[3 ]
290+ return isapprox (sig_a[2 ], sig_b[2 ]; atol= 1.0e-9 , rtol= 0.0 ) &&
291+ isapprox (phase_a, phase_b; atol= 1.0e-9 , rtol= 0.0 )
292+ end
293+
294+ function _hard_dep_same_rate_as_parent (model_specs, parent_scale:: String , parent_process:: Symbol , child_scale:: String , child_process:: Symbol )
295+ parent_scale == child_scale || return false
296+ parent_specs = get (model_specs, parent_scale, nothing )
297+ isnothing (parent_specs) && return false
298+ parent_spec = get (parent_specs, parent_process, nothing )
299+ child_spec = get (parent_specs, child_process, nothing )
300+ isnothing (parent_spec) && return false
301+ isnothing (child_spec) && return false
302+
303+ parent_sig = _timestep_signature (_effective_timestep_spec (parent_spec))
304+ child_sig = _timestep_signature (_effective_timestep_spec (child_spec))
305+ return _same_timestep_signature (parent_sig, child_sig)
306+ end
307+
308+ function _collect_same_rate_hard_dependency_children! (
309+ ignored_processes_by_scale:: Dict{String,Set{Symbol}} ,
310+ model_specs,
311+ parent_scale:: String ,
312+ parent_process:: Symbol ,
313+ child:: HardDependencyNode
314+ )
315+ if _hard_dep_same_rate_as_parent (model_specs, parent_scale, parent_process, child. scale, child. process)
316+ push! (get! (ignored_processes_by_scale, child. scale, Set {Symbol} ()), child. process)
317+ end
318+
319+ for nested in child. children
320+ _collect_same_rate_hard_dependency_children! (
321+ ignored_processes_by_scale,
322+ model_specs,
323+ child. scale,
324+ child. process,
325+ nested
326+ )
327+ end
328+
329+ return nothing
330+ end
331+
332+ function _soft_nodes_for_hard_dependency_analysis (dep_graph:: DependencyGraph{Dict{String,Any}} )
333+ nodes = SoftDependencyNode[]
334+ for (_, roots_at_scale) in pairs (dep_graph. roots)
335+ haskey (roots_at_scale, :soft_dep_graph ) || continue
336+ append! (nodes, values (roots_at_scale[:soft_dep_graph ]))
337+ end
338+ return nodes
339+ end
340+
341+ _soft_nodes_for_hard_dependency_analysis (dep_graph:: DependencyGraph ) = traverse_dependency_graph (dep_graph, false )
342+
343+ function _same_rate_hard_dependency_children (model_specs, dep_graph:: DependencyGraph )
344+ ignored_processes_by_scale = Dict {String,Set{Symbol}} ()
345+
346+ for soft_node in _soft_nodes_for_hard_dependency_analysis (dep_graph)
347+ for child in soft_node. hard_dependency
348+ _collect_same_rate_hard_dependency_children! (
349+ ignored_processes_by_scale,
350+ model_specs,
351+ soft_node. scale,
352+ soft_node. process,
353+ child
354+ )
355+ end
356+ end
357+
358+ return ignored_processes_by_scale
359+ end
360+
361+ function _active_processes_for_inference (model_specs, ignored_processes_by_scale:: Dict{String,Set{Symbol}} )
362+ active = Dict {String,Set{Symbol}} ()
363+ for (scale, specs_at_scale) in pairs (model_specs)
364+ procs = Set {Symbol} (keys (specs_at_scale))
365+ ignored = get (ignored_processes_by_scale, scale, Set {Symbol} ())
366+ for process in ignored
367+ delete! (procs, process)
368+ end
369+ active[scale] = procs
370+ end
371+ return active
372+ end
373+
374+ function _input_candidates_for_var (
375+ model_specs,
376+ consumer_scale:: String ,
377+ consumer_process:: Symbol ,
378+ input_var:: Symbol ;
379+ scale_reachability= nothing ,
380+ active_processes_by_scale= nothing
381+ )
263382 same_scale = NamedTuple[]
264383 cross_scale = NamedTuple[]
265384
266385 for (scale, specs_at_scale) in pairs (model_specs)
267386 for (process, spec) in pairs (specs_at_scale)
387+ if ! isnothing (active_processes_by_scale)
388+ active = get (active_processes_by_scale, scale, Set {Symbol} ())
389+ process in active || continue
390+ end
268391 scale == consumer_scale && process == consumer_process && continue
269392 input_var in keys (outputs_ (model_ (spec))) || continue
270393 _is_stream_only_output (spec, input_var) && continue
@@ -283,8 +406,22 @@ function _input_candidates_for_var(model_specs, consumer_scale::String, consumer
283406 return same_scale, cross_scale
284407end
285408
286- function _infer_input_binding_for_var (model_specs, scale:: String , process:: Symbol , input_var:: Symbol ; scale_reachability= nothing )
287- same_scale, cross_scale = _input_candidates_for_var (model_specs, scale, process, input_var; scale_reachability= scale_reachability)
409+ function _infer_input_binding_for_var (
410+ model_specs,
411+ scale:: String ,
412+ process:: Symbol ,
413+ input_var:: Symbol ;
414+ scale_reachability= nothing ,
415+ active_processes_by_scale= nothing
416+ )
417+ same_scale, cross_scale = _input_candidates_for_var (
418+ model_specs,
419+ scale,
420+ process,
421+ input_var;
422+ scale_reachability= scale_reachability,
423+ active_processes_by_scale= active_processes_by_scale
424+ )
288425
289426 if length (same_scale) == 1
290427 c = only (same_scale)
@@ -329,7 +466,7 @@ function _infer_input_binding_for_var(model_specs, scale::String, process::Symbo
329466 return nothing
330467end
331468
332- function _infer_input_bindings! (model_specs; scale_reachability= nothing )
469+ function _infer_input_bindings! (model_specs; scale_reachability= nothing , active_processes_by_scale = nothing )
333470 for (scale, specs_at_scale) in pairs (model_specs)
334471 # When a scale is absent from the initial MTG, input producer inference at
335472 # init time is unreliable (dynamic growth may introduce it later). Keep
@@ -338,6 +475,10 @@ function _infer_input_bindings!(model_specs; scale_reachability=nothing)
338475 continue
339476 end
340477 for (process, spec) in pairs (specs_at_scale)
478+ if ! isnothing (active_processes_by_scale)
479+ active = get (active_processes_by_scale, scale, Set {Symbol} ())
480+ process in active || continue
481+ end
341482 current_bindings = input_bindings (spec)
342483 current_bindings isa NamedTuple || continue
343484
@@ -346,7 +487,14 @@ function _infer_input_bindings!(model_specs; scale_reachability=nothing)
346487
347488 for input_var in model_inputs
348489 input_var in keys (current_bindings) && continue
349- inferred_binding = _infer_input_binding_for_var (model_specs, scale, process, input_var; scale_reachability= scale_reachability)
490+ inferred_binding = _infer_input_binding_for_var (
491+ model_specs,
492+ scale,
493+ process,
494+ input_var;
495+ scale_reachability= scale_reachability,
496+ active_processes_by_scale= active_processes_by_scale
497+ )
350498 isnothing (inferred_binding) && continue
351499 push! (inferred, input_var => inferred_binding)
352500 end
@@ -409,8 +557,12 @@ Fill missing `ModelSpec` fields from inference:
409557- model-level hint traits (`timestep_hint`, `meteo_hint`)
410558Explicit `ModelSpec` user values always take precedence over inferred values.
411559"""
412- function infer_model_specs_configuration! (model_specs; scale_reachability= nothing )
413- _infer_input_bindings! (model_specs; scale_reachability= scale_reachability)
560+ function infer_model_specs_configuration! (model_specs; scale_reachability= nothing , active_processes_by_scale= nothing )
561+ _infer_input_bindings! (
562+ model_specs;
563+ scale_reachability= scale_reachability,
564+ active_processes_by_scale= active_processes_by_scale
565+ )
414566 _infer_timestep_hints! (model_specs)
415567 _infer_meteo_hints! (model_specs)
416568 return model_specs
0 commit comments