sysmlpy Tutorial¶
A guide to using sysmlpy — a pure Python library for constructing and parsing SysML v2.0 models.
Installation¶
Quick Start¶
from sysmlpy import loads, Model, Package, Part, Attribute, ureg
# Parse SysML text
model = loads("""
package MyModel {
part def Engine;
part engine1: Engine {
attribute mass = 100 [kg];
}
}
""")
# Or build programmatically
p = Part(name="Stage_1", shortname="'3.1'")
a = Attribute(name="mass")
a.set_value(100 * ureg.kilogram)
p.add_child(a)
print(p.dump())
# → part <'3.1'> Stage_1 { attribute mass= 100[kilogram]; }
Architecture¶
sysmlpy has four layers:
- Public API (
usage.py) — Python classes you use directly:Part,Item,Action,State, etc. - Grammar Layer (
grammar/classes.py) — ~360 internal classes that mirror the ANTLR parse tree. Used for round-trip parsing. - ANTLR Parser (
antlr/,antlr_visitor.py) — Parses SysML v2 text into an internal dict, then into grammar objects. Two-stage SLL→LL prediction keeps large models fast (identical trees, errors, and diagnostics). - Analysis layer (
semantic.py,store.py,plantuml.py,diff.py) — semantic analysis, graph stores, view rendering, and model diffing.
SysML text → ANTLR Lexer/Parser → Visitor → dict → Grammar Classes → Public API Classes
↓
analyze() · stores · views · diff
SysML v2 to Python Mapping¶
Base Classes¶
| Python Class | Role | Key Methods |
|---|---|---|
Searchable |
Mixin — find(), all(), typed property accessors |
find(name, sysml_type, recursive), find_one(), parts, actions, states, etc. |
Usage |
Base for all usage/definition wrappers | dump(), load_from_grammar(), add_child(), set_typed_by(), set_specializes(), set_subsets(), set_redefines() |
Model |
Root container | load(s), dump() |
Package |
Namespace container | load_from_grammar(), dump() |
Transition |
State machine transition (standalone) | load_from_grammar(), source, trigger, guard, target, effect |
Structural Elements¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
Part |
part, part def |
Both | PartUsage, PartDefinition |
Yes |
Item |
item, item def |
Both | ItemUsage, ItemDefinition |
Yes |
Attribute |
attribute, attribute def |
Both | AttributeUsage, AttributeDefinition |
Yes |
Port |
port, port def |
Both | PortUsage, PortDefinition |
Yes |
Connection |
connection, connection def |
Both | ConnectionUsage, ConnectionDefinition |
Yes (via Package) |
Flow |
flow, flow def |
Both | FlowConnectionUsage, FlowConnectionDefinition |
Yes (via Package) |
FlowDef |
flow def |
Def only | FlowDefinition |
No |
Allocation |
allocation, allocation def |
Both | AllocationUsage, AllocationDefinition |
Yes (via Package) |
Individual |
individual, individual def |
Both | IndividualUsageSimple, IndividualDefinition |
Yes (via Package) |
Behavioral Elements¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
Action |
action, action def |
Both | ActionUsage, ActionDefinition |
Yes (custom) |
State |
state, state def |
Both | StateUsage, StateDefinition |
Yes (custom) |
Transition |
transition, then, entry |
N/A | TransitionUsage, TargetTransitionUsage |
Yes (standalone) |
Requirements¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
Requirement |
requirement, requirement def |
Both | RequirementUsage, RequirementDefinition |
Yes (via Package) |
UseCase |
use case, use case def |
Both | UseCaseUsage, UseCaseDefinition |
Yes (via Package) |
Cases¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
Case |
case, case def |
Both | CaseUsage, CaseDefinition |
Yes (via Package) |
AnalysisCase |
analysis, analysis def |
Both | AnalysisCaseUsage, AnalysisCaseDefinition |
Yes (via Package) |
VerificationCase |
verification, verification case def |
Both | VerificationCaseUsage, VerificationCaseDefinition |
Yes (via Package) |
Constraints & Calculations¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
Constraint |
constraint, constraint def |
Both | ConstraintUsage, ConstraintDefinition |
Yes (via Package) |
Calculation |
calc, calc def |
Both | CalculationUsage, CalculationDefinition |
Yes (via Package) |
Views & Viewpoints¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
View |
view, view def |
Both | ViewUsage, ViewDefinition |
Yes (via Package) |
Viewpoint |
viewpoint, viewpoint def |
Both | ViewpointUsage, ViewpointDefinition |
Yes (via Package) |
Concern |
concern, concern def |
Both | ConcernUsage, ConcernDefinition |
Yes (via Package) |
Metadata & Rendering¶
| Python Class | SysML Keywords | Def/Usage | Grammar Class | load_from_grammar |
|---|---|---|---|---|
Metadata |
metadata, metadata def |
Both | MetadataUsage, MetadataDefinition |
Yes (via Package) |
Rendering |
rendering, rendering def |
Both | RenderingUsage, RenderingDefinition |
Yes (via Package) |
Enumeration |
enum def |
Def only | EnumerationDefinition |
Yes (via Package) |
Custom (No Grammar Backing)¶
| Python Class | SysML Keywords | Notes |
|---|---|---|
Interface |
interface, interface def |
Custom Python implementation, grammar wrapper only |
Message |
message |
Custom Python implementation |
Reference |
ref |
Custom Python implementation |
DefaultReference |
in/out/inout ref |
Grammar-backed via DefaultReferenceUsage |
Usage Examples¶
Building Parts Programmatically¶
from sysmlpy import Part, Item, Attribute, ureg
# Create a sensor part with children
sensor = Part(name="sensor")
camera = Part(name="camera")
lens = Item(name="lens")
mass = Attribute(name="mass")
mass.set_value(100 * ureg.kilogram)
camera.add_child(mass)
sensor.add_child(camera)
sensor.add_child(lens)
print(sensor.dump())
# part sensor {
# part camera {
# attribute mass = 100 [kilogram];
# }
# item lens;
# }
Actions with Inputs and Outputs¶
from sysmlpy import Action
# Action definition
a = Action(definition=True, name="Focus")
a.add_input("scene", "Scene")
a.add_output("image", "Image")
print(a.dump())
# → action def Focus { in scene : Scene; out image : Image; }
# Action usage
b = Action(name="TakePicture")
b.add_input("scene")
b.add_output("picture")
print(b.dump())
# → action TakePicture { in scene; out picture; }
References¶
from sysmlpy import Reference, Item
# Simple reference
r = Reference(name="driver")
print(r.dump())
# → ref driver;
# Typed reference
person = Item(name="Person")
r2 = Reference(name="driver")
r2.set_type(person)
print(r2.dump())
# → ref driver : Person;
# Reference redefinition
r3 = Reference(name="payload", redefines=True)
r3.set_type(person)
print(r3.dump())
# → ref :>> payload : Person;
Specialization keywords on usages (v0.40.0+)¶
SysML v2 has four specialization kinds, and all of them round-trip on any usage — attributes, parts, actions, requirements, and more:
| Source form | Meaning | Grammar relationship |
|---|---|---|
: TypeName |
typing | Typings |
:> Base / subsets Base |
subsetting | Subsettings |
:>> Base / redefines Base |
redefinition | Redefinitions |
::> T / references T |
type-only reference | References |
from sysmlpy import loads
model = loads("""package P {
action a1 :> BaseType;
action a2 ::> RefType;
action a3 references OtherType; # keyword form
action a4 :>> RedefType;
action a5 : Real;
}""")
def find_actions(node):
out = []
if type(node).__name__ == 'Action':
out.append(node)
for c in getattr(node, 'children', []):
out.extend(find_actions(c))
return out
for a in find_actions(model):
print(a.dump())
# → action a1 :> BaseType;
# → action a2 ::> RefType;
# → action a3 ::> OtherType; # keyword canonicalizes to operator form
# → action a4 :>> RedefType;
# → action a5 : Real;
Parsing and Round-Trip¶
from sysmlpy import loads
from sysmlpy.formatting import classtree
text = """package 'Action Example' {
action def Focus { in scene : Scene; out image : Image; }
action TakePicture {
in item scene : Scene;
out item picture : Picture;
action focus : Focus { in scene; out image; }
}
}"""
model = loads(text)
tree = classtree(model)
print(tree.dump())
Redeclared Feature Names (v0.39.0+)¶
When a usage re-declares a feature (:>>, :>, ::>) without its own
identification, the user-visible name lives in the specialization chain —
not on the feature declaration. Two helpers surface it:
from sysmlpy import loads
text = """package DummyUsageModel {
private import DummyDefinitionModel::*;
view def ViewUsage :> ViewDefinition {
attribute :>> exampleAttribute = "Example Value";
}
}"""
model = loads(text)
view = model.get_child("DummyUsageModel.ViewUsage")
attr = view.attributes[0]
print(attr.get_value()) # → Example Value
print(attr.dump()) # → attribute :>> exampleAttribute= "Example Value";
print(attr.name) # → 'd68f2dc6-...' (UUID sentinel, unchanged)
print(attr.redefined_name) # → 'exampleAttribute'
print(attr.display_name) # → 'exampleAttribute'
redefined_name— last identifier segment of the first re-declaration chain;""when the element has none.display_name— user-meaningful name: returnsself.namewhen it holds a real identifier, otherwise falls back toredefined_name, suppressing the UUID sentinel. Use it for UI / log output.
self.name deliberately stays the UUID sentinel so symbol resolution,
dump, and navigation behavior is preserved.
State Machines¶
from sysmlpy import State
# State definition
s = State(definition=True, name="Running")
print(s.dump())
# → state def Running;
# State with transitions (via grammar)
model = loads("""
package States {
state def Engine {
state off;
state on {
entry start;
do run;
exit stop;
}
transition ignite first off accept KeyTurn then on;
}
}
""")
Requirements¶
from sysmlpy import Requirement
r = Requirement(definition=True, name="PowerRequirement")
r.set_doc("The system shall provide sufficient power.")
r.add_constraint("Power output >= 1000W")
print(r.dump())
Working with Units¶
from sysmlpy import Attribute, ureg
a = Attribute(name="thrust")
a.set_value(1000 * ureg.newton)
print(a.get_value()) # 1000 newton
a.set_value(a.get_value() + 199 * ureg.newton)
print(a.dump()) # attribute thrust= 1199[newton];
Model Navigation (v0.30.2+)¶
These methods are available on Model, Package, and every usage node for navigating and analyzing models.
find¶
Recursively find matching elements with flexible filtering:
from sysmlpy import loads, Part
model = loads("""
package Vehicle {
part def Engine;
part engine1: Engine {
attribute mass = 100 [kg];
}
part chassis {
part wheel1;
part wheel2;
}
}
""")
# Find by type string using sysml_type=
all_parts = model.find(sysml_type="part")
print(f"Found {len(all_parts)} parts: {[p.name for p in all_parts]}")
# → Found 5 parts: ['Engine', 'engine1', 'chassis', 'wheel1', 'wheel2']
# (find() is recursive and includes definitions)
# Find by class
all_parts = model.find(sysml_type=Part)
# Find by name (single or ambiguous)
engine = model.find_one("engine1") # returns element or None
assert engine is not None
# find_one() raises LookupError on multiple matches
# model.find_one("wheel") → LookupError: 2 matches
# Shorthand for all parts
all_parts = model.all("part")
count¶
Count elements by type across packages (non-recursive — direct package children):
# Count specific type
part_count = model.count('part')
print(f"Parts: {part_count}") # → Parts: 3 (Engine, engine1, chassis)
# Count all types
counts = model.count()
print(counts)
# → {'part': 3}
# For a full recursive count, use find() with sysml_type=
len(model.find(sysml_type="part")) # → 5
traverse¶
Walk the element tree with a callback function:
# Print tree structure with indentation
def print_tree(elem, depth):
name = getattr(elem, 'name', '?')
stype = getattr(elem, 'sysml_type', '')
indent = " " * depth
print(f"{indent}{stype}: {name}")
model.traverse(print_tree)
# → package: Vehicle
# → part: engine1
# → attribute: mass
# → part: chassis
# → part: wheel1
# → part: wheel2
to_dict¶
Export the model as a nested dictionary:
d = model.to_dict()
print(list(d.keys()))
# → ['name', 'children']
import json
print(json.dumps(d, indent=2, default=str))
# {
# "name": "Model",
# "children": [
# {
# "name": "Vehicle",
# "sysml_type": "package",
# "children": [...]
# }
# ]
# }
to_graph¶
Export the model to a NetworkX graph for analysis:
# Requires: pip install sysmlpy[graph]
store = model.to_graph()
# Graph statistics
print(store.stats())
# → {'nodes': 7, 'edges': 6, 'density': 0.143,
# 'is_connected': True, 'avg_degree': 1.71}
# Find connected components
components = store.connected_components()
print(f"Connected components: {len(components)}")
# Find cycles (useful for detecting circular type references)
cycles = store.cycles()
print(f"Cycles: {len(cycles)}")
# Node centrality (which elements have the most connections)
centrality = store.centrality()
top = sorted(centrality.items(), key=lambda x: x[1], reverse=True)[:3]
for eid, score in top:
data = store.get(eid)
print(f" {data['name']}: {score:.3f}")
# Export to GraphML for visualization in Gephi or Cytoscape
store.export_graphml("model.graphml")
path_between¶
Find the path between two elements by name:
# Path from parent to child
path = model.path_between('chassis', 'wheel1')
print(path)
# → ['chassis', 'wheel1']
# Path between siblings (goes through common parent)
path = model.path_between('wheel1', 'wheel2')
print(path)
# → ['wheel1', 'chassis', 'wheel2']
# No path returns None
path = model.path_between('engine1', 'nonexistent')
print(path) # → None
Semantic Analysis¶
Parsing only checks syntax. analyze() runs the full semantic
engine — 31 rule codes covering symbol resolution, imports, OCL
well-formedness, expression typing, and unit dimensions:
from sysmlpy import loads, analyze
model = loads("""
package Types {
part def Engine;
}
package Vehicle {
import Types::*;
part myCar : Engine; # resolved via import
part myWheel : Wheel; # undefined!
}
""")
result = analyze(model)
for issue in result:
print(f"[{issue.severity}] {issue.code}: {issue.message}")
# → [error] UNDEFINED_SYMBOL: Undefined symbol 'Wheel' referenced in Part 'myWheel'
result.errors # errors only
result.warnings # warnings only
bool(result) # True when no errors (warnings are OK)
result.raise_on_errors() # ValueError if any errors exist
len(result) # total issue count (AnalysisResult is a list)
Highlights of what the analyzer checks:
- Symbol resolution — qualified names (
P::A), imports (import Types::*, membership,::**recursion, visibility), inheritance chains - Expression validation — operand types (
"a" + 5is an error), unit compatibility ([m] + [kg]), and*//dimension derivation ([N]inferred from[kg] * [m/s^2]) - OCL well-formedness — duplicate names, cyclic specialization, subsetting/redefinition compatibility, part/port typing, feature chaining, multiplicity bounds
- Structure checks — connector ends (unresolved chains, direction wiring, port-definition compatibility), state machines, requirement subjects, traceability and verification coverage
The complete rule-code catalogue lives in STATUS.md.
Model Diff¶
Compare two models semantically — additions, removals, and changed signatures (typing, requirement subject, documentation):
from sysmlpy import diff_models
old = loads(open("v1.sysml").read())
new = loads(open("v2.sysml").read())
d = diff_models(old, new)
d.is_empty() # False when the models differ
d.added, d.removed, d.changed
print(d.as_text()) # +/-/~ unified text view
print(d.as_markdown()) # for release notes / PRs
# JSON output for tooling: sysmlpy diff old.sysml new.sysml --format json
Identity is (kind, qualified name) — repurposing a name across
roles reports removed + added rather than a silent change. The same
engine backs the CLI: sysmlpy diff old.sysml new.sysml.
Command-Line Tools¶
Everything above is scriptable from the shell with CI-friendly exit
codes (0 clean, 1 findings/error, 2 load failure):
sysmlpy analyze model.sysml # semantic gate
sysmlpy diff old.sysml new.sysml # semantic diff
sysmlpy view model.sysml # PlantUML / Markdown / HTML views
sysmlpy format model.sysml # canonicalize
sysmlpy trace model.sysml # requirement traceability
sysmlpy export model.sysml # JSON interchange
sysmlpy eval model.sysml # evaluate expressions/values
sysmlpy sim model.sysml # simulate a state machine
sysmlpy xlsx model.sysml # Excel workbook of tabular views
See the README CLI table for all flags, and sim.md for simulation specifics.
Partial Parse Recovery (v0.38.0+)¶
By default loads / load raise SysMLSyntaxError on malformed input.
When you want to keep whatever did parse — e.g. linting a file the spec
rejects, or triaging a broken model — use the _partial variants:
from sysmlpy import loads_partial, load_partial, PartialParseError
# Canonical example: a visibility-less `import ScalarValues;`
text = """package ImportVisibility {
public import ScalarValues;
private import ScalarValues;
protected import ScalarValues;
import ScalarValues; # <- rejected by the grammar
}"""
try:
model = load_partial(text)
except PartialParseError as e:
print(len(e.errors)) # → 1 ("Syntax error at 5:4: extraneous input 'import' ...")
print(e.partial is not None) # → True
from sysmlpy.formatting import classtree
print(classtree(e.partial).dump())
# package ImportVisibility {
# public import ScalarValues;
# private import ScalarValues;
# protected import ScalarValues;
# }
loads_partial(text)returns the visitor dict (raisingPartialParseErrorwith.partialon errors).load_partial(text)returns the typed Model on success.- The strict
loads/loadare unchanged.
Loading Functions¶
| Function | Description |
|---|---|
loads(text) |
Parse SysML v2 text string into a Model |
load(file) |
Parse SysML v2 file into a Model |
parse(text) |
Parse SysML v2 text into (Model, errors) tuple — never raises |
loads_partial(text) |
Like loads, but raises PartialParseError (carrying .partial) instead of aborting |
load_partial(file) |
Like load, but raises PartialParseError (carrying .partial) instead of aborting |
load_grammar(text) |
Parse into grammar dict (internal) |
load_antlr(text) |
Explicit ANTLR4 parsing path |
load_grammar_antlr(text) |
Parse into grammar dict via ANTLR4 |
Storage Backends¶
Models can be exported to graph stores for analysis and persistence — memory, NetworkX, Kùzu (embedded, Cypher), or Cayley (HTTP server):
store = model.to_graph() # NetworkX by default
print(store.stats())
store.export_graphml("model.graphml")
from sysmlpy.store import create_store
kuzu = create_store("kuzu", database="model.db") # persists to disk
See the README's Storage Backends section for backend-by-backend examples.
Where to Go Next¶
| Topic | Document |
|---|---|
| Full feature walkthrough | Quick Start |
| Architecture & capabilities | Project Summary |
| Analyzer rule codes & conformance | STATUS.md |
| State-machine simulation | docs/sim.md |
Guard conditions (if vs guard) |
docs/GUARDS.md |
| Boxes visualizer (Java-free rendering) | docs/boxes_view.md |
| Language server setup | docs/LSP.md, docs/LSP_EDITORS.md |
| Release history | CHANGELOG.md |
Conformance¶
100% of 123 OMG XPect conformance tests pass (123/123).
Run the full suite: