@@ -368,6 +368,112 @@ function _default_policy_for_inferred_binding(model_specs, source_scale::Symbol,
368368 )
369369end
370370
371+ function _mapped_source_scales_for_input (spec:: ModelSpec , input_var:: Symbol )
372+ mapped = mapped_variables_ (spec)
373+ isempty (mapped) && return Set {Symbol} ()
374+
375+ scales = Set {Symbol} ()
376+ for mv in mapped
377+ mapped_input = first (mv)
378+ mapped_input = mapped_input isa PreviousTimeStep ? mapped_input. variable : mapped_input
379+ mapped_input == input_var || continue
380+
381+ rhs = last (mv)
382+ if rhs isa Pair{Symbol,Symbol}
383+ src_scale = first (rhs)
384+ src_scale == Symbol (" " ) || push! (scales, src_scale)
385+ elseif rhs isa AbstractVector
386+ for item in rhs
387+ item isa Pair{Symbol,Symbol} || continue
388+ src_scale = first (item)
389+ src_scale == Symbol (" " ) || push! (scales, src_scale)
390+ end
391+ end
392+ end
393+
394+ return scales
395+ end
396+
397+ function _input_has_multiscale_mapping (spec:: ModelSpec , input_var:: Symbol )
398+ mapped = mapped_variables_ (spec)
399+ isempty (mapped) && return false
400+
401+ for mv in mapped
402+ mapped_input = first (mv)
403+ mapped_input = mapped_input isa PreviousTimeStep ? mapped_input. variable : mapped_input
404+ mapped_input == input_var && return true
405+ end
406+
407+ return false
408+ end
409+
410+ function _mapped_sources_for_input (spec:: ModelSpec , input_var:: Symbol )
411+ mapped = mapped_variables_ (spec)
412+ isempty (mapped) && return Pair{Symbol,Symbol}[]
413+
414+ sources = Pair{Symbol,Symbol}[]
415+ for mv in mapped
416+ mapped_input = first (mv)
417+ mapped_input = mapped_input isa PreviousTimeStep ? mapped_input. variable : mapped_input
418+ mapped_input == input_var || continue
419+
420+ rhs = last (mv)
421+ if rhs isa Pair{Symbol,Symbol}
422+ push! (sources, rhs)
423+ elseif rhs isa AbstractVector
424+ for item in rhs
425+ item isa Pair{Symbol,Symbol} || continue
426+ push! (sources, item)
427+ end
428+ end
429+ end
430+
431+ return sources
432+ end
433+
434+ function _infer_binding_from_multiscale_mapping (
435+ model_specs,
436+ scale:: Symbol ,
437+ process:: Symbol ,
438+ spec:: ModelSpec ,
439+ input_var:: Symbol ;
440+ active_processes_by_scale= nothing
441+ )
442+ has_mapping = _input_has_multiscale_mapping (spec, input_var)
443+ has_mapping || return nothing
444+
445+ mapped_sources = _mapped_sources_for_input (spec, input_var)
446+ # Mapping exists but does not point to another scale (self/same-scale aliasing):
447+ # avoid generic same-name inference in that case.
448+ filtered_sources = filter (s -> first (s) != Symbol (" " ), mapped_sources)
449+ isempty (filtered_sources) && return :skip
450+
451+ # Multi-source mapping (e.g. vectors from several scales) cannot be represented
452+ # as one `InputBindings` entry; keep binding unresolved and skip generic inference.
453+ length (filtered_sources) == 1 || return :skip
454+
455+ src = only (filtered_sources)
456+ src_scale = first (src)
457+ src_var = last (src)
458+ haskey (model_specs, src_scale) || return :skip
459+
460+ procs = Symbol[]
461+ for (src_process, src_spec) in pairs (model_specs[src_scale])
462+ if ! isnothing (active_processes_by_scale)
463+ active = get (active_processes_by_scale, src_scale, Set {Symbol} ())
464+ src_process in active || continue
465+ end
466+ src_var in keys (outputs_ (model_ (src_spec))) || continue
467+ _is_stream_only_output (src_spec, src_var) && continue
468+ push! (procs, src_process)
469+ end
470+
471+ length (procs) == 1 || return :skip
472+ src_process = only (procs)
473+ policy = _default_policy_for_inferred_binding (model_specs, src_scale, src_process, src_var)
474+ return (process= src_process, var= src_var, scale= src_scale, policy= policy)
475+ end
476+
371477function _infer_input_binding_for_var (
372478 model_specs,
373479 scale:: Symbol ,
@@ -388,7 +494,7 @@ function _infer_input_binding_for_var(
388494 if length (same_scale) == 1
389495 c = only (same_scale)
390496 policy = _default_policy_for_inferred_binding (model_specs, c. scale, c. process, c. var)
391- return (process= c. process, var= c. var, policy= policy)
497+ return (process= c. process, var= c. var, scale = c . scale, policy= policy)
392498 elseif length (same_scale) > 1
393499 error (
394500 " Ambiguous inferred producer for input `$(input_var) ` in process `$(process) ` at scale `$(scale) `. " ,
@@ -415,9 +521,25 @@ function _infer_input_binding_for_var(
415521 policy = _default_policy_for_inferred_binding (model_specs, src_scale, proc, input_var)
416522 return (process= proc, var= input_var, scale= src_scale, policy= policy)
417523 end
418- # Same process name appears at multiple scales (common in multiscale
419- # mappings). Keep scale unresolved so runtime resolves through parent links.
420- return (process= proc, var= input_var, policy= HoldLast ())
524+
525+ # When multiscale mapping already declares a source scale for this
526+ # input, use it to disambiguate instead of forcing explicit bindings.
527+ consumer_spec = model_specs[scale][process]
528+ mapped_scales = _mapped_source_scales_for_input (consumer_spec, input_var)
529+ candidate_scales = Set (scales)
530+ hinted_scales = intersect (mapped_scales, candidate_scales)
531+ if length (hinted_scales) == 1
532+ src_scale = only (hinted_scales)
533+ policy = _default_policy_for_inferred_binding (model_specs, src_scale, proc, input_var)
534+ return (process= proc, var= input_var, scale= src_scale, policy= policy)
535+ end
536+
537+ error (
538+ " Ambiguous inferred producer for input `$(input_var) ` in process `$(process) ` at scale `$(scale) `. " ,
539+ " Process `$(proc) ` publishes this variable at multiple reachable scales: $(join (scales, " , " )) . " ,
540+ " Please provide explicit `InputBindings(...)` with `scale`, " ,
541+ " or add a `MultiScaleModel(...)` mapping so the source scale is unambiguous."
542+ )
421543 end
422544
423545 error (
@@ -453,6 +575,20 @@ function _infer_input_bindings!(model_specs; scale_reachability=nothing, active_
453575
454576 for input_var in model_inputs
455577 input_var in keys (current_bindings) && continue
578+ mapped_binding = _infer_binding_from_multiscale_mapping (
579+ model_specs,
580+ scale,
581+ process,
582+ spec,
583+ input_var;
584+ active_processes_by_scale= active_processes_by_scale
585+ )
586+ if mapped_binding === :skip
587+ continue
588+ elseif ! isnothing (mapped_binding)
589+ push! (inferred, input_var => mapped_binding)
590+ continue
591+ end
456592 inferred_binding = _infer_input_binding_for_var (
457593 model_specs,
458594 scale,
0 commit comments