Filter Evaluation v0.9.0
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.
Inline Filters on Views
The model also defines two views that demonstrate inline filter syntax - filter
expressions attached directly to an expose statement using square brackets, instead
of separate filter declarations:
part def FilteredViews {
view electricalView {
expose vehicle::**[@ Electrical];
}
view cotsView {
expose vehicle::**[
(as Source).kind == PartSource::COTS
];
}
}
Inline filters apply only to their own expose statement, while standalone filter
declarations apply to every expose in the enclosing view. See
Understanding Filters for the full discussion of the syntax and its scoping
rules.
For inline filters, view.exposed_elements.with_import_filter(import_filter) returns
a LazyImportsIterator over the view’s exposed
elements, with inline filter expressions already applied. The matches appear in the
output under the view name (electricalView, cotsView) instead of a filter
package name. See the v0.9.0 API release notes for the canonical
pattern.
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 a namespace. Avoids recursing into relationships. |
|
Materializes lazy iterator into a list |
|
Lazily resolves elements exposed by the view |
|
Import filter evaluator. compatible with |
|
Lazy iterator over imported and exposed members |
|
Attaches a filter evaluator so inline filter expressions are applied during iteration |
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; }
}
// View-based inline filters by part source type
part def FilteredViews {
view electricalView { expose vehicle::**[@ Electrical]; }
view cotsView { expose vehicle::**[(as Source).kind == PartSource::COTS]; }
}
// 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
# Names displayed as trees; the rest as flat lists
DISPLAY_AS_TREE = {"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_view_usages(model: syside.Model) -> list[syside.ViewUsage]:
"""Collect every view usage in the model."""
return list(model.elements(syside.ViewUsage, include_subtypes=True))
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 the element and any PartUsage descendants that match every filter expression."""
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 of matching elements, keeping non-matching ancestors 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)
views = get_view_usages(model)
vehicle = find_element_by_name(model, "vehicle")
assert vehicle is not None and isinstance(vehicle, syside.PartDefinition)
compiler = syside.Compiler()
import_filter = syside.CompilerFilter(compiler, STANDARD_LIBRARY)
matched: dict[str, list[syside.Element]] = {}
# Match parts against each view's inline filter
for view in views:
if not view.name:
continue
matched[str(view.name)] = view.exposed_elements.with_import_filter(
import_filter
).collect()
# Match parts against each standalone filter
for package, filter_exprs in filters.items():
# Skip unnamed packages and inline filters
if not package.name or isinstance(package.parent, syside.Import):
continue
matched[str(package.name)] = collect_matching(
compiler, vehicle, filter_exprs
)
for name, matches in matched.items():
if name in DISPLAY_AS_TREE:
# Tree view for presence-based filters
tree = build_filtered_tree(vehicle, matches)
print(f"\n{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 matches if p.name]
print(f"{name:>14} : {', '.join(names) if names else '(none)'}")
if __name__ == "__main__":
main()
Output
electricalView : brakeECU, steeringECU, lighting, infotainment, headlights, interiorLights, displayUnit, speakers
cotsView : suspension, infotainment, interiorLights, displayUnit, speakers
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.