Skip to content

Component variants

Most components in Trano (radiators, spaces, controls, air handling units, boilers, valves, ...) ship with several pre-defined implementations called variants. A variant is a concrete Modelica realisation of a component class — for example the Radiator class has a default variant (RadiatorEN442_2, a water radiator), an ideal variant (an idealised heat injection block driven by its control) and an idealbus variant (the same, driven through the data bus).

This tutorial explains:

  1. What variants are and where they live in the codebase.
  2. How to use an existing variant from your YAML model.
  3. How to create a new variant (built-in or in your own folder).
  4. How to register an external variants folder so Trano picks up your variants automatically.
  5. How variants interact with model validation.
  6. How to test custom variants.

1. What is a variant?

A variant describes how an instance of a Trano element class (e.g. Radiator, Space, EmissionControl, AirHandlingUnit, ...) is rendered to Modelica code.

Each variant is a YAML entry that contains:

Field Purpose
classes The Trano element class(es) this variant applies to (e.g. Radiator).
variant The string name used to select this variant from a YAML model.
library The library (or list of libraries) this variant targets (default, ideas, buildings, FastSmoothBuildings, ...).
template The Jinja2 template rendered into the generated Modelica model.
ports Connection ports the component exposes (fluid, heat, data, weather).
parameter_processing How the user-provided parameters are mapped to the template.
figures Plots auto-generated for the reporting view (optional).
component_template An optional inner Modelica model wrapping the component (optional).

The built-in variants live under trano/elements/library/models/<library>/<component>.yaml. Each YAML file contains a list of variant definitions for one or several classes. A definition whose library is default serves every library that has no definition of its own for that class and variant.

Examples of variants currently shipped

The table below summarises the variants that already exist for the most common components, so you can pick the right one for your model. The full list can always be discovered with:

from trano.elements.library.components import get_components_registry

for c in get_components_registry().components:
    print(c["classes"], c["variant"], c.get("library"))

Radiator (Radiator)

Variant Use case
default Detailed Buildings.Fluid.HeatExchangers.Radiators.RadiatorEN442_2 water radiator with hydronic ports, followed by a valve.
ideal Idealised heat emission driven by its emission control — no fluid loop required.
idealbus Idealised heat emission driven by its control through the data bus.

Space (Space)

Variant Use case
default The library's zone model (MixedAir for Buildings, IDEAS...Zone for IDEAS, 5R1C for iso_13790, RC for reduced_order).
infiltration Same zone, configured for infiltration.
rectangular_zone IDEAS RectangularZoneTemplate (IDEAS only).

Air Handling Unit (AirHandlingUnit)

Variant Use case
default Detailed AHU partial model with VAV boxes, dampers and fans (needs an ahu_control).
system_d System-D mechanical balanced ventilation with heat recovery.
test Simplified HVAC model — no VAV boxes.

Emission Control (EmissionControl)

Variant Use case
default ASHRAE G36 thermal zone control loops with heating/cooling outputs.
onoff Simple on/off thermostat with occupancy schedule.
PController Proportional (P) controller.
MonitoredSetpoint P controller whose setpoint is read from a data source (e.g. measured boundary data).

Boiler (Boiler)

Variant Use case
default Gas boiler with a thermal storage tank.
without_storage Gas boiler without storage (direct loop).
water_water_heat_pump Water/water heat pump as production system.
air_water_heat_pump Air/water heat pump as production system.

Boiler Control (BoilerControl)

Variant Use case
default Default boiler control loop.
complex Detailed boiler control with cascade logic.
simple Simple on/off boiler control.

VAV box (VAV)

Variant Use case
default Pressure-independent damper as a single block (takes a vav_control of variant constant).
complex Full VAV box partial model with reheat coil, dampers, sensors (takes a vav_control of variant default).

VAV Control (VAVControl)

Variant Use case
default ASHRAE G36 reheat-box terminal-unit controller.
constant Simple constant-flow VAV controller.

Occupancy (Occupancy)

