This notebook introduces part usage (composition); after running it you can declare that a system definition owns named instances of its subsystem types.
The previous notebook declared that Toaster specializes ToastingSystem. This notebook completes Toaster’s declaration: a cycle-time slot with no value yet, and named parts for its heating and control subsystems. The model is now structurally complete for Chapter 1.
from pathlib import Path
import opensysml
from toaster.report import format_diagnostics
conn = opensysml.connect(version="v0.9.0")
# editor.add_part_def(owner='ToasterDemo', name='Toaster', specializes=['ToastingSystem']) when API ships
TOASTER_DEF = "part def Toaster :> ToastingSystem {"
print(TOASTER_DEF)part def Toaster :> ToastingSystem {
Every notebook in this chapter loads the same already-complete models/ch01-cumulative.sysml, so there is no partial Toaster in this chapter for one notebook to hand off to the next. Notebook 03’s bare Toaster :> ToastingSystem; and this notebook’s full declaration are each an illustrative fragment, checked independently by check_construction.py against a minimal stub, showing what one step of Editor-API authoring would add once the API supports it, not a live patch to a running model. The real continuation mechanism is between chapters, not within one: each chNN-cumulative.sysml is a file on disk, and the next chapter’s file is authored to include everything the previous one has, plus its own new declarations.
# editor.add_attribute(owner='ToasterDemo::Toaster', name='cycleTime', type='ISQ::DurationValue') when API ships
CYCLE_TIME_ATTR = " attribute cycleTime : ISQ::DurationValue;"
print(CYCLE_TIME_ATTR) attribute cycleTime : ISQ::DurationValue;
cycleTime is a typed, unit-bearing slot with no value: how long a cycle actually takes is a result the design produces, derived later from the mechanism and the energy balance, not a number chosen here.
# editor.add_part(owner='ToasterDemo::Toaster', name='heating', type='HeatingSystem') when API ships
HEATING_PART = " part heating : HeatingSystem;"
print(HEATING_PART)
# editor.add_part(owner='ToasterDemo::Toaster', name='control', type='ControlSystem') when API ships
CONTROL_PART = " part control : ControlSystem;"
print(CONTROL_PART) part heating : HeatingSystem;
part control : ControlSystem;
Each part usage declares that Toaster owns one instance of its type. heating : HeatingSystem means there is one heating subsystem per toaster: the heating subsystem exists inside the Toaster, not just pointed to by it. These are structural ownership relationships, not Python-style references.
TOASTER_INCREMENT = f"{TOASTER_DEF}\n{CYCLE_TIME_ATTR}\n{HEATING_PART}\n{CONTROL_PART}\n}}"
source = Path("../../models/ch01-cumulative.sysml").read_text()
model = conn.load_from_content(source, strict=False)
assert model.ok, f"Model failed: {format_diagnostics(model.diagnostics)}"The complete Toaster body loads with no diagnostics. The next cell checks what happens when a part usage names a type the model does not have.
# Negative control: a part usage must name a type that exists in the model.
bad_source = """
package Bad {
part def Toaster {
part heating : UndefinedSubsystem;
}
}
"""
bad = conn.load_from_content(bad_source, strict=False)
assert not bad.ok
print("Expected error:", bad.diagnostics[0].message)Expected error: unresolved reference: UndefinedSubsystem
Composing an undefined type raises an unresolved-reference error, reported in bad.diagnostics[0].
toaster = model.find("ToasterDemo::Toaster")
assert toaster is not None
parts = toaster.parts()
attrs = toaster.attributes()
print(f"Toaster parts ({len(parts)}):")
for p in parts:
print(f" {p.id}")
print(f"Toaster attributes ({len(attrs)}):")
for a in attrs:
print(f" {a.id}")Toaster parts (2):
ToasterDemo::Toaster::heating
ToasterDemo::Toaster::control
Toaster attributes (1):
ToasterDemo::Toaster::cycleTime
Diamond arrows show heating and control as parts Toaster owns; dashed arrows show each typed by its own part definition.
from toaster.render import model_to_dot, containment_subgraph, render_dot
from IPython.display import SVG
elements = containment_subgraph(
model, "ToasterDemo::Toaster", relations=("composition", "typing"), depth=2
)
dot = model_to_dot(model, title="Ch1", elements=elements)
out_path = Path("../../figures/ch01-composition.svg")
render_dot(dot, out_path)
SVG(filename=str(out_path))The diagram redraws what parts() reported above, now showing heating and control each typed by its own definition.
conn.close()toaster.parts() returns the two part symbols and toaster.attributes() returns cycleTime, confirming the composition is now part of the model.
The assembled Toaster declaration loaded without error, and the diagram above confirms heating and control are part of the model.
Try the chapter exercise in exercises/ch01/exercise.ipynb: compose a CoffeeMaker from BrewUnit and HeatExchanger and verify both parts appear via parts().