5. Using STRUCTS¶
This tutorial is based on example code which can be found in the TRAC GitHub Repository under examples/models/python.
Not all data is naturally tabular. Structured data lets a model define an input or output as a Python dataclass (or Pydantic model), including nested objects, enums and dictionaries, rather than a flat table of fields.
Defining a struct schema¶
A struct schema is built from an ordinary Python dataclass. Structs can be nested inside one another and can contain enums, dictionaries and other common Python types.
16import enum
17
18import typing as _tp
19import dataclasses as _dc
20import datetime as _dt
21
22import tracdap.rt.api as trac
23
24
25class EvolutionModel(enum.Enum):
26 PERTURB = 1
27 SCATTER = 2
28 STOCHASTIC = 3
29
30@_dc.dataclass
31class ScenarioConfig:
32
33 scenario_name: str
34 default_weight: float
35 evolution_model: EvolutionModel
36 apply_smoothing: bool
37
38@_dc.dataclass
39class RunConfig:
40
41 include_front_book: bool
42 base_date: _dt.date
43
44 base_scenario: ScenarioConfig
45 stress_scenarios: dict[str, ScenarioConfig]
To turn a dataclass into a schema, use define_struct().
The runtime validates the supplied type and builds a matching
SchemaDefinition, which can be used with
define_input() in the usual way. For outputs, the
shorthand define_output_struct() combines both
steps.
56 def define_inputs(self) -> _tp.Dict[str, trac.ModelInputSchema]:
57
58 run_config_struct = trac.define_struct(RunConfig)
59 run_config = trac.define_input(run_config_struct, label="Run configuration")
60
61 return {"run_config": run_config}
62
63
64 def define_outputs(self) -> _tp.Dict[str, trac.ModelOutputSchema]:
65
66 modified_config = trac.define_output_struct(RunConfig, label="Modified config for next model stage")
67 return {"modified_config": modified_config}
Reading and writing structured data¶
Structured inputs and outputs are read and written with
get_struct() and
put_struct(), passing the dataclass type to use for
the result. The runtime returns (and validates) an instance of that type, so the rest of the model
code can work with ordinary Python objects and attribute access, rather than looking up fields by name.
69 def run_model(self, ctx: trac.TracContext):
70
71 run_config = ctx.get_struct("run_config", RunConfig)
72
73 new_scenario = ScenarioConfig(
74 scenario_name="hpi_shock",
75 default_weight=1.0,
76 evolution_model=EvolutionModel.STOCHASTIC,
77 apply_smoothing=True)
78
79 run_config.stress_scenarios["hpi_shock"] = new_scenario
80
81 ctx.put_struct("modified_config", run_config)
Note
The type passed to get_struct() does not have
to be the exact type used in define_inputs(),
so long as the schema is compatible the runtime will perform the conversion. In practice, models
should normally use the same type in both places, as this example does with RunConfig.
This model reads a run configuration struct, adds a new stress scenario to it, and saves the modified configuration as an output - which could then be picked up as the input to another model further down a flow.
See also
Full source code is available for the Structured Objects example on GitHub.