Skip to content

Commit c52a541

Browse files
committed
Add helpers for debugging model specs and meteo specs
1 parent cdc5571 commit c52a541

6 files changed

Lines changed: 122 additions & 0 deletions

File tree

docs/src/API/API_public.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ For mapping-level multi-rate configuration, combine:
2626
- `ScopeModel(...)`
2727
- `timestep_hint(::Type{<:AbstractModel})` (optional trait)
2828
- `meteo_hint(::Type{<:AbstractModel})` (optional trait)
29+
- `resolved_model_specs(mapping)` (utility)
30+
- `explain_model_specs(mapping_or_sim)` (utility)
2931
- `OutputRequest(...)` in `tracked_outputs` for resampled exports
3032

3133
`TimeStepModel(...)` accepts:

docs/src/model_execution.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ For meteo hints:
3636
and `window` matches `MeteoWindow(...)`.
3737
- Explicit `MeteoBindings(...)` / `MeteoWindow(...)` always take precedence.
3838

39+
Inspection helpers:
40+
- `resolved_model_specs(mapping)` returns resolved specs after inference/validation.
41+
- `explain_model_specs(mapping_or_sim)` prints a compact summary (`timestep`,
42+
`meteo_bindings`, `meteo_window`) for each model process.
43+
3944
Policy parameterization:
4045
- `Integrate()` defaults to `SumReducer()`; you can pass another reducer, e.g. `Integrate(MeanReducer())` or `Integrate(vals -> maximum(vals) - minimum(vals))`.
4146
- `Aggregate()` defaults to `MeanReducer()`; you can pass reducers such as `Aggregate(MaxReducer())`.

src/PlantSimEngine.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export OutputCache, HoldLastCache, InterpolateCache, IntegrateCache, AggregateCa
125125
export TemporalState
126126
export OutputRequest, collect_outputs
127127
export ModelList, MultiScaleModel, ModelSpec, TimeStepModel, InputBindings, MeteoBindings, MeteoWindow, OutputRouting, ScopeModel
128+
export resolved_model_specs, explain_model_specs
128129
export RMSE, NRMSE, EF, dr
129130
export Status, TimeStepTable, status
130131
export init_status!

