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

variation part def

Define multiple variant alternatives

Variant parts

variant part

Specific alternatives within a variation

Subsetting variants

seat subsets basicSeat

Select which variant to use via subsetting

Attribute redefinition

attribute :>> material

Override inherited attribute values

Syside API:

API

Purpose

Model.elements()

Finds elements of a specific type

Compiler.evaluate_feature()

Evaluates attribute values within a scope

Usage.usages

Accesses features (parts and attributes, owned or inherited) of the Usage element

PartDefinition.variants

Accesses variant configurations

Document.document_tier

Returns the document tier of an element (StandardLibrary, External, or Project)

Redefinition

Relationship indicating attribute redefinition

LazyIterator.collect()

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 change

  • External - Third-party library elements that change only with library updates

  • Project - 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.