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:
@Electricalor@Mechanicalfor domain classification (presence-based)@Sourcewith akindattribute 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 |
|
Define a metadata type that can be applied to elements |
Metadata annotations |
|
Annotate an element with metadata |
Metadata with attributes |
|
Annotate with metadata containing attribute values |
Filter expressions |
|
Select elements by metadata presence |
Metadata cast |
|
Cast to metadata type to access attributes in filters |
Enumerations |
|
Define enumeration types with named literal values |
Syside API:
API |
Purpose |
|---|---|
Evaluates a filter expression on the metadata of a target element |
|
Iterates over semantic elements, optionally including subtypes |
|
AST node containing a filter condition and its owning package |
|
Accesses direct children of an element |
|
Accesses direct members of an namespace. Avoids recursing into relationships. |
|
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.