This notebook introduces GenerateHeat, a level-2 function nested inside ApplyHeat, and HeatGenerator, the abstract logical carrier that performs it and exposes an energy port; after running it you can see the nest-and-carry pattern that gave ApplyHeat its own logical carrier recur one level deeper.
Chapter 5 gave ApplyHeat a logical carrier, HeatingSystem, that performs it and exposes a port for a duration signal. HeatingSystem’s own applyHeat step has an inner behavior with no function of its own yet: generating heat from the energy supplied. This notebook nests GenerateHeat inside ApplyHeat, the same way Chapter 4 nested ApplyHeat inside ToastBread, then gives it a logical carrier of its own, HeatGenerator, one level below HeatingSystem.
from pathlib import Path
import opensysml
from toaster.report import format_diagnostics
conn = opensysml.connect(version="v0.9.0")
GENERATE_HEAT_DEF = """\
action def GenerateHeat {
in energyIn : ISQ::EnergyValue[0..*];
out heatOut : ISQ::EnergyValue;
}"""
print(GENERATE_HEAT_DEF)action def GenerateHeat {
in energyIn : ISQ::EnergyValue[0..*];
out heatOut : ISQ::EnergyValue;
}
GenerateHeat states typed flows only: an energy input and a thermal energy output, with no mechanism and no energy form committed. Any device that turns some supplied energy into heat satisfies it, the same substitution test ApplyHeat itself passes: a resistive coil and a gas flame both take some energy input and deliver heat.
ENERGY_PORT_DEF = """\
port def EnergyPort {
out energy : ISQ::EnergyValue[0..*];
}"""
print(ENERGY_PORT_DEF)port def EnergyPort {
out energy : ISQ::EnergyValue[0..*];
}
EnergyPort carries the same energy quantity GenerateHeat already declares, not a bound value: a place for it to enter a component, the same role DurationPort plays for the duration signal (Chapter 5).
# ApplyHeat's flows and balance constraint are unchanged from Chapter 4; printed here
# in full because a nested step cannot be added to an already-declared action in a
# separate statement.
APPLY_HEAT_INCREMENT = """\
action def ApplyHeat {
in bread : Bread;
in energy : ISQ::EnergyValue[0..*];
in duration : ISQ::DurationValue[0..*];
out toast : Toast;
out delivered : ISQ::EnergyValue;
out loss : ISQ::EnergyValue;
assert constraint balance {
delivered >= 0.0 [SI::J] and loss >= 0.0 [SI::J] and delivered + loss <= energy
}
first start;
then action generateHeat : GenerateHeat {
in energyIn = ApplyHeat::energy;
}
then done;
}"""
print(APPLY_HEAT_INCREMENT)action def ApplyHeat {
in bread : Bread;
in energy : ISQ::EnergyValue[0..*];
in duration : ISQ::DurationValue[0..*];
out toast : Toast;
out delivered : ISQ::EnergyValue;
out loss : ISQ::EnergyValue;
assert constraint balance {
delivered >= 0.0 [SI::J] and loss >= 0.0 [SI::J] and delivered + loss <= energy
}
first start;
then action generateHeat : GenerateHeat {
in energyIn = ApplyHeat::energy;
}
then done;
}
generateHeat nests inside ApplyHeat exactly the way applyHeat nests inside ToastBread: a named step, bound to the outer action’s own input. ApplyHeat now has an inner behavior with a function of its own.
HEAT_GENERATOR_DEF = """\
abstract part def HeatGenerator {
perform action generateHeat : GenerateHeat;
port energyIn : ~EnergyPort;
attribute power : ISQ::PowerValue;
}"""
print(HEAT_GENERATOR_DEF)abstract part def HeatGenerator {
perform action generateHeat : GenerateHeat;
port energyIn : ~EnergyPort;
attribute power : ISQ::PowerValue;
}
HeatGenerator is named for the function it carries, not for a mechanism: no concrete part specializes it yet, so no selection among alternatives has been made. power is a typed slot with no value, the same design-space idiom Toaster::cycleTime uses: a performance measure a concrete realization will bind, not a value this carrier chooses. energyIn is declared and typed but no interface connects it to a producer: a genuine gap, not yet filled, the same kind of incompleteness Chapter 5 closed for duration by building durationIn/durationOut and connecting them. No such connection is built for energyIn in this chapter.
HEATING_ASSEMBLY_DEF = """\
part def HeatingAssembly :> HeatingSystem {
part heatGen : HeatGenerator;
allocation heatGenAllocation allocate applyHeat.generateHeat to heatGen;
}"""
print(HEATING_ASSEMBLY_DEF)part def HeatingAssembly :> HeatingSystem {
part heatGen : HeatGenerator;
allocation heatGenAllocation allocate applyHeat.generateHeat to heatGen;
}
HeatingAssembly specializes HeatingSystem and composes heatGen, giving the heating subsystem real internal structure that traces to a function for the first time. heatGenAllocation is nested inside HeatingAssembly itself rather than stated at package level: it points at applyHeat.generateHeat (the nested step, reached through the applyHeat usage HeatingAssembly inherits from HeatingSystem) and at heatGen (the usage typed by the carrier that now performs it), and neither end is an accessible feature of the other outside HeatingAssembly’s own context, so the allocation has to be a member here to resolve.
TOASTER_INCREMENT = (
f"{GENERATE_HEAT_DEF}\n{ENERGY_PORT_DEF}\n{APPLY_HEAT_INCREMENT}\n"
f"{HEAT_GENERATOR_DEF}\n{HEATING_ASSEMBLY_DEF}"
)
source = Path("../../models/ch06-cumulative.sysml").read_text()
model = conn.load_from_content(source, strict=False)
assert model.ok, f"Model failed: {format_diagnostics(model.diagnostics)}"generateHeat, nested inside ApplyHeat one level deeper than Chapter 4’s own flow, drawn the same way.
from toaster.render import render_action_flow
from IPython.display import SVG
out_path = Path("../../figures/ch06-applyheat-flow.svg")
out_path.parent.mkdir(exist_ok=True)
render_action_flow(model, "ToasterDemo::ApplyHeat", out_path)
SVG(filename=str(out_path))An allocation’s target must resolve to a real usage. The negative control below allocates to a slot that was never composed.
bad_source = """
package BadAlloc {
private import ScalarValues::*;
action def GenerateHeat;
action def ApplyHeat { action generateHeat : GenerateHeat; }
abstract part def HeatGenerator;
part def HeatingAssembly { part heatGen : HeatGenerator; }
allocation badAlloc allocate ApplyHeat::generateHeat to HeatingAssembly::undefinedSlot;
}
"""
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: HeatingAssembly::undefinedSlot'
The diagnostic reports an unresolved reference: HeatingAssembly::undefinedSlot was never composed, so the allocation’s target does not exist.
from toaster.query import perform_relationships
print(f"Perform relationships: {perform_relationships(model)}")Perform relationships: [{'performer': 'ToasterDemo::ToastingSystem', 'action': 'ToasterDemo::ToastBread'}, {'performer': 'ToasterDemo::HeatingSystem', 'action': 'ToasterDemo::ApplyHeat'}, {'performer': 'ToasterDemo::HeatGenerator', 'action': 'ToasterDemo::GenerateHeat'}]
heatGenAllocation, the allocation declared above inside HeatingAssembly, drawn next as a picture rather than queried as text.
from toaster.render import build_interconnection_intent, render_interconnection
intent = build_interconnection_intent(model, "ToasterDemo::HeatingAssembly", depth=1)
out_path = Path("../../figures/ch06-heatingassembly-interconnect.svg")
render_interconnection(intent, out_path)
from IPython.display import SVG
SVG(filename=str(out_path))The diagram shows heatGenAllocation’s own two ends: applyHeat.generateHeat (the nested step, reached through the applyHeat usage HeatingAssembly inherits from HeatingSystem) allocated to heatGen (the usage typed by the carrier that now performs it). This is HeatingAssembly’s own allocation, scoped to its own subsystem -- not Chapter 5’s heatAllocation, which belongs to Toaster and is not part of this picture. perform_relationships shows HeatGenerator performing GenerateHeat, the same performer-and-allocation split Chapter 5 established for HeatingSystem and ApplyHeat, one level deeper.
The definitions printed above loaded without error, and the diagram confirms heatGenAllocation’s own ends by picture, the same way Chapter 5 confirmed heatAllocation’s own ends by query.
Try the chapter exercise in exercises/ch06/exercise.ipynb: nest a level-2 function, MoveWater, inside ApplyWater (mirroring GenerateHeat nested inside ApplyHeat), give it an abstract logical carrier, WaterMover (mirroring HeatGenerator: a port, an unbound throughput slot, perform action moveWater : MoveWater;), then build BrewAssembly :> BrewUnit that composes part mover : WaterMover; with a usage-level allocation, the same structural pattern this notebook’s own HeatingAssembly/HeatGenerator composition follows.