src/mtg/model_spec_inference.jl

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,98 @@ function infer_model_specs_configuration!(model_specs)
269269
_infer_meteo_hints!(model_specs)
270270
return model_specs
271271
end
272+
273+
"""
274+
resolved_model_specs(mapping; infer=true, validate=true)
275+
resolved_model_specs(sim::GraphSimulation)
276+
277+
Return process-indexed `ModelSpec` dictionaries as used by runtime:
278+
`Dict{String, Dict{Symbol, ModelSpec}}`.
279+
280+
For a mapping, this parses model declarations and optionally applies inference
281+
(`timestep_hint`, `meteo_hint`) and validation.
282+
For a `GraphSimulation`, this returns the already resolved model specs used by the simulation.
283+
"""
284+
function resolved_model_specs(mapping::AbstractDict; infer::Bool=true, validate::Bool=true)
285+
model_specs = Dict{String,Dict{Symbol,ModelSpec}}()
286+
for (scale, declarations) in pairs(mapping)
287+
model_specs[string(scale)] = parse_model_specs(declarations)
288+
end
289+
290+
infer && infer_model_specs_configuration!(model_specs)
291+
validate && validate_model_specs_configuration(model_specs)
292+
return model_specs
293+
end
294+
295+
resolved_model_specs(sim::GraphSimulation; infer::Bool=true, validate::Bool=true) = get_model_specs(sim)
296+
297+
function _stringify_compact(x; maxlen::Int=120)
298+
s = sprint(show, x)
299+
return ncodeunits(s) <= maxlen ? s : string(first(s, maxlen - 3), "...")
300+
end
301+
302+
function _model_specs_rows(model_specs)
303+
rows = NamedTuple[]
304+
for scale in sort!(collect(keys(model_specs)))
305+
specs_at_scale = model_specs[scale]
306+
for process in sort!(collect(keys(specs_at_scale)); by=string)
307+
spec = specs_at_scale[process]
308+
push!(rows, (
309+
scale=scale,
310+
process=process,
311+
model=typeof(model_(spec)),
312+
timestep=timestep(spec),
313+
meteo_bindings=meteo_bindings(spec),
314+
meteo_window=meteo_window(spec),
315+
))
316+
end
317+
end
318+
return rows
319+
end
320+
321+
"""
322+
explain_model_specs(target; io=stdout, infer=true, validate=true)
323+
324+
Print a compact per-model summary of resolved runtime configuration and return it
325+
as a vector of named tuples.
326+
327+
Summary fields:
328+
- `scale`
329+
- `process`
330+
- `model`
331+
- `timestep`
332+
- `meteo_bindings`
333+
- `meteo_window`
334+
"""
335+
function explain_model_specs(target; io::IO=stdout, infer::Bool=true, validate::Bool=true)
336+
specs = target isa GraphSimulation ? resolved_model_specs(target) : resolved_model_specs(target; infer=infer, validate=validate)
337+
rows = _model_specs_rows(specs)
338+
339+
println(io, "Resolved model specs:")
340+
if isempty(rows)
341+
println(io, " (no model specs)")
342+
return rows
343+
end
344+
345+
for row in rows
346+
timestep_desc = isnothing(row.timestep) ? "(timespec(model))" : _stringify_compact(row.timestep)
347+
meteo_bindings_desc = (row.meteo_bindings isa NamedTuple && isempty(keys(row.meteo_bindings))) ? "(none)" : _stringify_compact(row.meteo_bindings)
348+
meteo_window_desc = isnothing(row.meteo_window) ? "(default rolling)" : _stringify_compact(row.meteo_window)
349+
println(
350+
io,
351+
" - ",
352+
row.scale,
353+
"/",
354+
row.process,
355+
" [",
356+
row.model,
357+
"]: timestep=",
358+
timestep_desc,
359+
", meteo_bindings=",
360+
meteo_bindings_desc,
361+
", meteo_window=",
362+
meteo_window_desc
363+
)
364+
end
365+
return rows
366+
end

test/test-multirate-runtime.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,12 @@ PlantSimEngine.meteo_hint(::Type{<:MRMeteoHintConsumerModel}) = (
599599
@test status(sim_timestep_hints)["Leaf"][1].XB == 3.0
600600
@test status(sim_timestep_hints)["Leaf"][1].XF == 4.0
601601

602+
io_hints = IOBuffer()
603+
explained_hints = PlantSimEngine.explain_model_specs(sim_timestep_hints; io=io_hints)
604+
explain_hints_txt = String(take!(io_hints))
605+
@test any(r -> r.process == :mrrangehinta && Dates.value(Dates.Second(r.timestep)) == 10800, explained_hints)
606+
@test occursin("Leaf/mrrangehinta", explain_hints_txt)
607+
602608
if _HAS_METEO_SAMPLER_API
603609
# Expectation 18: meteo is sampled at model clock using default weather aggregation.
604610
mapping_meteo_default = Dict(

test/test-multirate-scaffolding.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ using Test
1616
@test output_routing(ModelSpec(m)) == NamedTuple()
1717
@test model_scope(ModelSpec(m)) == :global
1818

19+
mapping = Dict("Leaf" => (m,))
20+
resolved_specs = resolved_model_specs(mapping)
21+
@test haskey(resolved_specs, "Leaf")
22+
@test haskey(resolved_specs["Leaf"], :process1)
23+
24+
io = IOBuffer()
25+
explained = explain_model_specs(mapping; io=io)
26+
explain_txt = String(take!(io))
27+
@test length(explained) == 1
28+
@test explained[1].process == :process1
29+
@test occursin("Resolved model specs:", explain_txt)
30+
@test occursin("Leaf/process1", explain_txt)
31+
1932
spec = ModelSpec(m) |>
2033
TimeStepModel(24.0) |>
2134
InputBindings(; var1=(process=:process1, var=:var3)) |>

0 commit comments

Comments
 (0)