Advent of SysML v2 | Lesson 15 – Expression Evaluation with Automator

Hello and welcome back to the Advent of SysML v2 – a daily, hands-on mini-course designed to bring you up to speed with the new SysML v2 standard and the modern Syside tooling that makes you productive from day one.

By the end of this lesson, you will:

  • Understand what feature evaluation means in SysML v2
  • Learn how to use the Automator to evaluate feature values
  • Understand how evaluation scope affects specialization and overrides
  • Discover the Concrete Syntax Tree explorer as a tool for writing better scripts

If you want to dive deeper into the world of SysML v2, check out The SysML v2 Book, where this topic is also discussed in more detail.

Feature Evaluation

So far, we have been modeling definitions and usages – the blueprints and recipes for our systems. We’ve declared parts, attributes, quantities, and their relationships. But viewing the model is not the same as evaluating it.

Feature evaluation means computing the actual values that features will have when the system exists. It’s the difference between:

  • Viewing a definition: “Reindeer has an attribute called weight of type mass with a default value of 110 kg.”
  • Evaluating a usage: “This specific reindeer (Rudolph) weighs 100 kg.”

Evaluation is essential for:

  • Verification: Does this design meet the requirements? (e.g., “Is the total weight under the limit?”)
  • Validation: Can we analyze the system’s behavior? (e.g., “What happens when energy drops to zero?”)
  • Understanding: What are the actual values in this configuration?

In SysML v2, evaluation is performed by computational tools that interpret the model. Syside Automator provides this capability, allowing us to evaluate feature expressions and get their computed values through Python scripts.

Evaluating Rudolph’s Attributes

Let’s start by evaluating the attributes of our specialized Rudolph definition. We’ll use the reindeer model from Lesson 11 as our foundation.

The Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package L15_Basic_Feature_Evaluation {
private import ScalarValues::Real;
private import ISQ::length;
private import ISQ::mass;
private import SI::kg;
private import SI::cm;

attribute def ReindeerFeatures {
attribute noseColor : Color;
attribute eyeColor : Color;
attribute antlerLength subsets ISQ::length;
}

part def Reindeer {
constant attribute features : ReindeerFeatures;
constant attribute weight redefines ISQ::mass default 110 [kg];
attribute energyLevel : Real default := 100;
}

part def Rudolph specializes Reindeer {
constant attribute :>> features = new ReindeerFeatures(Color::red, Color::brown, 60 [cm]);
constant attribute :>> weight = 100 [kg];
attribute :>> energyLevel := 200;
}

enum def Color {
enum orange; enum yellow; enum green;
enum blue; enum indigo; enum violet;
enum silver; enum gold; enum red;
enum brown; enum white; enum black;
}
}

This model defines a generic Reindeer with default values, and a specialized Rudolph definition that overrides those defaults.

Setup

To evaluate this model, we’ll use Python with Syside Automator. These examples run in Syside Cloud, where both the model file and Python script are provided for you. The script loads your SysML model and evaluates its features.

First, let’s load the model and define our helper functions:

1
2
3
4
5
6
7
8
9
10
11
12
import pathlib
import syside

# Path to our SysML model file
MODEL_FILE_PATH = pathlib.Path(“L15_BasicFeatureEvaluation.sysml”)
STANDARD_LIBRARY = syside.Environment.get_default().lib

# Load SysML model and get diagnostics (errors/warnings)
(model, diagnostics) = syside.load_model([MODEL_FILE_PATH])

# Make sure the model contains no errors before proceeding
assert not diagnostics.contains_errors(warnings_as_errors=True)

We’ll need two helper functions. First, to find elements by name:

1
2
3
4
5
6
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

And second, to evaluate expressions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def evaluate_expression(expression: syside.Expression) -> syside.Value | None:
“””Evaluate a single feature expression.”””
compiler = syside.Compiler()
value, compilation_report = compiler.evaluate(
expr=expression,
stdlib=STANDARD_LIBRARY,
experimental_quantities=True,
)
if compilation_report.fatal:
print(f“Error evaluating: {expression})
print(compilation_report.diagnostics)
exit(1)
return value

def evaluate_attribute_values(element: syside.Element) -> dict:
“””Evaluate all attribute values for a given element.”””
results = {}
for attribute in element.owned_elements:
if isinstance(attribute, syside.Feature):
results[attribute.name] = evaluate_expression(
attribute.feature_value_expression
)
return results

The evaluate_expression() function uses Syside’s compiler to evaluate a feature’s value expression. Note the experimental_quantities=True flag – this enables support for quantities with units.

