Value Rollup with Unit Conversion Labs
Experimental feature
This example uses experimental unit conversion functionality.
Enable with experimental_quantities=True.
This example demonstrates recursive value aggregation with automatic unit conversion
using Compiler.evaluate_feature(). It showcases two rollup scenarios:
Mass Rollup - Aggregating mass from vehicle components
Power Rollup - Calculating total power consumption for an aircraft
Both examples use the subsetting pattern where child components declare membership using
subsets relationships, and the Python API builds explicit lists to enable expression
evaluation. It is worth noting that subsetting pattern could lead to multiple possible
values.
Ambiguous subset definition example
A vehicle electrical system has a main bus and an emergency bus (subset of main). Emergency loads (fuelPump 8A) subset emergencyBus.loads. Regular loads (headlights 10A) subset mainBus.loads.
When calculating mainBus.totalCurrent, there is ambiguity on how the emergency loads should be included. Since emergencyBus subsets mainBus, the answer could be: - 10A (only explicit mainBus loads) - 18A (mainBus loads + emergency loads, transitively)
Both interpretations are valid. However, undersizing a battery based on 10A would lead to system failure.
part vehicle {
part mainBus : ElectricalBus;
part emergencyBus : ElectricalBus subsets mainBus;
part fuelPump : ElectricalLoad subsets emergencyBus.loads { current = 8A }
part headlights : ElectricalLoad subsets mainBus.loads { current = 10A }
// mainBus.totalCurrent = 10A or 18A?
// Need explicit: mainBus.loads = (fuelPump, headlights)
}
Mass Rollup Example
The vehicle mass rollup calculates total mass by aggregating values from nested components, each specifying mass in different units (kg, tonne, g):
vehicle
├── chassis (100 kg)
│ ├── drivetrain (1 tonne)
│ └── suspension (200 kg)
└── engine (500000 g)
Power Rollup Example
The aircraft power rollup calculates total power consumption from powered subsystems,
demonstrating selective aggregation where only powered components (those subsetting
poweredSubsystems) contribute to the total:
aircraft (20 W)
├── avionics (15 W)
│ ├── navigationSystem (10 W)
│ │ ├── gps (5 W)
│ │ ├── imu (300 mW)
│ │ ├── altimeter (100 mW)
│ │ └── magnetometer (80 mW)
│ ├── radar (120 W)
│ └── autopilot (35 W)
└── propulsion (1.2 kW)
Note that structural components like wings or fuselage would not contribute to power
consumption, as they don’t subset poweredSubsystems.
Building Explicit Subset Lists
The subsetting pattern (part chassis subsets subcomponents) requires explicit lists
for expression evaluation. The example includes a helper function that programmatically
builds these lists:
build_explicit_subset_list(
element=root_element,
part_def=rollup_type,
subsetting_collection=subparts_collection,
)
This converts implicit subsetting relationships into explicit comma-separated lists in memory, enabling the rollup expressions to evaluate correctly.
Note
Explicit subset lists are needed because SysML v2 specification defines subsetting as a relationship between collections, not a specification of which elements belong to each subset. Without explicit membership, value calculations cannot determine which elements to aggregate.
Evaluating with Unit Conversion
The rollup expressions are evaluated using:
compiler = syside.Compiler()
value, report = compiler.evaluate_feature(
feature=rollup_attribute, # Attribute to calculate
scope=root_element, # Evaluate in context of this part
stdlib=model.environment.lib, # Standard library for evaluation
experimental_quantities=True, # Enable automatic unit conversion
)
The compiler converts all units to SI base units and returns scalar values:
Mass rollup:
1800.0(kg)Power rollup:
1405.48(W)
Example Model
package VehicleMassRollup {
private import ScalarValues::*;
private import NumericalFunctions::*;
private import SI::*;
part def MassedThing {
part subcomponents : MassedThing [*] default null;
attribute ownMass : MassValue default 0 [kg];
attribute totalMass : MassValue = ownMass + sum(subcomponents.totalMass);
}
part vehicle : MassedThing {
// Explicit definition of subsetted parts
part redefines subcomponents = (chassis, engine);
part chassis subsets subcomponents {
attribute redefines ownMass = 100 [kg];
// part redefines subpart = (drivetrain, suspension);
part drivetrain subsets subcomponents {
attribute redefines ownMass = 1 [tonne];
}
part suspension subsets subcomponents {
attribute redefines ownMass = 200 [kg];
}
}
part engine subsets subcomponents {
attribute redefines ownMass = 500000 [g];
}
}
}
package AircraftPowerRollup {
private import ScalarValues::*;
private import NumericalFunctions::sum;
private import SI::*;
part def PoweredComponent {
part poweredSubsystems : PoweredComponent [*] default null;
attribute power : PowerValue default 0 [W];
attribute totalPower : PowerValue = power + sum(poweredSubsystems.totalPower);
}
part def CompositeSubsystem specializes PoweredComponent {
part flightComputer : PoweredComponent {
attribute redefines power = 25 [W];
}
part navigationSystem : PoweredComponent {
attribute redefines power = 10 [W];
part gps : PoweredComponent subsets poweredSubsystems {
attribute redefines power = 5 [W];
}
part imu : PoweredComponent subsets poweredSubsystems {
attribute redefines power = 300 [milli * W];
}
part altimeter : PoweredComponent subsets poweredSubsystems {
attribute redefines power = 100 [milli * W];
}
part magnetometer : PoweredComponent subsets poweredSubsystems {
attribute redefines power = 80 [milli * W];
}
}
}
part aircraft : PoweredComponent {
attribute redefines power = 20 [W];
part avionics : CompositeSubsystem subsets poweredSubsystems {
attribute redefines power = 15 [W];
part redefines navigationSystem subsets poweredSubsystems;
part radar subsets poweredSubsystems {
attribute redefines power = 120 [W];
}
part autopilot subsets poweredSubsystems {
attribute redefines power = 35 [W];
}
}
part propulsion subsets poweredSubsystems {
attribute redefines power = 1.2 [kW];
}
}
}
Example Script
import pathlib
import syside
EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
def build_explicit_subset_list(
element: syside.PartUsage,
part_def: syside.PartDefinition,
subsetting_collection: syside.PartUsage,
) -> None:
"""
Recursively builds explicit subset lists for parts using the subsetting pattern.
Creates an in-memory explicit list from subsetting relationships:
part <element> subsets <subsetting_collection> { ... }
becomes:
part redefines <subsetting_collection> = (<element>, ...)
Args:
element: SysML element to process
part_def: Part definition type to match when filtering
subsetting_collection: The feature being subsetted
"""
if (
part_def not in element.part_definitions.collect()
or element.name == subsetting_collection.name
):
return
build_list = True
# Check if subsetting part has been explicitly redefined with a value
collection_name = subsetting_collection.name
if collection_name is not None:
subpart_usage = element.get_member(collection_name)
if subpart_usage is not None and hasattr(
subpart_usage, "feature_value_expression"
):
compiler = syside.Compiler()
subpart_value, _ = compiler.evaluate(
subpart_usage.feature_value_expression
)
if subpart_value is not None:
build_list = False
# Build list if subpart value is not defined
if build_list:
list_items = [
x
for x in element.members.collect()
if isinstance(x, syside.PartUsage)
and subsetting_collection
in [
y.subsetted_feature
for y in x.heritage.relationships
if type(y) is syside.Subsetting
]
]
# Only create the explicit list if we found subsetting parts
if len(list_items) > 0:
# Create a new redefinition of subsetting_collection
_, subpart = element.children.append(
syside.FeatureMembership, syside.PartUsage
)
subpart.heritage.append(syside.Redefinition, subsetting_collection)
# Create a comma-separated list expression
_, operator_exp = subpart.feature_value_member.set_member_element(
syside.OperatorExpression
)
operator_exp.try_set_operator(syside.ExplicitOperator.Comma)
# Add each part that subsets the collection to the list
for item in list_items:
_, expr = operator_exp.arguments.append(
syside.FeatureReferenceExpression
)
expr.referent_member.set_member_element(item)
# Recursively process all child parts
for child in element.members.collect():
if type(child) is syside.PartUsage:
build_explicit_subset_list(child, part_def, subsetting_collection)
def rollup_values(
document: syside.Document,
stdlib: syside.Stdlib,
package_name: str,
rollup_part_def: str,
subparts_collection_name: str,
rollup_attribute_name: str,
root_element_name: str,
) -> syside.Value | None:
try:
package = document.root_node[package_name].cast(syside.Package)
rollup_type = package[rollup_part_def].cast(syside.PartDefinition)
subparts_collection = rollup_type[subparts_collection_name].cast(
syside.PartUsage
)
rollup_attribute = rollup_type[rollup_attribute_name].cast(
syside.Feature
)
root_element = package[root_element_name].cast(syside.PartUsage)
except KeyError as err:
print(
f"At least one of the required elements do not exist in the {package_name} definitions: {err}"
)
exit(1)
# Build explicit subset lists for all parts that use the subsetting pattern
build_explicit_subset_list(
element=root_element,
part_def=rollup_type,
subsetting_collection=subparts_collection,
)
# Evaluate the expression with automatic unit conversion
compiler = syside.Compiler()
value, report = compiler.evaluate_feature(
feature=rollup_attribute,
scope=root_element,
stdlib=stdlib,
experimental_quantities=True, # Enables automatic unit conversion
)
if report.fatal:
print(report.diagnostics)
exit(1)
return value
def main() -> None:
model, _ = syside.load_model(paths=[MODEL_FILE_PATH])
with model.user_docs[0].lock() as locked:
mass_rollup_result = rollup_values(
document=locked,
stdlib=model.environment.lib,
package_name="VehicleMassRollup",
rollup_part_def="MassedThing",
subparts_collection_name="subcomponents",
rollup_attribute_name="totalMass",
root_element_name="vehicle",
)
# Evaluates to 1,800.00 kg
print(f"Calculated total mass: {mass_rollup_result:,.2f} kg")
power_rollup_result = rollup_values(
document=locked,
stdlib=model.environment.lib,
package_name="AircraftPowerRollup",
rollup_part_def="PoweredComponent",
subparts_collection_name="poweredSubsystems",
rollup_attribute_name="totalPower",
root_element_name="aircraft",
)
# Evaluates to 1,405.48 W
print(f"Calculated total power: {power_rollup_result:,.2f} W")
if __name__ == "__main__":
main()
Output
Calculated total mass: 1,800.00 kg
Calculated total power: 1,405.48 W
Download
Download this example here.