Filter Evaluation v0.8.5

This example evaluates SysML v2 filter expressions against model elements using Compiler.evaluate_filter.

It demonstrates two filter styles: presence-based filters (@Electrical, @Mechanical) and value-based filters that match on metadata attributes using an enum (PartSource). Results are displayed as filtered tree views or flat lists depending on the filter type.

This pattern is useful when generating filtered BOMs, producing domain-specific component views, or exporting element subsets to external tools.

Example Model Structure

The model is a vehicle system where each component carries two kinds of metadata:

  • @Electrical or @Mechanical for domain classification (presence-based)

  • @Source with a kind attribute for procurement type (value-based)

vehicle
├── chassis (@Mechanical)
│   ├── brakingSystem (@Mechanical)
│   │   ├── brakeECU (@Electrical)
│   │   └── ...
│   ├── steeringSystem (@Mechanical)
│   └── suspension (@Mechanical)
└── body
    ├── lighting (@Electrical)
    ├── infotainment (@Electrical)
    └── seats (@Mechanical)

Note

Metadata annotations are applied directly to each part rather than inherited from part definitions. In SysML v2, metadata operates at the structural (model) level, not the semantic (type) level. A filter @Electrical checks the annotations on the element itself, not on its type definition. Placing @Electrical on a part def would not make its usages match the filter.

Six filter expressions are defined in two groups:

// Presence-based: select by metadata tag
package ElectricalComponents { filter @ Electrical; }
package MechanicalComponents { filter @ Mechanical; }

// Value-based: select by metadata attribute value
package COTS {
    filter (as Source).kind == PartSource::COTS;
}

Value-based filters use the (as Source).kind pattern to access metadata attributes. The as operator selects the matching metadata type from the element’s annotations and .kind accesses the attribute on it. This is important when elements carry multiple metadata annotations (e.g. both @Electrical and @Source), as it ensures the attribute is evaluated on the correct metadata type.

Note

The SysML v2 training example use Safety::isMandatory to access metadata attributes in filters. This works when references are resolved in the filtered element’s scope. To avoid ambiguity between member access and plain references, use the cast operator (as Safety).isMandatory instead to explicitly select which metadata type to evaluate.

Concepts Used

SysML v2 filter concepts:

Concept

Syntax

Description

Metadata definitions

metadata def

Define a metadata type that can be applied to elements

Metadata annotations

@Metadata

Annotate an element with metadata

Metadata with attributes

@Metadata { attr = val; }

Annotate with metadata containing attribute values

Filter expressions

filter @ Metadata;

Select elements by metadata presence

Metadata cast

(as Metadata).attr

Cast to metadata type to access attributes in filters

Enumerations

enum def / enum

Define enumeration types with named literal values

Syside API:

API

Purpose

Compiler.evaluate_filter()

Evaluates a filter expression on the metadata of a target element

Model.elements()

Iterates over semantic elements, optionally including subtypes

ElementFilterMembership

AST node containing a filter condition and its owning package

Element.owned_elements

Accesses direct children of an element

Namespace.owned_members

Accesses direct members of an namespace. Avoids recursing into relationships.

LazyIterator.collect()

Materializes lazy iterator into a list

Extracting Filter Expressions

Each filter declaration in the SysML model (e.g. inside package COTS) is represented by an ElementFilterMembership node. The script groups these by their owning package, since a package can contain multiple filter expressions (all of which must match):

def get_filter_expressions(
    model: syside.Model,
) -> dict[syside.Package, list[syside.Expression]]:
    filter_groups: dict[syside.Package, list[syside.Expression]] = {}

    for efm in model.elements(syside.ElementFilterMembership):
        package = efm.parent
        if not isinstance(package, syside.Package):
            continue

        assert efm.condition is not None

        if package not in filter_groups:
            filter_groups[package] = []

        filter_groups[package].append(efm.condition)

    return filter_groups

Evaluating Filters

Filters don’t propagate through the hierarchy, so each part must be checked individually. The script walks the vehicle’s part tree recursively and calls Compiler.evaluate_filter on every PartUsage. The call returns a boolean (does this element match?) for spec-conforming filter expressions, and a compilation report:

compiler = syside.Compiler()
result, compilation_report = compiler.evaluate_filter(
    target=element,
    filter=filter_expr,
    stdlib=STANDARD_LIBRARY,
)

The stdlib parameter provides the standard library needed for expression evaluation, obtained via Environment.get_default() which ensures that reflective metaclasses are found and usable in expression evaluation.

Displaying Results