Note: Property names like feature_value_expression can be discovered through the Automator API documentation or by exploring your model’s structure with the CST Explorer (covered in the Understanding Model Structure section below).

Evaluating Simple Values

Now we can evaluate Rudolph’s attributes:

1
2
3
4
5
6
7
8
9
10
11
# Evaluate Rudolph’s attributes
rudolph_element = find_element_by_name(model, “Rudolph”)
rudolph_values = evaluate_attribute_values(rudolph_element)

print(f“Rudolph values: {rudolph_values})

# For numeric attributes, we get direct values
weight = rudolph_values[“weight”]
energy = rudolph_values[“energyLevel”]
print(f” – Weight: {weight})
print(f” – Energy: {energy})

Output:

1
2
3
Rudolph values: {‘features’: <syside.core.Feature object at 0x…>, ‘weight’: 100.0, ‘energyLevel’: 200}
– Weight: 100.0
– Energy: 200

Key observations:

  • Numeric values are returned without units (100.0 instead of 100 [kg])
  • Quantity values are automatically converted to SI base units
  • The features attribute evaluates to a Feature object, not a simple value

Evaluating Complex Attributes

The features attribute is more complex – it’s an instance of ReindeerFeatures, which itself has attributes. To access its values, we need to evaluate it again:

1
2
3
4
5
6
7
8
9
10
11
# For complex attributes like ‘features’, we get a Syside object
# We need to evaluate that object to access its nested attributes
rudolph_feature_values = evaluate_attribute_values(rudolph_values[“features”])
print(f“Feature values: {rudolph_feature_values})

nose_color = rudolph_feature_values[“noseColor”]
eye_color = rudolph_feature_values[“eyeColor”]
antler_length = rudolph_feature_values[“antlerLength”]
print(f” – Nose: {nose_color})
print(f” – Eyes: {eye_color})
print(f” – Antlers: {antler_length})

Output:

1
2
3
4
Feature values: {‘noseColor’: <syside.core.EnumerationUsage object at 0x…>, ‘eyeColor’: <syside.core.EnumerationUsage object at 0x…>, ‘antlerLength’: 0.6}
– Nose: L15_Basic_Feature_Evaluation::Color::red
– Eyes: L15_Basic_Feature_Evaluation::Color::brown
– Antlers: 0.6

Key observations:

  • Enumeration values evaluate to EnumerationUsage objects
  • When printed, enumerations show their fully qualified names
  • Nested quantities follow the same conversion rules (0.6 m from 60 cm)

The evaluation pattern: Evaluation happens at the Feature level. When you evaluate a Feature’s expression:

  • Simple values (numbers/text strings) → get the value directly
  • Complex objects (attribute instances, Features) → get a Syside object that you can explore by evaluating its owned Features

Understanding Evaluation Scope

One of the most powerful concepts in feature evaluation is scope. When you evaluate a Feature, the scope determines which specialization’s values are used. This is the mechanism behind how default values and overrides actually work.

Same Feature, Different Scopes

Here’s the key insight: we can evaluate the same feature with different scopes and get different results. Let’s define a function that evaluates a feature within a specific scope:

1
2
3
4
5
6
7
8
9
10
11
12
13
def evaluate_feature(feature: syside.Feature, scope: syside.Type) -> syside.Value | None:
“””Evaluate a feature within a specific scope.”””
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

Now let’s evaluate the weight feature in different scopes:

1
2
3
4
5
6
7
8
9
10
# Find the base definition and the specialization
reindeer = find_element_by_name(model, “Reindeer”)
rudolph = find_element_by_name(model, “Rudolph”)

# Evaluate the same feature with different scopes
weight_in_reindeer_scope = evaluate_feature(reindeer[“weight”], reindeer)
weight_in_rudolph_scope = evaluate_feature(reindeer[“weight”], rudolph)

print(f“Reindeer weight: {weight_in_reindeer_scope})
print(f“Rudolph weight: {weight_in_rudolph_scope})

Output:

1
2
Reindeer weight: 110.0
Rudolph weight: 100.0

Scope and Specialization

This is the crucial point:

  • We’re evaluating reindeer["weight"] – the weight feature from the base Reindeer definition
  • When evaluated with scope=reindeer, we get 110.0 (the default value)
  • When evaluated with scope=rudolph, we get 100.0 (the overridden value in Rudolph)

The scope tells the evaluator: “evaluate this feature as if you’re in this type’s context.” Since Rudolph specializes Reindeer and overrides the weight, evaluating in Rudolph’s scope gives us Rudolph’s value.

