|
| 1 | +const _INPUT_BINDING_FIELDS = (:process, :var, :scale, :policy) |
| 2 | + |
| 3 | +function _validate_timestep_spec(scale::String, process::Symbol, spec::ModelSpec) |
| 4 | + ts = timestep(spec) |
| 5 | + isnothing(ts) && return nothing |
| 6 | + |
| 7 | + if ts isa ClockSpec |
| 8 | + float(ts.dt) > 0 || error( |
| 9 | + "Invalid timestep for process `$(process)` at scale `$(scale)`: ", |
| 10 | + "`ClockSpec.dt` must be > 0, got $(ts.dt)." |
| 11 | + ) |
| 12 | + return nothing |
| 13 | + end |
| 14 | + |
| 15 | + if ts isa Real |
| 16 | + float(ts) > 0 || error( |
| 17 | + "Invalid timestep for process `$(process)` at scale `$(scale)`: ", |
| 18 | + "numeric timestep must be > 0, got $(ts)." |
| 19 | + ) |
| 20 | + return nothing |
| 21 | + end |
| 22 | + |
| 23 | + error( |
| 24 | + "Invalid timestep for process `$(process)` at scale `$(scale)`: ", |
| 25 | + "expected `Real` or `ClockSpec`, got `$(typeof(ts))`." |
| 26 | + ) |
| 27 | +end |
| 28 | + |
| 29 | +function _validate_binding_policy(scale::String, process::Symbol, input_var::Symbol, policy) |
| 30 | + if policy isa DataType |
| 31 | + policy <: SchedulePolicy || error( |
| 32 | + "Invalid policy for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 33 | + "expected a `SchedulePolicy` type or instance, got `$(policy)`." |
| 34 | + ) |
| 35 | + return nothing |
| 36 | + end |
| 37 | + |
| 38 | + policy isa SchedulePolicy || error( |
| 39 | + "Invalid policy for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 40 | + "expected a `SchedulePolicy` type or instance, got `$(typeof(policy))`." |
| 41 | + ) |
| 42 | + |
| 43 | + return nothing |
| 44 | +end |
| 45 | + |
| 46 | +function _validate_binding_target( |
| 47 | + scale::String, |
| 48 | + process::Symbol, |
| 49 | + input_var::Symbol, |
| 50 | + source_process::Symbol, |
| 51 | + source_scale, |
| 52 | + model_specs, |
| 53 | + known_processes::Set{Symbol} |
| 54 | +) |
| 55 | + source_process in known_processes || error( |
| 56 | + "Unknown source process `$(source_process)` for input `$(input_var)` in process `$(process)` at scale `$(scale)`." |
| 57 | + ) |
| 58 | + |
| 59 | + isnothing(source_scale) && return nothing |
| 60 | + src_scale = string(source_scale) |
| 61 | + haskey(model_specs, src_scale) || error( |
| 62 | + "Unknown source scale `$(src_scale)` for input `$(input_var)` in process `$(process)` at scale `$(scale)`." |
| 63 | + ) |
| 64 | + source_process in keys(model_specs[src_scale]) || error( |
| 65 | + "Source process `$(source_process)` for input `$(input_var)` in process `$(process)` ", |
| 66 | + "is not declared at scale `$(src_scale)`." |
| 67 | + ) |
| 68 | + return nothing |
| 69 | +end |
| 70 | + |
| 71 | +function _validate_input_binding( |
| 72 | + scale::String, |
| 73 | + process::Symbol, |
| 74 | + input_var::Symbol, |
| 75 | + binding, |
| 76 | + model_specs, |
| 77 | + known_processes::Set{Symbol} |
| 78 | +) |
| 79 | + source_process = nothing |
| 80 | + source_scale = nothing |
| 81 | + policy = HoldLast() |
| 82 | + |
| 83 | + if binding isa Symbol |
| 84 | + source_process = binding |
| 85 | + elseif binding isa Pair{Symbol,Symbol} |
| 86 | + source_process = first(binding) |
| 87 | + elseif binding isa NamedTuple |
| 88 | + extra = setdiff(collect(keys(binding)), collect(_INPUT_BINDING_FIELDS)) |
| 89 | + isempty(extra) || error( |
| 90 | + "Invalid input binding for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 91 | + "unsupported fields $(extra)." |
| 92 | + ) |
| 93 | + haskey(binding, :process) || error( |
| 94 | + "Invalid input binding for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 95 | + "field `process` is required." |
| 96 | + ) |
| 97 | + binding.process isa Symbol || error( |
| 98 | + "Invalid input binding for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 99 | + "`process` must be a Symbol, got `$(typeof(binding.process))`." |
| 100 | + ) |
| 101 | + source_process = binding.process |
| 102 | + |
| 103 | + if haskey(binding, :var) |
| 104 | + isnothing(binding.var) || binding.var isa Symbol || error( |
| 105 | + "Invalid input binding for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 106 | + "`var` must be a Symbol or `nothing`, got `$(typeof(binding.var))`." |
| 107 | + ) |
| 108 | + end |
| 109 | + |
| 110 | + if haskey(binding, :scale) |
| 111 | + isnothing(binding.scale) || binding.scale isa Symbol || binding.scale isa AbstractString || error( |
| 112 | + "Invalid input binding for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 113 | + "`scale` must be a Symbol, String or `nothing`, got `$(typeof(binding.scale))`." |
| 114 | + ) |
| 115 | + source_scale = binding.scale |
| 116 | + end |
| 117 | + |
| 118 | + policy = haskey(binding, :policy) ? binding.policy : HoldLast() |
| 119 | + else |
| 120 | + error( |
| 121 | + "Invalid input binding for input `$(input_var)` in process `$(process)` at scale `$(scale)`: ", |
| 122 | + "unsupported binding type `$(typeof(binding))`." |
| 123 | + ) |
| 124 | + end |
| 125 | + |
| 126 | + _validate_binding_policy(scale, process, input_var, policy) |
| 127 | + _validate_binding_target(scale, process, input_var, source_process, source_scale, model_specs, known_processes) |
| 128 | + return nothing |
| 129 | +end |
| 130 | + |
| 131 | +function _validate_input_bindings_for_spec( |
| 132 | + scale::String, |
| 133 | + process::Symbol, |
| 134 | + spec::ModelSpec, |
| 135 | + model_specs, |
| 136 | + known_processes::Set{Symbol} |
| 137 | +) |
| 138 | + bindings = input_bindings(spec) |
| 139 | + bindings isa NamedTuple || error( |
| 140 | + "InputBindings for process `$(process)` at scale `$(scale)` must be a NamedTuple, got `$(typeof(bindings))`." |
| 141 | + ) |
| 142 | + |
| 143 | + model_inputs = Set(keys(inputs_(model_(spec)))) |
| 144 | + for (input_var, binding) in pairs(bindings) |
| 145 | + input_var isa Symbol || error( |
| 146 | + "InputBindings key for process `$(process)` at scale `$(scale)` must be a Symbol, got `$(typeof(input_var))`." |
| 147 | + ) |
| 148 | + input_var in model_inputs || error( |
| 149 | + "InputBindings for process `$(process)` at scale `$(scale)` declares binding for input `$(input_var)`, ", |
| 150 | + "but model inputs are $(collect(model_inputs))." |
| 151 | + ) |
| 152 | + _validate_input_binding(scale, process, input_var, binding, model_specs, known_processes) |
| 153 | + end |
| 154 | + return nothing |
| 155 | +end |
| 156 | + |
| 157 | +function _validate_output_routing_for_spec(scale::String, process::Symbol, spec::ModelSpec) |
| 158 | + routing = output_routing(spec) |
| 159 | + routing isa NamedTuple || error( |
| 160 | + "OutputRouting for process `$(process)` at scale `$(scale)` must be a NamedTuple, got `$(typeof(routing))`." |
| 161 | + ) |
| 162 | + |
| 163 | + model_outputs = Set(keys(outputs_(model_(spec)))) |
| 164 | + for (out_var, mode) in pairs(routing) |
| 165 | + out_var isa Symbol || error( |
| 166 | + "OutputRouting key for process `$(process)` at scale `$(scale)` must be a Symbol, got `$(typeof(out_var))`." |
| 167 | + ) |
| 168 | + out_var in model_outputs || error( |
| 169 | + "OutputRouting for process `$(process)` at scale `$(scale)` declares routing for output `$(out_var)`, ", |
| 170 | + "but model outputs are $(collect(model_outputs))." |
| 171 | + ) |
| 172 | + |
| 173 | + mode_sym = mode isa Symbol ? mode : (mode isa AbstractString ? Symbol(mode) : nothing) |
| 174 | + isnothing(mode_sym) && error( |
| 175 | + "OutputRouting mode for output `$(out_var)` in process `$(process)` at scale `$(scale)` ", |
| 176 | + "must be `:canonical` or `:stream_only`." |
| 177 | + ) |
| 178 | + mode_sym in (:canonical, :stream_only) || error( |
| 179 | + "OutputRouting mode `$(mode_sym)` for output `$(out_var)` in process `$(process)` at scale `$(scale)` ", |
| 180 | + "is invalid. Allowed values: `:canonical`, `:stream_only`." |
| 181 | + ) |
| 182 | + end |
| 183 | + |
| 184 | + return nothing |
| 185 | +end |
| 186 | + |
| 187 | +""" |
| 188 | + validate_model_specs_configuration(model_specs) |
| 189 | +
|
| 190 | +Validate mapping-level `ModelSpec` configuration before simulation runtime starts. |
| 191 | +This catches invalid timestep declarations, input bindings and output routing early. |
| 192 | +""" |
| 193 | +function validate_model_specs_configuration(model_specs) |
| 194 | + known_processes = Set{Symbol}() |
| 195 | + for specs_at_scale in values(model_specs) |
| 196 | + union!(known_processes, keys(specs_at_scale)) |
| 197 | + end |
| 198 | + |
| 199 | + for (scale, specs_at_scale) in pairs(model_specs) |
| 200 | + for (process, spec) in pairs(specs_at_scale) |
| 201 | + _validate_timestep_spec(scale, process, spec) |
| 202 | + _validate_input_bindings_for_spec(scale, process, spec, model_specs, known_processes) |
| 203 | + _validate_output_routing_for_spec(scale, process, spec) |
| 204 | + end |
| 205 | + end |
| 206 | + |
| 207 | + return nothing |
| 208 | +end |
0 commit comments