Variant Use case
default Schedule-based occupancy block.
co2 Occupancy with CO2 generation.

Floor on ground (FloorOnGround)

Variant Use case
default Floor on the ground.
outdoor_air Floor exposed to outdoor air.

2. Selecting a variant from a YAML model

Use the variant: field of the component you are configuring. If a component does not declare a variant, Trano falls back to the variant called default.

spaces:
  - id: SPACE:001
    parameters:
      floor_area: 80.0
      average_room_height: 2.5
    external_boundaries: ...
    emissions:
      - radiator:
          id: RADIATOR:001
          # Use the *ideal* radiator instead of the default detailed one
          variant: ideal
          parameters:
            nominal_heating_power_positive_for_heating: 2500
          control:
            emission_control:
              id: EMISSION_CONTROL:001
              # Use the on/off emission control variant
              variant: onoff
              parameters:
                schedule: 3600*{10, 20}
                temperature_heating_setpoint: 295.15
                temperature_heating_setback: 291.15

The variant key works the same way for any component in the YAML schema: spaces[i].variant, boiler.variant, air_handling_unit.variant, vav.variant, occupancy.variant, etc.

3. Creating a new variant

Creating a new variant is a small, focused YAML edit. There are two equally valid options:

Option When to use
A — built-in variant The variant should ship with Trano and be available to everyone.
B — external variants folder The variant is project- or vendor-specific; lives in your repo.

In both cases the YAML schema is identical.

Step-by-step

  1. Pick the class you want to extend (e.g. Radiator).
  2. Pick a unique variant name for that class. The name + library combo must be unique. The string is what users will write under variant: in their YAML model.
  3. Write the Modelica template as a Jinja2 string. Trano renders it with the element, package_name, library_name and parameters variables (see the existing templates under trano/elements/library/models/ for reference).
  4. Declare the ports — for hydronic components typically port_a / port_b fluid ports, plus optional heat ports (heatPortCon, heatPortRad) and data ports for control buses. The declared ports are also how Trano classifies the component when validating the model: a radiator variant with fluid ports is a water radiator (and must be followed by a valve); one without fluid ports needs no hydronic loop.
  5. Choose a parameter_processing function — default_parameters, modify_alias, exclude_parameters (see trano/elements/library/parameters.py).
  6. Save the file.

Below is the YAML you need to add a custom radiator that injects a fixed heat flow, driven by its control (same shape as the built-in ideal variant):

- classes:
  - Radiator
  variant: my_constant_emission
  library: default
  parameter_processing:
    function: default_parameters
  ports:
    - flow: convective
      medium: heat
      names: [heatPortCon]
      targets: [Space]
    - flow: radiative
      medium: heat
      names: [heatPortRad]
      targets: [Space]
    - flow: undirected
      medium: data
      names: [y]
      targets: [Control]
  template: |2-

      MyCompany.Trano.Emission.ConstantHeatRadiator
      {{ element.name }}(
        {{ macros.render_parameters(parameters) | safe }})
        "Constant-power radiator"
  figures: []

Option A — Add the variant to Trano's built-in catalogue

Drop the YAML snippet above (as a list) into the right file under trano/elements/library/models/<library>/. For a radiator targeting every library, append it to trano/elements/library/models/default/radiator.yaml.

Built-in variants are also validated against the schema enums in trano/data_models/trano.yaml (e.g. RadiatorVariant), so a new built-in variant must be added there (and in trano_final.yaml) as well.

Option B — Keep the variant in an external folder

If your variant should not live inside Trano (proprietary code, project specific, internal Modelica library...), place it in any folder of your choice and tell Trano about it.

my_project/
├── modelica/
│   └── MyCompany/...
└── trano_variants/             ← any path, anywhere
    └── radiator_constant.yaml  ← contains the YAML list shown above

There are three equivalent ways to register the folder:

From the Python API

from trano.elements.library.components import register_variants_folder
from trano.main import create_model

