1. The model is data¶
What is a SysML v2 model once longeron parses it?
The answer is data. longeron parses SysML v2 text into a tree of plain
Python dataclasses. The rest of this notebook shows what follows from that
fact:
You can walk the tree and read typed fields.
You can build new elements from the same dataclasses.
The model survives a JSON round trip without loss.
One
savecall writes SysML text, JSON, or KerML.validatechecks the tree before you trust it.
The subject is the DeepScout UAV program in examples/deepscout: six
SysML files, loaded as one workspace. The program holds one abstract
multirotor, five build configurations, a fleet of mission airframes,
and three mission studies. Every tutorial in this series builds on this
one program. The first cell loads the directory and prints one line per
package.
import longeron
program = longeron.load("../examples/deepscout")
for package in program.members:
nested = sum(1 for _ in package.iter_tree()) - 1
print(f"package {package.name:16s} {nested:4d} nested elements")
print("total:", sum(1 for _ in program.iter_tree()), "elements")
package DeepScout 517 nested elements
package FlyingWings 129 nested elements
package ScoutMissions 258 nested elements
package Rotorcraft 310 nested elements
package ScoutParts 294 nested elements
package ScoutSizing 173 nested elements
package ScoutSurfaces 55 nested elements
package TiltRotors 89 nested elements
package WingedVtol 86 nested elements
total: 1921 elements
Every element is a typed dataclass¶
Six files hold the whole program, and the merged tree spans parts,
calcs, requirements, actions, and state machines. Each printed line
is a package. Each element under it is a dataclass with a kind, a
qualified_name, and typed fields. Closed vocabularies such as kind
are typing.Literal aliases, so static checkers can read them.
find looks up one element by qualified name. The next cell reads
maxCruiseSpeed, the stock quadcopter’s computed max cruise speed. Its
value is an expression tree, not a number. Tutorial 2 evaluates it.
The same dataclasses are the authoring API. The second cell below builds a
long-range battery without any SysML text. It adds the new part definition
to the parts catalog. to_sysml prints any element back as text, so both
authoring routes meet in one object model.
speed = program.find("Rotorcraft::QuadCopter::maxCruiseSpeed")
print("kind: ", speed.kind)
print("types: ", speed.types)
print("owner: ", speed.owner.qualified_name)
print("value expr:", speed.value.expr.to_text())
print()
print("Motor doc: ", program.find("ScoutParts::F450Kit::Motor").doc)
kind: attribute
types: ['Real']
owner: Rotorcraft::QuadCopter
value expr: MaxCruiseSpeed(tiltDeg = cruiseTilt, massKg = totalMass, dragArea = dragArea)
Motor doc: EMAX MT2213-935KV, a 2212-class brushless outrunner. kV is
the motor velocity constant in rpm per volt (no such unit in
the vendored SI). Nominal bench table with the matched
10x4.5 propeller at 11.1 V (thrust / current by throttle):
50% 360 g 3.3 A
65% 505 g 5.5 A
75% 620 g 7.5 A
85% 715 g 9.3 A
100% 850 g 12.0 A
PropThrust is calibrated to the 100% point; MotorCurrent
reproduces the current column within a few percent.
from longeron import model as M
battery = M.Definition(kind="part", name="LongRangeBattery", supers=["Battery"])
battery.add(
M.Usage(
kind="attribute",
name="capacity",
types=["Real"],
value=M.FeatureValue(longeron.parse_expression("8000.0")),
),
M.Usage(
kind="attribute",
name="mass",
types=["Real"],
value=M.FeatureValue(longeron.parse_expression("0.55")),
),
)
program.find("ScoutParts::F450Kit").add(battery)
assert program.find("ScoutParts::F450Kit::LongRangeBattery") is battery
print(longeron.to_sysml(battery))
part def LongRangeBattery :> Battery {
attribute capacity : Real = 8000.0;
attribute mass : Real = 0.55;
}
The JSON round trip is lossless¶
The claim is checkable, so the next cell checks it. to_json writes the
tree. from_json reads it back. The clone must equal the original,
dictionary for dictionary. The clone must also still execute. The cell
calls the program’s HoverTime calc on the clone, with the capacity of
the battery you just built.
clone = longeron.from_json(longeron.to_json(program))
assert longeron.to_dict(clone) == longeron.to_dict(program)
print("round trip preserved all", sum(1 for _ in clone.iter_tree()), "elements")
minutes = longeron.Interpreter(clone).call("DeepScout::HoverTime", capacity=8000.0)
print("HoverTime on the clone:", minutes, "minutes")
round trip preserved all 1924 elements
HoverTime on the clone: 35.82089552238806 minutes
One save call, three formats¶
save dispatches on the file suffix:
.sysmlwrites the textual notation..jsonwrites the lossless schema from the previous cell..kermlwrites a projection onto KerML, the kernel language beneath SysML v2.
The KerML projection is one-way. The bundled KerML grammar re-parses the
output, and the next cell proves that with parse_kerml_text.
load accepts one file or a directory. A directory load merges every file
under one root namespace, so references resolve across files. You already
used that: examples/deepscout is a real multi-file program, split so
each specialization branch owns one file. The second cell below walks the
workspace seams. The quadcopter lives in multirotor.sysml, its abstract
base in aircraft.sysml, and its motors in parts.sysml, yet one merged
tree answers for all three.
import tempfile
from pathlib import Path
out = Path(tempfile.mkdtemp())
for suffix in (".sysml", ".json", ".kerml"):
longeron.save(program, out / f"deepscout{suffix}")
print(sorted(path.name for path in out.iterdir()))
kerml_text = (out / "deepscout.kerml").read_text()
longeron.parse_kerml_text(kerml_text) # raises on invalid KerML
print()
print("\n".join(kerml_text.splitlines()[:6]))
['deepscout.json', 'deepscout.kerml', 'deepscout.sysml']
package DeepScout {
doc /* The DeepScout UAV program root. Two branches specialize the
abstract Aircraft: the MultiRotor build family
(multirotor.sysml -- one bird you build, at bench fidelity)
and the fleet airframes (multirotor.sysml + vtolwing.sysml --
the airframe families the three missions trade). All physics
program_dir = Path("../examples/deepscout")
print("the workspace:", *sorted(p.name for p in program_dir.glob("*.sysml")))
# cross-file references resolve in the merged tree: the branch file
# specializes a definition from the root file...
quad = program.find("Rotorcraft::QuadCopter")
print("\nRotorcraft::QuadCopter specializes", quad.supers[0], "-- aircraft.sysml")
base = program.find("DeepScout::MultiRotor")
print("resolved:", base.qualified_name, f"({base.kind} def)")
# ...and types its parts from the catalog file
motors = program.find("Rotorcraft::QuadCopter::motors")
print("motors typed by", motors.types[0], "-- parts.sysml")
interp = longeron.Interpreter(program)
thrust = interp.instantiate("Rotorcraft::QuadCopter").slots["thrustPerRotor"]
print(f"\none chain across four files: thrustPerRotor = {thrust:.3f} N")
the workspace: aircraft.sysml flyingwing.sysml missions.sysml multirotor.sysml parts.sysml sizing.sysml surfaces.sysml tilttri.sysml vtolwing.sysml
Rotorcraft::QuadCopter specializes MultiRotor -- aircraft.sysml
resolved: DeepScout::MultiRotor (part def)
motors typed by Motor -- parts.sysml
one chain across four files: thrustPerRotor = 8.324 N
The first quality gate¶
validate returns one diagnostic per problem. Structural problems are
errors. Unresolved references are warnings. The validator knows the
standard library, so a bare Real resolves without an import. The command
line exposes the same gate as longeron lint <path>, and a directory
lints as one workspace.
The program validates clean, and the first assert checks that. The second model plants three defects in a package of field modifications. Each defect prints as one diagnostic.
assert not longeron.validate(program), "the program must validate clean"
print("deepscout: 0 diagnostics")
buggy = longeron.loads("""
package FieldMods {
part def LandingSkid;
part def LandingSkid; // duplicate name
part spare : NoSuchBattery; // dangling reference
part def CameraMount {
attribute mass : Real = 0.02;
attribute margin : Real = maas + 0.01; // typo in the expression
}
}
""")
for diagnostic in longeron.validate(buggy):
print(diagnostic)
deepscout: 0 diagnostics
<text>:4:5: error[duplicate-name] FieldMods::LandingSkid: name 'LandingSkid' is already used by another member of FieldMods
<text>:8:9: warning[unresolved-name] FieldMods::CameraMount::margin: expression name 'maas' does not resolve
<text>:5:5: warning[unresolved-reference] FieldMods::spare: typed by 'NoSuchBattery' does not resolve
The answer¶
Once longeron parses it, a SysML v2 model is a tree of typed dataclasses.
In this notebook you:
walked the program’s six packages and read typed fields
built a new part definition from the same dataclasses
proved the JSON round trip lossless
saved the model as SysML text, JSON, and KerML
validated the whole workspace and read three planted diagnostics
So far the tree only holds data. The quadcopter’s motor bench table also backs a claimed max cruise speed of 20.0 m/s. Tutorial 2, “The model executes”, makes the model compute that number.