Source filters produce a flat list of matching part names. Domain filters produce a filtered tree that preserves hierarchy: just listing matching parts would lose the structural context of where they sit in the vehicle. The tree keeps non-matching ancestors (shown in parentheses, e.g. (body)) so the nesting remains readable:

@dataclass
class FilteredNode:
    name: str
    children: list["FilteredNode"]

def build_filtered_tree(
    element: syside.Namespace,
    matched_elements: list[syside.Element],
) -> FilteredNode | None:
    self_matches = element in matched_elements
    children = []
    for child in element.owned_members.collect():
        if isinstance(child, syside.PartUsage):
            subtree = build_filtered_tree(child, matched_elements)
            if subtree is not None:
                children.append(subtree)
    if self_matches or children:
        name = element.name or "<anonymous>"
        return FilteredNode(
            name=name if self_matches else "(" + name + ")",
            children=children,
        )
    return None

Example Model

package 'Vehicle System' {
    private import ScalarValues::*;

    metadata def Electrical;
    metadata def Mechanical;

    enum def PartSource {
        enum <COTS> 'Commercial Off-The-Shelf';
        enum <MOTS> 'Modified Off-The-Shelf';
        enum <IN_HOUSE> 'Developed In-House';
        enum <CONTRACT> 'Contract Manufactured';
    }

    metadata def Source {
        attribute kind : PartSource;
    }

    package Filters {
        // Value-based filters by part source type
        package Sources {
            package COTS { filter (as Source).kind == PartSource::COTS; }
            package MOTS { filter (as Source).kind == PartSource::MOTS; }
            package 'In-House' { filter (as Source).kind == PartSource::IN_HOUSE; }
            package Contract { filter (as Source).kind == PartSource::CONTRACT; }
        }

        // Presence-based filters for tree views
        package ElectricalComponents { filter @ Electrical; }
        package MechanicalComponents { filter @ Mechanical; }
    }

    part def vehicle {
        part chassis {
            @Mechanical;
            @Source { kind = PartSource::IN_HOUSE; }

            part brakingSystem {
                @Mechanical;
                @Source { kind = PartSource::CONTRACT; }

                part brakeECU {
                    @Source { kind = PartSource::CONTRACT; }
                    @Electrical;
                }
                part brakePedal {
                    @Mechanical;
                    @Source { kind = PartSource::IN_HOUSE; }
                }
                part brakeDiscs {
                    @Mechanical;
                    @Source { kind = PartSource::CONTRACT; }
                }
            }

            part steeringSystem {
                @Mechanical;
                @Source { kind = PartSource::CONTRACT; }

                part steeringECU {
                    @Electrical;
                    @Source { kind = PartSource::MOTS; }
                }
                part steeringColumn {
                    @Mechanical;
                    @Source { kind = PartSource::IN_HOUSE; }
                }
            }

            part suspension {
                @Mechanical;
                @Source { kind = PartSource::COTS; }
            }
        }

        part body {
            part lighting {
                @Electrical;
                @Source { kind = PartSource::MOTS; }

                part headlights {
                    @Electrical;
                    @Source { kind = PartSource::MOTS; }
                }
                part interiorLights {
                    @Electrical;
                    @Source { kind = PartSource::COTS; }
                }
            }

            part infotainment {
                @Electrical;
                @Source { kind = PartSource::COTS; }

                part displayUnit {
                    @Electrical;
                    @Source { kind = PartSource::COTS; }
                }
                part speakers {
                    @Electrical;
                    @Source { kind = PartSource::COTS; }
                }
            }

            part seats {
                @Mechanical;
                @Source { kind = PartSource::CONTRACT; }
            }
        }
    }
}

Example Script

import pathlib
from dataclasses import dataclass

import syside

EXAMPLE_DIR = pathlib.Path(__file__).parent
MODEL_FILE_PATH = EXAMPLE_DIR / "example_model.sysml"
STANDARD_LIBRARY = syside.Environment.get_default().lib

# Filters in these packages are displayed as trees, the rest as flat lists
TREE_VIEWS = {"ElectricalComponents", "MechanicalComponents"}


def find_element_by_name(
    model: syside.Model, name: str
) -> syside.Element | None:
    """Search the model for a specific element by name."""

    for element in model.elements(syside.Element, include_subtypes=True):
        if element.name == name:
            return element
    return None


