This notebook introduces requirement def; after running it you can declare a formal requirement with a subject and a constraint expression.
Chapter 1 established the structure of the toaster: Toaster specializes ToastingSystem and composes HeatingSystem and ControlSystem. A complete requirement has three parts (inspired by Brian Douglas, Part 4): a description of the need, a rationale for why that need is valid, and a verification method. This notebook declares TimelyToast with description and rationale using the SysML v2 doc comment (§7.21.2); Chapter 3 adds the formal verification case (§7.24). Each code cell contains a commented-out editor.add_*() call showing the future Editor API equivalent; these are informational; run the cell as written.
from pathlib import Path
import opensysml
from toaster.report import format_diagnostics
conn = opensysml.connect(version="v0.9.0")
# editor.add_requirement_def(owner='ToasterDemo', name='TimelyToast', doc=..., subject_type='Toaster') when API ships
# spec: SysML v2 formal/2026-03-02 §7.21.2: doc gives the informal text (description + rationale)
TIMELY_TOAST_REQ = """\
requirement def TimelyToast {
doc /*
* The toaster shall complete a toasting cycle in at most 180 seconds.
* Rationale: kitchen workflows typically span 5-15 minutes; a cycle
* exceeding 3 minutes delays meal preparation and falls outside where
* and how a user prepares a meal.
*/
subject toaster : Toaster;
"""
print(TIMELY_TOAST_REQ)requirement def TimelyToast {
doc /*
* The toaster shall complete a toasting cycle in at most 180 seconds.
* Rationale: kitchen workflows typically span 5-15 minutes; a cycle
* exceeding 3 minutes delays meal preparation and falls outside where
* and how a user prepares a meal.
*/
subject toaster : Toaster;
TimelyToast is a requirement definition. The doc block is the informal text (§7.21.2): it combines the description of the need with the rationale for the 180-second threshold. The subject toaster : Toaster declaration names the part type this requirement is about.
# editor.add_require_constraint(owner='ToasterDemo::TimelyToast', ...) when API ships
# require constraint: the Editor API does not yet author it (https://github.com/Open-MBEE/toaster/issues/11 / https://github.com/Open-MBEE/OpenSysML/issues/597);
# it parses and loads correctly via conn.load_from_content(), confirmed below.
# spec: SysML v2 formal/2026-03-02 §7.19 (RequirementConstraintMembership)
CONSTRAINT_BODY = " require constraint { toaster.cycleTime <= 180.0 [SI::s] }"
print(CONSTRAINT_BODY) require constraint { toaster.cycleTime <= 180.0 [SI::s] }
The require constraint body contains the condition that must hold: toaster.cycleTime <= 180.0 [SI::s]. This is a logical proposition over a subject attribute; the constraint form is stated now, but nothing yet tests a specific usage against it. Chapter 3 adds assert satisfy, the mechanism that tests a specific usage against a requirement.
# editor.add_part(owner='ToasterDemo', name='nominal', type='Toaster') when API ships
NOMINAL_PART = "part nominal : Toaster;"
print(NOMINAL_PART)part nominal : Toaster;
nominal is a package-level part usage: a Toaster with no attribute values set. cycleTime carries no default, since Chapter 1 removed it. It is a named usage of the subject, not a physical candidate: it adds nothing beyond Toaster itself. The next notebook introduces slow, a fixture built to fail the requirement’s check: its cycle time will be a deliberately injected fault value, not a plausible design point.
TOASTER_INCREMENT = f"{TIMELY_TOAST_REQ}{CONSTRAINT_BODY}\n}}\n{NOMINAL_PART}"
source = Path("../../models/ch02-cumulative.sysml").read_text()
model = conn.load_from_content(source, strict=False)
assert model.ok, f"Model failed: {format_diagnostics(model.diagnostics)}"TimelyToast and nominal load with no diagnostics. The next cell checks what happens when a constraint references an attribute that does not exist.
# Negative control: a constraint that references a non-existent attribute
# raises "unresolved member" at the point of use.
bad_source = """
package Bad {
private import ScalarValues::*;
part def Toaster { attribute cycleTime : Real default = 120.0; }
requirement def BadReq {
subject t : Toaster;
require constraint { t.nonExistentAttr <= 180.0 }
}
}
"""
bad = conn.load_from_content(bad_source, strict=False)
assert not bad.ok
print("Expected error:", bad.diagnostics[0].message)Expected error: unresolved member: nonExistentAttr
With the negative control confirmed, the next cell looks up TimelyToast in the loaded model directly.
req = model.find("ToasterDemo::TimelyToast")
assert req is not None
print(f"requirement kind: {req.kind}")
print(f"requirement id : {req.id}")
for e in model.query():
d = e.as_dict()
if d["@type"] == "RequirementDefinition":
print(f"RequirementDefinition: {d['qualifiedName']}")
conn.close()requirement kind: requirementDef
requirement id : ToasterDemo::TimelyToast
RequirementDefinition: ToasterDemo::TimelyToast
The TimelyToast fragments printed above loaded without error, and model.find()/model.query() confirm it’s now part of the model.
Try the chapter exercise in exercises/ch02/exercise.ipynb: add a new brewTemp attribute to BrewUnit, declare a TemperatureReq that requires bu.brewTemp <= 369.15 [SI::K] (96 degrees Celsius), and confirm it loads.