Concept: Systems

A System is the root of an executable model. It owns the Part tree, the Requirement tree, and the allocation wiring between them. There is exactly one System per configured model.


Anatomy of a System

from unitflow.catalogs.si import kg, m
from tg_model import Part, Requirement, System


class TankPart(Part):
    @classmethod
    def define(cls, model):
        model.name("tank_part")
        model.parameter("capacity_kg", unit=kg)
        model.parameter("loaded_mass_kg", unit=kg)


class MassReq(Requirement):
    @classmethod
    def define(cls, model):
        model.name("mass_req")
        model.doc("Loaded mass shall not exceed tank capacity.")
        capacity = model.parameter("capacity_kg", unit=kg)
        loaded   = model.parameter("loaded_mass_kg", unit=kg)
        margin   = model.attribute("margin_kg", unit=kg, expr=capacity - loaded)
        model.constraint("within_capacity", expr=margin >= 0 * kg)


class FuelSystem(System):
    @classmethod
    def define(cls, model):
        model.name("fuel_system")                          # required

        # 1. Compose Parts
        tank = model.composed_of("tank", TankPart)

        # 2. Compose Requirements
        reqs = model.composed_of("reqs", MassReq)

        # 3. Allocate: wire requirement parameters to Part slots
        model.allocate(reqs, tank, inputs={
            "capacity_kg":    tank.capacity_kg,
            "loaded_mass_kg": tank.loaded_mass_kg,
        })

The three responsibilities of a System.define():

  1. Compose the Part tree via model.composed_of(name, PartType).

  2. Compose the Requirement tree via model.composed_of(name, RequirementType).

  3. Allocate requirements to Parts via model.allocate(req_ref, part_ref, inputs={...}).


Scenario parameters on a System

Systems can declare their own parameters for scenario values that are not owned by any single Part. These appear directly on cm.root after instantiation:

class MissionSystem(System):
    @classmethod
    def define(cls, model):
        model.name("mission_system")

        # Scenario-level inputs live directly on the System
        scenario_dv = model.parameter("scenario_delta_v_m_s", unit=m_per_s)
        design_dv   = model.parameter("design_delta_v_m_s",  unit=m_per_s)

        vehicle = model.composed_of("vehicle", VehiclePart)
        reqs    = model.composed_of("reqs",    DeltaVReq)

        model.allocate(reqs, vehicle, inputs={
            "scenario_dv": scenario_dv,
            "design_dv":   design_dv,
        })

After instantiate, bind these as: cm.evaluate(inputs={cm.root.scenario_delta_v_m_s: 3800 * m_per_s, ...}).


Allocation wiring in depth

model.allocate(req_ref, target_ref, inputs={...}) is where requirement parameters get their values at evaluation time.

req_ref — the RequirementRef to allocate. Obtain it from the return value of model.composed_of("name", RequirementType). For nested packages, navigate with dot access: reqs.l1_mission.payload.

target_ref — the PartRef representing the design element being allocated to. The allocation_target_path in ConstraintResult rows matches the path to this Part.

inputs — maps each parameter name declared in the Requirement class to an AttributeRef that provides its value at runtime. The ref must point to a slot accessible from the System — either a direct System parameter, or a slot on a child Part:

# System-level parameter → requirement parameter
model.allocate(reqs.payload, aircraft, inputs={
    "scenario_payload_kg": scenario_payload_param,   # AttributeRef from model.parameter(...)
    "envelope_payload_kg": aircraft.max_payload_kg,  # AttributeRef from a child Part slot
})

Every parameter declared in the Requirement must appear in inputs for the constraint to evaluate correctly (unbound parameters are free inputs that must be supplied separately).


Allocating nested requirement packages

When a Requirement composes children, allocate each child independently:

class L1Reqs(Requirement):
    @classmethod
    def define(cls, model):
        model.name("l1_reqs")
        model.doc("L1 requirements.")
        model.composed_of("payload", PayloadReq)
        model.composed_of("range",   RangeReq)


class ProgramSystem(System):
    @classmethod
    def define(cls, model):
        model.name("program_system")
        aircraft = model.composed_of("aircraft", Aircraft)
        reqs     = model.composed_of("reqs",     L1Reqs)

        # Allocate each leaf package separately
        model.allocate(reqs.payload, aircraft, inputs={
            "scenario_payload_kg": ...,
            "envelope_payload_kg": aircraft.max_payload_kg,
        })
        model.allocate(reqs.range, aircraft, inputs={
            "scenario_range_m": ...,
            "envelope_range_m": aircraft.max_range_m,
        })

Multiple allocations across different Parts

A requirement package can be allocated to different Parts to verify the same property against different targets:

model.allocate(reqs.mass_budget, fuselage, inputs={
    "allowable_mass_kg": fuselage.structural_mass_limit_kg,
    "actual_mass_kg":    fuselage.oem_kg,
})
model.allocate(reqs.mass_budget, wing, inputs={
    "allowable_mass_kg": wing.structural_mass_limit_kg,
    "actual_mass_kg":    wing.oem_kg,
})

Each allocation produces its own ConstraintResult row with distinct allocation_target_path.


Reference: System methods

Method

Returns

Description

model.name(str)

None

Required once.

model.parameter(name, unit=...)

AttributeRef

Scenario-level input.

model.attribute(name, unit=..., expr=...)

AttributeRef

Derived system-level value.

model.constraint(name, expr=...)

Ref

System-level boolean check.

model.composed_of(name, PartType)

PartRef

Declare a child Part.

model.composed_of(name, RequirementType)

RequirementRef

Mount a requirement tree.

model.allocate(req_ref, target_ref, inputs={})

None

Wire requirement parameters to Part slots.

model.citation(name, ...)

Ref

External provenance reference.

model.references(node, citation)

None

Traceability edge to a citation.