Extract Variants
This example extracts and displays nested variant configurations from a SysML v2 model. It shows how to find variation definitions, walk through part hierarchies with variant subsets, evaluate attributes, and handle attribute redefinition.
Example Model Structure
The model demonstrates product line configuration using vehicle seat variants:
AvailableConfigurations
├── economy
│ └── seat (basicSeat)
│ ├── material = FauxLeather
│ ├── heatingElement (singleZone)
│ │ └── heatingPower = 45W
│ └── adjustmentHandle
├── comfort
│ └── seat (basicSeat)
│ ├── material = FauxLeather
│ ├── heatingElement (dualZone)
│ │ └── heatingPower = 75W
│ └── adjustmentHandle
├── premiumEco
│ └── seat (premiumSeat)
│ ├── material = RealLeather
│ ├── heatingElement (singleZone)
│ │ └── heatingPower = 45W
│ └── adjustmentHandle
└── luxury
└── seat (premiumSeat)
├── material = RealLeather
├── heatingElement (dualZone)
│ └── heatingPower = 75W
└── adjustmentHandle
Each configuration combines seat types (basicSeat vs premiumSeat) with heating
systems (singleZone vs dualZone). This pattern is typical in product line
engineering where variations define multiple valid product configurations using
subsetting.
Concepts Used
SysML v2 variation concepts:
Concept |
Syntax |
Description |
|---|---|---|
Variation definitions |
|
Define multiple variant alternatives |
Variant parts |
|
Specific alternatives within a variation |
Subsetting variants |
|
Select which variant to use via subsetting |
Attribute redefinition |
|
Override inherited attribute values |
Syside API:
API |
Purpose |
|---|---|
Finds elements of a specific type |
|
Evaluates attribute values within a scope |
|
Accesses |
|
Accesses variant configurations |
|
Returns the document tier of an element (StandardLibrary, External, or Project) |
|
Relationship indicating attribute redefinition |
|
Materializes lazy iterator into a list |
Walking Part Hierarchies
The script recursively walks through the part ownership tree to display the complete configuration structure:
def walk_ownership_tree(element: syside.PartUsage, level: int = 0) -> None:
# Print part name
if element.name is not None:
print(" " * level, f"Part: {element.name}")
# Show attributes and their evaluated values
show_part_attributes(element, level)
# Recursively process child parts
filtered_children = [
x
for x in element.usages.collect()
if type(x) is syside.PartUsage
and x.document.document_tier is syside.DocumentTier.Project
]
for child in filtered_children:
walk_ownership_tree(child, level + 1)
This walks the part hierarchy, evaluating attributes at each level and recursively processing nested parts.
Evaluating Attributes
The script uses the compiler to evaluate attribute values within their scope:
def evaluate_feature(
feature: syside.Feature, scope: syside.Type
) -> syside.Value | None:
compiler = syside.Compiler()
value, compilation_report = compiler.evaluate_feature(
feature=feature,
scope=scope,
stdlib=STANDARD_LIBRARY,
experimental_quantities=True,
)
return value
This evaluates redefined attributes like attribute :>> material =
Materials::FauxLeather to get their actual values in the context of each variant
configuration.
Handling Attribute Redefinition
When attributes are redefined in variant configurations, the script deduplicates them to show only the active (most-derived) version:
def deduplicate_attributes(
attributes: list[syside.AttributeUsage],
) -> list[syside.Feature]:
redefined = set()
for attribute in attributes:
for inherited_relationship in attribute.heritage.relationships:
if isinstance(inherited_relationship, syside.Redefinition):
redefined.add(inherited_relationship.first_target)
return [attr for attr in attributes if attr not in redefined]
This prevents displaying both the base and redefined versions of the same attribute, showing only the final effective value.
Filtering Members
The script filters members to show only relevant model elements:
# Get only user-defined attributes (exclude standard library)
filtered_attributes = deduplicate_attributes(
[
x
for x in part.usages.collect()
if type(x) is syside.AttributeUsage
and x.document.document_tier is syside.DocumentTier.Project
]
)
# Get only child parts (exclude library elements)
filtered_children = [
x
for x in element.usages.collect()
if type(x) is syside.PartUsage
and x.document.document_tier is syside.DocumentTier.Project
]
The .collect() method materializes the lazy iterator into a list. The filtering uses
document.document_tier to show only project-level elements (excluding standard
library and external dependencies). For attributes, deduplication ensures only the
most-derived (redefined) versions are shown.
Tip
Document Tiers categorize elements by their source:
StandardLibrary- Built-in standard library elements that rarely changeExternal- Third-party library elements that change only with library updatesProject- User-defined project elements that may be edited at any time
Filtering by DocumentTier.Project ensures you see only the elements you have
defined, excluding standard library and imported elements.
Example Model
package Example_NestedVariations {
private import SI::*;
part def PoweredSystem {
attribute heatingPower : PowerValue default 0 [W];
}
part def GenericSeat {
attribute material : Materials;
part heatingElement : HeaterType;
part adjustmentHandle;
}
variation part def VehicleSeatOptions specializes GenericSeat {
variant part basicSeat {
attribute :>> material = Materials::FauxLeather;
}
variant part premiumSeat {
attribute :>> material = Materials::RealLeather;
}
}
variation part def HeaterType specializes PoweredSystem {
variant part singleZone {
attribute :>> heatingPower = 45 [W];
}
variant part dualZone {
attribute :>> heatingPower = 75 [W];
}
}
enum def Materials {
enum RealLeather;
enum FauxLeather;
}
abstract part def GenericConfiguration {
part seat : VehicleSeatOptions;
}
variation part def AvailableConfigurations specializes GenericConfiguration {
variant part economy {
part :>> seat subsets VehicleSeatOptions::basicSeat {
doc
/* Basic seat with single-zone heating */
part :>> heatingElement subsets HeaterType::singleZone;
}
}
variant part comfort {
part :>> seat subsets VehicleSeatOptions::basicSeat {
doc
/* Basic seat with dual-zone heating (upsell option) */
part :>> heatingElement subsets HeaterType::dualZone;
}
}
variant part premiumEco {
part :>> seat subsets VehicleSeatOptions::premiumSeat {
doc
/* Premium seat with single-zone (cost optimization) */
part :>> heatingElement subsets HeaterType::singleZone;
}
}
variant part luxury {
part :>> seat subsets VehicleSeatOptions::premiumSeat {
doc
/* Premium seat with dual-zone heating */
part :>> heatingElement subsets HeaterType::dualZone;
}
}
}
}
Example Script
import pathlib
import syside
EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
STANDARD_LIBRARY = syside.Environment.get_default().lib
def find_element_by_name(
model: syside.Model, name: str
) -> syside.Element | None:
"""Search the model for a specific element by name."""
# Iterates through all model elements that subset Element type
# e.g. PartUsage, ItemUsage, OccurrenceUsage, etc.
for element in model.elements(syside.Element, include_subtypes=True):
if element.name == name:
return element
return None
def deduplicate_attributes(
attributes: list[syside.AttributeUsage],
) -> list[syside.Feature]:
"""Removes attributes that have been redefined, keeping only the active versions.
Args:
attributes: List of attributes to deduplicate
"""
redefined = set()
for attribute in attributes:
for inherited_relationship in attribute.heritage.relationships:
if isinstance(inherited_relationship, syside.Redefinition):
redefined.add(inherited_relationship.first_target)
# Only keep attributes that are not redefined by anything
return [attr for attr in attributes if attr not in redefined]
def show_part_attributes(part: syside.PartUsage, level: int = 0) -> None:
"""Prints attributes and their evaluated values for a part.
Args:
part: The part whose attributes to display
level: Indentation level for hierarchical display
"""
# Filter to get only user-defined attributes (exclude standard library)
filtered_attributes = deduplicate_attributes(
[
x
for x in part.usages.collect()
if type(x) is syside.AttributeUsage
and x.document.document_tier is syside.DocumentTier.Project
]
)
for attribute in filtered_attributes:
# Evaluate the attribute value in the context of the part
value = evaluate_feature(attribute, part)
# For enum values, display just the name
if type(value) is syside.EnumerationUsage:
value = value.name
print(" " * level, f" └ Attribute: {attribute.name} = {value}")
def evaluate_feature(
feature: syside.Feature, scope: syside.Type
) -> syside.Value | None:
"""Evaluates a feature within a given scope.
Args:
feature: The feature to evaluate (attribute, part, etc.)
scope: The context in which to evaluate the feature
"""
compiler = syside.Compiler()
value, compilation_report = compiler.evaluate_feature(
feature=feature,
scope=scope,
stdlib=STANDARD_LIBRARY,
experimental_quantities=True,
)
if compilation_report.fatal:
print(compilation_report.diagnostics)
exit(1)
return value
def walk_ownership_tree(element: syside.PartUsage, level: int = 0) -> None:
"""Recursively prints the part hierarchy with attributes in a tree format.
Args:
element: The part to display and traverse
level: Indentation level for hierarchical display
"""
# Skip printing root node name
if level > 0:
if element.name is not None:
print(" " * level, f"Part: {element.name}")
else:
print(" " * level, "Part: <anonymous>")
# Get attributes and their values
show_part_attributes(element, level)
# Filter child parts: exclude library elements
filtered_children = [
x
for x in element.usages.collect()
if type(x) is syside.PartUsage
and x.document.document_tier is syside.DocumentTier.Project
]
# Recursively process each child part
for child in filtered_children:
walk_ownership_tree(child, level + 1)
def main() -> None:
# Load SysML model and get diagnostics (errors/warnings)
(model, _) = syside.load_model([MODEL_FILE_PATH], warnings_as_errors=True)
# Find the variation definition containing all configurations
available_configurations = find_element_by_name(
model, "AvailableConfigurations"
)
assert available_configurations is not None and isinstance(
available_configurations, syside.PartDefinition
)
# For each variant configuration, print its part hierarchy and attributes
for index, config in enumerate(available_configurations.variants.collect()):
if type(config) is syside.PartUsage:
print(
"\n",
"=" * 40,
f"\n CONFIGURATION #{index + 1}: {config.name}\n",
"=" * 40,
)
walk_ownership_tree(config)
print()
if __name__ == "__main__":
main()
Output
========================================
CONFIGURATION #1: economy
========================================
Part: seat
└ Attribute: material = FauxLeather
Part: heatingElement
└ Attribute: heatingPower = 45.0
Part: adjustmentHandle
========================================
CONFIGURATION #2: comfort
========================================
Part: seat
└ Attribute: material = FauxLeather
Part: heatingElement
└ Attribute: heatingPower = 75.0
Part: adjustmentHandle
========================================
CONFIGURATION #3: premiumEco
========================================
Part: seat
└ Attribute: material = RealLeather
Part: heatingElement
└ Attribute: heatingPower = 45.0
Part: adjustmentHandle
========================================
CONFIGURATION #4: luxury
========================================
Part: seat
└ Attribute: material = RealLeather
Part: heatingElement
└ Attribute: heatingPower = 75.0
Part: adjustmentHandle
Download
Download this example here.