Automate your first model check

Build your first Syside Automator scripts against a small automobile model: load it, walk its parts, and evaluate a mass requirement against it, finding the design 100 kg over its limit. Along the way you extend the model itself with attributes and the requirement. You need Syside Automator installed and licensed; everything else is pasted from this page.

Getting started

Start by creating a simple SysML v2 model, used throughout this example. It represents a basic automobile structure with electrical and mechanical components.

Create a new file named example_model.sysml and paste the following model:

package 'Part Tree Example' {
    part def Electrical;
    part def Mechanical;

    part Automobile {
      part 'Drive Train' {
          part Battery : Electrical;
          part Motor : Electrical;
      }

      part Chassis {
          part Suspension : Mechanical;
          part Body : Mechanical;
      }
    }
}

Model validation

Before working with the model, it’s important to validate it. This needs no script: installing syside also installs its command-line interface, available as syside, python -m syside or, in a uv project, uv run syside. To validate your model, open a terminal in the directory containing your model and run:

syside check example_model.sysml

It checks for any semantic errors or warnings in your model. After running, you should see the following output:

Checks passed!

Basic model analysis

Now analyze that model from Python, starting with a script that prints every element in a tree-like structure. The script asks its questions through syside.query, whose calls return plain Python values: query.contents hands back an element’s children as a list.

Create a new file right next to the example_model.sysml file named analyze_model.py with the following code:

import syside
from syside import query

# Load the model - this is the first step for any Syside Automator script
(model, diagnostics) = syside.load_model(["example_model.sysml"])


def walk_ownership_tree(element: syside.Element, level: int = 0) -> None:
    """Recursively print all elements in the model."""
    if element.name is not None:
        print("  " * level, element.name)
    else:
        print("  " * level, "anonymous element")
    # `contents` returns the owned elements as a plain list
    for child in query.contents(element):
        walk_ownership_tree(child, level + 1)


# Process each document in the model
for document_resource in model.documents:
    with document_resource.lock() as document:
        print("Walking the ownership tree printing all elements:")
        walk_ownership_tree(document.root_node)

load_model raises on errors, so every line after it works with a valid model, and diagnostics carries the warnings.

Run the script:

python analyze_model.py

When you run this script, you’ll see the following output:

Walking the ownership tree printing all elements:
anonymous element
  Part Tree Example
    Electrical
    Mechanical
    Automobile
      Drive Train
        Battery
        Motor
      Chassis
        Suspension
        Body

Working with part types

The model defines two types of parts, Electrical and Mechanical. Next the script identifies and displays parts by their type.

Add the following function to your script right after the walk_ownership_tree function. It looks the part definition up once with query.find_by_name_and_type, then asks each part usage whether it specializes that definition:

def show_parts_of_type(model: syside.Model, part_type: str) -> None:
    """Display all parts of a specific type in the model."""
    definition = query.find_by_name_and_type(
        model, part_type, syside.PartDefinition
    )
    assert definition is not None, f"no part definition named {part_type}"
    # `specializes` follows the whole heritage chain, so a part typed by a
    # subtype of `part_type` is found as well.
    for part in query.find_elements_of_type(model, syside.PartUsage):
        if query.specializes(part, definition):
            print("- ", part.name)


print("\nElectrical parts in the model:")
show_parts_of_type(model, "Electrical")

print("\nMechanical parts in the model:")
show_parts_of_type(model, "Mechanical")

This will output:

Electrical parts in the model:
-  Battery
-  Motor

Mechanical parts in the model:
-  Suspension
-  Body

Enhancing the model

A mass requirement makes the model more realistic. Give each part a mass attribute, then add a requirement that the total must not exceed 500 kg.

Update your example_model.sysml file with this enhanced version:

package 'Part Tree Example' {
    private import ScalarValues;
    part def Electrical {
        attribute Mass;
    }
    part def Mechanical {
        attribute Mass;
    }

    part Automobile {
        part 'Drive Train' {
            part Battery : Electrical {
                attribute redefines Mass = 150;
            }
            part Motor : Electrical {
                attribute redefines Mass = 200;
            }
            attribute DriveTrainMass = Battery.Mass + Motor.Mass;
        }
        part Chassis {
            part Suspension : Mechanical {
                attribute redefines Mass = 100;
            }
            part Body : Mechanical {
                attribute redefines Mass = 150;
            }
            attribute ChassisMass = Suspension.Mass + Body.Mass;
        }
        attribute TotalMass = 'Drive Train'.DriveTrainMass + 'Chassis'.ChassisMass;
    }

    requirement def MassLimitation {
        doc /* Total mass of the Automobile must not
            exceed 500 */
        attribute MassActual = Automobile.TotalMass;
        attribute MassLimit = 500;
    }
}

Validating requirements

Now validate that mass requirement. Add the following code to your Python script:

def show_part_decomposition(
    element: syside.Element, part_level: int = 0
) -> None:
    """Display a clean part decomposition tree."""
    if element.try_cast(syside.PartUsage):
        print("  " * part_level, element.name)
        new_part_level = part_level + 1
    else:
        new_part_level = part_level
    for child in query.contents(element):
        show_part_decomposition(child, new_part_level)


def attribute_value(model: syside.Model, name: str) -> syside.Value | None:
    """Evaluate the expression assigned to the named attribute."""
    attribute = query.find_by_name_and_type(model, name, syside.AttributeUsage)
    assert attribute is not None, f"no attribute named {name}"
    assert attribute.feature_value_expression is not None
    value, report = syside.Compiler().evaluate(
        attribute.feature_value_expression
    )
    if report.fatal:
        print(f"Error evaluating {name}")
    return value


# Find total mass and mass requirement:
total_mass = attribute_value(model, "MassActual")
mass_limit = attribute_value(model, "MassLimit")

# Display results
print("\nPart decomposition:")
for document_resource in model.documents:
    with document_resource.lock() as document:
        show_part_decomposition(document.root_node)

print(f"\nTotal mass: {total_mass} kg")
if isinstance(total_mass, (int, float)) and isinstance(
    mass_limit, (int, float)
):
    if total_mass <= mass_limit:
        print("✓ Mass requirement met")
    else:
        print("✗ Mass requirement not met")
else:
    print("✗ Cannot compare mass values - invalid types")

When you run this script, you’ll see:

Part decomposition:
Automobile
  Drive Train
    Battery
    Motor
  Chassis
    Suspension
    Body

Total mass: 600 kg
✗ Mass requirement not met

The automobile design exceeds the mass requirement by 100 kg. To meet the requirement, you would need to reduce the mass of some components or redesign the system.

What’s next