This notebook introduces port def and interface, a connection whose ends are all ports; after running it you can declare a real, port-typed connection between two logical components and see it rendered as a diagram.
HeatingSystem (Ch5 nb02) and ControlSystem (Ch1) are logical components with no connection point yet. ApplyHeat::duration (Ch4) is declared but not bound to anything: this notebook gives the two components a typed connection point for a duration signal, showing where it would flow once something produces it, without binding ApplyHeat::duration itself. A port gives each component that connection point, and an interface between two ports states which two connection points are joined.
from pathlib import Path
import opensysml
from toaster.report import format_diagnostics
conn = opensysml.connect(version="v0.9.0")
DURATION_PORT_DEF = """\
port def DurationPort {
out duration : ISQ::DurationValue[0..*];
}"""
print(DURATION_PORT_DEF)port def DurationPort {
out duration : ISQ::DurationValue[0..*];
}
DurationPort carries a value of the same type ApplyHeat already declares for its duration input, not that value itself: the port gives that kind of signal a place to enter the component, not yet a source.
HEATING_SYSTEM_PORT = """\
abstract part def HeatingSystem {
perform action applyHeat : ApplyHeat;
port durationIn : ~DurationPort;
}"""
print(HEATING_SYSTEM_PORT)abstract part def HeatingSystem {
perform action applyHeat : ApplyHeat;
port durationIn : ~DurationPort;
}
~DurationPort is the conjugate of DurationPort: durationIn receives what a DurationPort sends, the SysML v2 idiom for matching a port to its interface partner.
CONTROL_SYSTEM_PORT = """\
part def ControlSystem {
port durationOut : DurationPort;
}"""
print(CONTROL_SYSTEM_PORT)part def ControlSystem {
port durationOut : DurationPort;
}
ControlSystem gets the matching, unconjugated port: durationIn and durationOut declare the same underlying port type, which is what makes them a type-compatible pair.
TOASTER_WITH_INTERFACE = """\
part def Toaster :> ToastingSystem {
attribute cycleTime : ISQ::DurationValue;
part heating : HeatingSystem;
part control : ControlSystem;
interface durationInterface connect control.durationOut to heating.durationIn;
allocation heatAllocation allocate toastBread.applyHeat to heating;
}"""
print(TOASTER_WITH_INTERFACE)part def Toaster :> ToastingSystem {
attribute cycleTime : ISQ::DurationValue;
part heating : HeatingSystem;
part control : ControlSystem;
interface durationInterface connect control.durationOut to heating.durationIn;
allocation heatAllocation allocate toastBread.applyHeat to heating;
}
The interface connects the two ports by their owning parts’ names, control and heating, both already composed inside Toaster. Naming it (durationInterface) makes it visible to model.query(), the same convention nb02’s allocation already follows. Toaster’s body carries nb02’s heatAllocation forward here too, alongside the new interface: restating a member from the previous notebook, the way this chapter’s convention already restates cycleTime, heating and control in every notebook that touches Toaster.
TOASTER_INCREMENT = (
f"{DURATION_PORT_DEF}\n{HEATING_SYSTEM_PORT}\n"
f"{CONTROL_SYSTEM_PORT}\n{TOASTER_WITH_INTERFACE}"
)
source = Path("../../models/ch05-cumulative.sysml").read_text()
model = conn.load_from_content(source, strict=False)
assert model.ok, f"Model failed: {format_diagnostics(model.diagnostics)}"An interface’s ends must resolve to real features. The negative control below connects to a part that was never composed.
bad_source = """
package BadInterface {
private import ScalarValues::*;
private import SI::*;
private import ISQ::*;
port def P { out x : ISQ::DurationValue[0..*]; }
part def A { port p : P; }
part def B {
part a : A;
interface iface connect a.p to undefined_receiver.p;
}
}
"""
bad = conn.load_from_content(bad_source, strict=False)
assert not bad.ok
print(f"Neg control diagnostics: {bad.diagnostics[0].message!r}")Neg control diagnostics: 'unresolved reference: undefined_receiver'
The diagnostic reports an unresolved reference: undefined_receiver was never composed inside B.
from toaster.query import port_type_mismatches
mismatches = port_type_mismatches(model)
print(f"Port type mismatches: {mismatches}")Port type mismatches: []
An empty list means durationIn and durationOut declare related types (equal, or one specializing the other): the check compares declared port types, not whether the conjugation itself is the correct one for this interface.
from toaster.render import render_toolkit_interconnection
from toaster.tools import resolve_java, resolve_library, resolve_plantuml_jar, resolve_sysmlv2
from IPython.display import SVG
# Ch5's own pedagogical point is a conjugated port (durationIn : ~DurationPort); the in-house
# render_interconnection() collapses port identity to a single edge label, so this cell uses
# sysml-toolkit's real viz CLI instead, which draws the real port names as their own boxes,
# not folded into the interface label.
BINARY = resolve_sysmlv2()
LIB = resolve_library()
PLANTUML_JAR = resolve_plantuml_jar()
JAVA = resolve_java()
out_path = Path("../../figures/ch05-interconnection.svg")
out_path.parent.mkdir(exist_ok=True)
render_toolkit_interconnection(
Path("../../models/ch05-cumulative.sysml"),
"ToasterDemo::Toaster",
out_path,
lib=LIB,
binary=BINARY,
plantuml_jar=PLANTUML_JAR,
java=JAVA,
)
print(out_path.with_suffix(".puml").read_text())
SVG(filename=str(out_path))@startuml
skinparam linetype ortho
rectangle "Toaster" as n1 <<part def>> {
rectangle "heating : HeatingSystem" as n2 <<part>> {
port "durationIn\n: ~DurationPort" as n3
}
rectangle "control : ControlSystem" as n4 <<part>> {
port "durationOut\n: DurationPort" as n5
}
}
n5 -- n3 : «interface» durationInterface
@enduml
The diagram shows control and heating, the two parts of Toaster, each drawn with its own named port -- durationOut on control, durationIn on heating -- connected by the durationInterface port connection. It supports the conclusion that ControlSystem and HeatingSystem are now joined through a single, type-checked connection point between those two specific ports, not a claim about what flows through ApplyHeat itself.
The assembled definitions loaded without error, and the diagram rendered directly from the loaded model shows the same control-to-heating connection, now by its own durationOut-to-durationIn port names rather than the interface label alone.
Try the chapter exercise in exercises/ch05/exercise.ipynb: add a CoffeeFlow assembly with a pump and a filterUnit, each with a matching port joined by a named interface, confirm port_type_mismatches() returns an empty list, and render the interconnection diagram.