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.

src/tutorial/structured_objects.py
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.