This concept is fundamental to understanding how SysML v2 specialization works:

  1. Features are inherited – Rudolph doesn’t have its own separate weight feature; it inherits and redefines the one from Reindeer
  2. Scope determines values – The same inherited feature can have different values depending on which specialization’s scope you evaluate in
  3. Default vs override – This is the mechanism behind default values and overrides: the base scope gives defaults, specialized scopes give overrides

Understanding scope is essential when navigating part hierarchies and evaluating nested structures – you need to know which context you’re evaluating in to get the correct values.

Understanding Model Structure

When writing Syside Automator scripts, you often need to know exactly what to access in the Automator. How do you know that a feature’s value expression can be accessed through feature_value_expression? How do you discover other available properties? This is where the Concrete Syntax Tree (CST) Explorer becomes invaluable.

Concrete Syntax Tree

The Concrete Syntax Tree is a structured representation of your SysML code that shows every element and its relationships. Unlike an abstract representation that focuses on semantics, the CST preserves the exact structure of your source code.

Syside provides a CST Explorer that visualizes this structure for any SysML v2 or KerML code you paste into it. This gives you a direct view into how your code is parsed and structured.

Example: Discovering SysML Properties

Let’s see how the CST helps you write Automator scripts. Consider this simple part definition:

SysML code:

1
2
3
part def Reindeer {
attribute energyLevel : Real default := 100;
}

CST structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
Namespace [0, 0] – [3, 0]
  children: OwningMembership [0, 0] – [2, 1]
    target: PartDefinition [0, 0] – [2, 1]
      declaredName: NAME [0, 9] – [0, 17]
      children: FeatureMembership [1, 1] – [1, 45]
        target: AttributeUsage [1, 1] – [1, 45]
          declaredName: NAME [1, 11] – [1, 22]
          heritage: FeatureTyping [1, 25] – [1, 29]
            target: TypeReference [1, 25] – [1, 29]
              parts: NAME [1, 25] – [1, 29]
          value: FeatureValue [1, 30] – [1, 44]
            target: LiteralInteger [1, 41] – [1, 44]
              literal: DECIMAL_VALUE [1, 41] – [1, 44]

How This Guides Your Script Writing

The CST reveals the structure that maps to Syside Automator:

  1. Element type – The attribute is an AttributeUsage in the tree, which corresponds to syside.AttributeUsage in Python
  2. Value location – The FeatureValue node tells you where the value information lives
  3. Automator properties – In Automator, you can access:
    1. feature.feature_value – Gets the FeatureValue node itself
    2. feature.feature_value_expression – Gets the expression to evaluate (the LiteralInteger target)
  4. Other properties – The heritage node suggests .heritage or typing-related properties in Automator

Without the CST, you’d resort to trial-and-error or extensive documentation reading. With it, you can see the structure and make educated guesses about what properties exist.

Key insight: The CST structure often mirrors Automator’s structure. If you see a FeatureValue node in the tree, expect properties like feature_value or feature_value_expression in Automator.

Using the CST Explorer

Syside provides a free Syside CST Explorer where you can paste any SysML v2 or KerML code and instantly see its tree structure. This is invaluable when writing Automator scripts.

Workflow for writing better scripts:

  1. Write your SysML code in the model editor
  2. Paste it into the CST Explorer to see the structure
  3. Identify the nodes you need to access (e.g., FeatureValue, FeatureTyping)
  4. Map to Automator properties based on node names
  5. Test in your script and refine

Practical tips:

  • Compare similar constructs (e.g., default vs :=) to see how their CSTs differ
  • Use the CST when documentation is unclear about property names
  • Verify your understanding of SysML v2 grammar by seeing how constructs parse

The CST bridges the gap between “I see this SysML syntax” and “I need to access this in Python.” It’s a map that shows you exactly how your code is structured under the hood.

Challenge for Tomorrow

You’re now ready to evaluate your own models! Head over to Syside Cloud and do the following tasks:

  1. Create a model with at least three specialized reindeer (Rudolph, Dasher, Dancer, etc.) with different attribute values
  2. Write a Python script using Automator to:
    • Evaluate the weight of each reindeer
    • Evaluate the nose color of each reindeer
    • Evaluate the energy level of each reindeer
  3. Experiment with evaluation scope:
    • Evaluate a base Reindeer feature in different specialization scopes
    • Observe how the values change
  4. Use the CST explorer to understand the structure of your model and improve your script

Summary

In this episode, you learned what feature evaluation means and how it differs from viewing model definitions. This hands-on experience with evaluation is foundational – being able to compute actual values transforms your model from a static blueprint into an analyzable system.

If you haven’t yet done so, it is now time to go to Syside Cloud to do the challenge. Don’t forget to come back tomorrow for another episode of the Advent of SysML v2!

Cookies