register_variants_folder("my_project/trano_variants")
create_model("my_project/model.yaml")

register_variants_folder accepts str and pathlib.Path. The folder is scanned recursively for *.yaml files, so you can organise variants into sub-folders.

From the CLI

trano create-model my_project/model.yaml --variants-folder my_project/trano_variants
trano simulate-model my_project/model.yaml --variants-folder my_project/trano_variants
trano compile-model my_project/model.yaml --variants-folder my_project/trano_variants

The --variants-folder option (-V) can be passed multiple times to register several folders.

From an environment variable

export TRANO_VARIANTS_PATH="$HOME/my_project/trano_variants:$HOME/shared_variants"

Trano reads TRANO_VARIANTS_PATH whenever the component registry is loaded. The value is an os.pathsep-separated list of folders (: on Linux/macOS, ; on Windows).

Using the new variant

Once registered, your variant is just like any built-in one — reference it by name:

emissions:
  - radiator:
      id: RADIATOR:001
      variant: my_constant_emission
      parameters:
        nominal_heating_power_positive_for_heating: 1500
      control:
        emission_control:
          id: EMISSION_CONTROL:001

4. Variants and model validation

Trano validates every model before generating it: against the schema (unknown fields, enums, bounds) and against topology rules (water loops, valves, controls). Externally registered variants take part in both:

  • The schema's variant enums only know the built-in variants; a variant: value that names a registered external variant of that component is accepted.
  • A radiator variant is classified by its declared ports: with fluid ports it is a water radiator and the usual rule applies (it must be followed by a valve in the emissions of its space); without fluid ports it is treated like the ideal variants and needs no hydronic loop.

An unknown variant (not built-in, not registered) is still rejected with the usual one-line validation error.

5. Programmatic listing and inspection

from trano.elements.library.components import (
    get_components_registry,
    register_variants_folder,
    registered_variants_folders,
    clear_variants_folders,
)

register_variants_folder("./trano_variants")

# What folders are currently registered?
print(registered_variants_folders())

# All variants known to Trano (built-in + external)
for c in get_components_registry().components:
    if "Radiator" in c["classes"]:
        print(c["library"], c["variant"])

# Reset to the built-in catalogue (useful in tests)
clear_variants_folders()

6. Testing your custom variant

A custom variant is just YAML, but it is still good practice to test it end-to-end. The recommended pattern:

from pathlib import Path

import pytest

from trano.elements.library.components import (
    clear_variants_folders,
    register_variants_folder,
)
from trano.elements.system import Radiator


@pytest.fixture(autouse=True)
def reset_variants():
    clear_variants_folders()
    yield
    clear_variants_folders()


def test_my_variant_is_registered(tmp_path: Path):
    register_variants_folder(Path(__file__).parent / "trano_variants")
    radiator = Radiator(name="r1", variant="my_constant_emission")
    library_data = next(ld for ld in radiator.libraries_data
                         if ld.variant == "my_constant_emission")
    assert "ConstantHeatRadiator" in library_data.template

For an end-to-end test that builds a network and a Modelica model from a YAML file, see tests/test_variants.py::test_create_model_yaml_using_custom_variant.

7. Cheat sheet

I want to ... Do this
Use an existing variant Set variant: <name> on the component in your YAML model.
Add a new variant for everyone Edit trano/elements/library/models/<library>/<component>.yaml and the variant enums in trano/data_models/trano.yaml / trano_final.yaml.
Add a new variant only for my project Put YAML files in a folder, then register_variants_folder(folder).
Register variants without changing my Python code export TRANO_VARIANTS_PATH=/path/to/folder.
Register variants from the CLI trano create-model model.yaml --variants-folder /path/to/folder (also on simulate-model and compile-model).
List registered external folders registered_variants_folders().
Reset to built-in only clear_variants_folders().

That's it — variants are the central extension point of Trano. Any new modelling capability (a new emission technology, a new control law, a new zone model from your in-house Modelica library...) is just a new YAML file.