def get_filter_expressions(
    model: syside.Model,
) -> dict[syside.Package, list[syside.Expression]]:
    """Extract filter expressions grouped by their owning package."""
    filter_groups: dict[syside.Package, list[syside.Expression]] = {}

    for efm in model.elements(syside.ElementFilterMembership):
        package = efm.parent
        if not isinstance(package, syside.Package):
            continue

        assert efm.condition is not None

        if package not in filter_groups:
            filter_groups[package] = []

        filter_groups[package].append(efm.condition)

    return filter_groups


def matches_filter(
    compiler: syside.Compiler,
    element: syside.Element,
    filter_expr: syside.Expression,
) -> bool:
    """Check if an element matches the filter."""

    result, compilation_report = compiler.evaluate_filter(
        target=element,
        filter=filter_expr,
        stdlib=STANDARD_LIBRARY,
    )
    if compilation_report.fatal:
        print(compilation_report.diagnostics)
        exit(1)

    return bool(result)


def collect_matching(
    compiler: syside.Compiler,
    element: syside.Element,
    filter_exprs: list[syside.Expression],
) -> list[syside.Element]:
    """Collect all PartUsage descendants that match all filter expressions."""
    matched = []

    if all(matches_filter(compiler, element, expr) for expr in filter_exprs):
        matched.append(element)

    for child in element.owned_elements.collect():
        if isinstance(child, syside.PartUsage):
            matched.extend(collect_matching(compiler, child, filter_exprs))

    return matched


@dataclass
class FilteredNode:
    name: str
    children: list["FilteredNode"]


def build_filtered_tree(
    element: syside.Namespace,
    matched_elements: list[syside.Element],
) -> FilteredNode | None:
    """Build a tree containing only matching elements, preserving hierarchy among them. Non-matching ancestors are included for context."""
    self_matches = element in matched_elements

    children = []
    for child in element.owned_members.collect():
        if isinstance(child, syside.PartUsage):
            subtree = build_filtered_tree(child, matched_elements)
            if subtree is not None:
                children.append(subtree)

    if self_matches or children:
        name = element.name or "<anonymous>"
        return FilteredNode(
            name=name if self_matches else "(" + name + ")",
            children=children,
        )
    return None


def print_tree(
    node: FilteredNode, prefix: str = "", is_last: bool = True
) -> None:
    """Print a tree using box-drawing characters."""
    connector = " └── " if is_last else " ├── "
    print(f"{prefix}{connector}{node.name}")

    child_prefix = prefix + ("     " if is_last else " │   ")
    for i, child in enumerate(node.children):
        print_tree(child, child_prefix, i == len(node.children) - 1)


def main() -> None:
    # Load SysML model and get diagnostics (errors/warnings)
    (model, _) = syside.load_model([MODEL_FILE_PATH], warnings_as_errors=True)

    filters = get_filter_expressions(model)

    vehicle = find_element_by_name(model, "vehicle")
    assert vehicle is not None and isinstance(vehicle, syside.PartDefinition)

    compiler = syside.Compiler()

    for package, filter_exprs in filters.items():
        view_name = "<anonymous>"

        if package.name:
            view_name = str(package.name)
        elif isinstance(package.parent, syside.Import):
            view_name = "<filtered import>"

        matched = collect_matching(compiler, vehicle, filter_exprs)

        if view_name in TREE_VIEWS:
            # Tree view for presence-based filters
            tree = build_filtered_tree(vehicle, matched)
            print(f"\n{view_name}")
            if tree is not None:
                for i, child in enumerate(tree.children):
                    print_tree(child, "", i == len(tree.children) - 1)
        else:
            # Flat list for value-based filters
            names = [p.name for p in matched if p.name]
            print(f"{view_name}: {', '.join(names) if names else '(none)'}")


if __name__ == "__main__":
    main()

Output

COTS: suspension, interiorLights, infotainment, displayUnit, speakers
MOTS: steeringECU, lighting, headlights
In-House: chassis, brakePedal, steeringColumn
Contract: brakingSystem, brakeECU, brakeDiscs, steeringSystem, seats

ElectricalComponents
 ├── (chassis)
 │    ├── (brakingSystem)
 │    │    └── brakeECU
 │    └── (steeringSystem)
 │         └── steeringECU
 └── (body)
      ├── lighting
      │    ├── headlights
      │    └── interiorLights
      └── infotainment
           ├── displayUnit
           └── speakers

MechanicalComponents
 ├── chassis
 │    ├── brakingSystem
 │    │    ├── brakePedal
 │    │    └── brakeDiscs
 │    ├── steeringSystem
 │    │    └── steeringColumn
 │    └── suspension
 └── (body)
      └── seats

Download

Download this example here.