Expressions and Constraints
So far, the coffee machine model describes what the system is and what it does. It has structure (parts, ports, connections), behavior (actions with control flow), and modes (states with guarded transitions). What it cannot do yet is answer a quantitative question: Is the brew temperature correct? Is it safe to dispense?
This chapter adds checkable logic. You will write expressions that compute values, define calculations that can be reused, and create constraints that the model must satisfy. By the end, the coffee machine will have a temperature target calculation, a safety interlock constraint, and the language vocabulary you need to read every expression in the book.
Expression Basics
An expression in SysML v2 is any combination of values and operators that produces a result. You have already seen simple expressions in attribute defaults and transition guards. Now we look at the full operator set.
The everyday operators come in four families:
Arithmetic: +, -, *, /
Relational: <, >, <=, >=
Equality: ==, !=
Logical: and, or, not, xor, implies, and the eager pair & and |
These work the way you expect from engineering math. Arithmetic operators produce numeric results. Relational and equality operators produce Boolean results. Logical operators combine booleans.
Here is an expression that checks whether a temperature is within brewing range:
waterTemp >= 90.0 and waterTemp <= 96.0
And one that computes a total cycle time:
grindSeconds + heatSeconds + extractSeconds
Both are just expressions – sequences of values and operators. They become useful when you put them inside calculations and constraints.
and or &? They are not the same
There are two spellings of logical conjunction, and the difference is the one you know from programming languages even though it is spelled differently here.
and is conditional. In the grammar its right-hand side is a reference to
an expression rather than the expression itself, which is what lets it be left
unevaluated when the left side already decides the answer. & is eager: both
sides are plain operands and both are evaluated. or and | divide the same way.
That matters when the right-hand side is expensive or unsafe:
constraint def SafeToBrew {
doc /* The second test is only reached when a tank is present. */
waterTank != null and waterTank.level > minimumLevel
}
With and, the level is never read when there is no tank. With & it would be.
Prefer and and or – they are the safer default and what the rest of this
book uses. Reach for & or | only when you actually want both sides evaluated
regardless.
A tool wrinkle.
&and|are accepted inside a model file, but the standalone expression evaluator rejects them:sysml eval 'true & false'reports that it cannot parse the expression, whilesysml eval 'true and false'answersfalse. If you are testing an expression at the command line, use the word forms.
xor is exclusive or, and implies is logical implication – a implies b is
false only when a is true and b is false. Both are ordinary infix operators,
and both evaluate: sysml eval 'true xor false' gives true, and
sysml eval 'true implies false' gives false.
Operator Precedence and Parentheses
SysML v2 follows standard mathematical precedence: multiplication and division bind tighter than addition and subtraction, relational operators bind tighter than logical ones. But relying on precedence in complex expressions is a recipe for review comments.
Compare these two:
not brewCommand or cupDetected and lidClosed
(not brewCommand) or (cupDetected and lidClosed)
They mean the same thing, but only the second one is obvious at a glance. Parentheses cost nothing to write and save time in every review. Use them whenever an expression has more than one logical operator.
Operators Beyond the Basics
The four families above cover most everyday model logic. The language has more, and you need them once your model gets serious.
Power and modulo. ** (or ^) raises to a power, % is integer remainder.
attribute storedEnergy = boilerVolume * thermalCapacity * tempDelta ** 2;
attribute selfTestSlot = cycleNumber % 10; // every tenth cycle is a self-test
More logical operators. Beyond and, or, not, the spec gives you xor (exclusive or) and implies (logical implication). They are useful when a boolean condition reads more naturally as one of these forms:
attribute brewOk = brewRequested implies cupPresent; // identical to: not brewRequested or cupPresent
attribute exactlyOne = manualOverride xor scheduledRun; // exactly one is active at a time
Null coalescing. ?? returns its left operand if that operand has a value, otherwise its right operand. Use it for defaulting:
configuredTargetTemp ?? 93.0
If configuredTargetTemp is set, you get that value; otherwise you get 93.0. This is far cleaner than if configuredTargetTemp != null ? configuredTargetTemp else 93.0.
Type tests and coercion. istype, hastype, and as ask and answer questions about an element’s classification:
attribute isExactly = sensor istype Thermistor; // is `sensor` exactly Thermistor?
attribute isAKind = sensor hastype Sensor; // is `sensor` Sensor or any subtype?
attribute coerced = sensor as Thermistor; // treat sensor as a Thermistor
istype is exact; hastype includes subtypes; as is a coercion that gives you access to the more specific feature set.
Sequence operators. When an attribute has multiplicity greater than one, you have a sequence, and you operate on it with -> operators. The per-element rule goes in a body expression – braces holding an in parameter, a ;, and the expression over that parameter:
attribute highPower = brewers->select {in b; b.power > 1000.0}; // filter
attribute allTargets = brewers->collect {in b; b.targetTemp}; // map
attribute healthy = brewers->reject {in b; b.fault}; // filter (negated)
attribute allBelow = brewers->forAll {in b; b.targetTemp <= 100.0}; // every element satisfies
attribute anyFault = brewers->exists {in b; b.fault}; // some element satisfies
These compose. A common pattern is “is every brewer’s temperature in the safe range?”:
brewers->forAll {in b; b.waterTemp >= 90.0 and b.waterTemp <= 96.0}
You will reach for ->forAll and ->select constantly once a model has any non-trivial structure. They turn what would be a repeated constraint into one rule that scales with the part count.
Indexing and ranges. #(n) selects the n’th element of a sequence; .. builds a range:
attribute firstBrewer = brewers#(1); // first brewer
attribute cycleNumbers = 1..5; // sequence 1, 2, 3, 4, 5
Metadata access. @ reads a metadata annotation; @@ reads all annotations of a kind. You will see these in Chapter 13, where metadata drives view rendering.
Qualified enumeration references. When two enumerations share a literal name, qualify the reference with :::
mode == BrewMode::Espresso // not just `Espresso`
Most of these operators show up rarely, but when you need ?? or ->forAll, no other operator does the job. Reach for them by name; do not work around them.
String Literals and Structured Values
So far every value in this chapter has been a number or a Boolean. SysML v2 treats strings as ordinary value types too, and it lets you build your own structured value types out of them.
String literals are written with double quotes:
attribute label : String := "Brewing";
attribute statusText : String := if waterLow ? "Low Water" else "System OK";
The quotes are not part of the value. You can include escaped characters like \" (an embedded quote) or \n (a newline) inside the literal. Strings are useful for human-readable labels and diagnostics; the next chapter uses them inside doc comments on requirements.
The second example uses the conditional expression from the next section – if <cond> ? <then> else <else> – with two string results.
Structured attribute definitions group a small, fixed set of named, typed fields into one value. There is no anonymous tuple type in the language – you name the record with an attribute def and then use it like any other type:
attribute def SensorReading {
attribute name : String;
attribute value : Real;
attribute unit : String;
}
attribute reading : SensorReading;
SensorReading is the type of a three-field record – a label, a measurement, and a unit. An attribute def is the right shape when a few values belong together (a sensor reading with its units, a coordinate’s x/y/z) but you do not need a full part def to give them an identity: an attribute value has no occurrence, no ports, and no lifecycle. Compare it to a sequence, which holds many values of one type: a structured attribute holds few values of different types, named.
The Conditional Expression
SysML v2 has a conditional expression with the syntax if <condition> ? <then-value> else <else-value>. This is not an if-statement – it is an expression that evaluates to one of two values.
if ambient < 18.0 ? 94.0 else 93.0
This reads: if the ambient temperature is below 18 degrees, the result is 94.0; otherwise 93.0. You will use this pattern in calculations where the output depends on a condition.
The ? separating the condition from the then-value is required. It prevents ambiguity in cases where the condition itself contains operators.
Calculations: Reusable Computations
A calc def defines a reusable computation. It takes typed inputs, declares a return, and contains an expression that computes the result.
calc def TargetBrewTemp {
in ambient : Real;
return result : Real;
result = if ambient < 18.0 ? 94.0 else 93.0
}
This says: given an ambient temperature, compute a target brew temperature. If the environment is cold (below 18 degrees), aim higher to compensate for heat loss.
A calc def is like a function in a programming language, but it lives inside your model. It has a name, typed parameters, and a body that computes a return value. The in keyword marks input parameters. The return keyword marks the output.
You can use a calculation anywhere you need its result. Inside a part, you create a usage of the calculation:
part def Brewer {
attribute waterTemp : Real;
attribute ambientTemp : Real;
calc targetTemp : TargetBrewTemp {
in ambient = ambientTemp;
return result : Real;
}
}
The usage targetTemp binds the calculation’s input to a concrete attribute. Whenever ambientTemp has a value, targetTemp can be evaluated. This is how calculations connect abstract formulas to specific parts of the model.
A calculation’s body is not restricted to its own input parameters. The body is a normal SysML expression and can refer to any element reachable through the owner chain at the call site. So targetTemp could read brewer.boiler.power directly, without that attribute being passed in as an in parameter – the name resolves at evaluation time, against the calc usage’s surrounding scope.
Why this matters. Authors often over-parameterise calc defs because they assume each call is a sealed black box. It is not: calc bodies resolve names through the owner chain when they evaluate, so a calc that reads
brewer.boiler.powercontinues to work after the boiler’spoweris set, overridden, or computed by another calc. You can compose calcs into a small DAG of named equations without threading every value throughinparameters.
Computed Attributes
For one-shot values that do not need a name or reuse, SysML v2 lets you put the expression directly on an attribute:
part def Brewer {
attribute ambientTemp : Real;
attribute targetTemp : Real = if ambientTemp < 18.0 ? 94.0 else 93.0;
}
targetTemp is now an attribute whose value is computed from ambientTemp. There is no separate calc def and no usage to bind. Reading targetTemp evaluates the right-hand side; if ambientTemp is set, you get a number.
Use a computed attribute when:
- the formula appears in exactly one place,
- it has no parameters (it reads other attributes by name),
- you do not need to reuse it,
- you do not need a named contract to refer to.
Use a calc def instead when any of those things are false. A calc def is a named, parametric, reusable equation. A computed attribute is a one-shot derived value.
Computed attributes evaluate lazily on read, and they compose. You can write:
part def Brewer {
attribute ambientTemp : Real;
attribute targetTemp : Real = if ambientTemp < 18.0 ? 94.0 else 93.0;
attribute boilerLimit : Real = 100.0;
attribute headroom : Real = boilerLimit - targetTemp;
attribute safe : Boolean = headroom > 0.0;
}
Reading safe evaluates headroom, which in turn reads targetTemp, which reads ambientTemp. The evaluation order is dictated by what the reader needs; you do not write it explicitly.
Why this matters. The chapter used to suggest you wrap every conditional default in a
calc def. That is overkill for one-shot fields. Computed attributes give you a small DAG of derived values withoutin/returnceremony, and the lazy-on-read evaluation means you do not pay for them until something asks.
Equations and Overrides
Once you start treating calc defs as named equations, two things follow.
First, the body of a calc def is the equation. Every usage of that calc def is an instance of the same equation, evaluated against whatever scope the usage is in. Two parts that use TargetBrewTemp are not running two different formulas – they are running the same formula with different bindings.
Second, you can replace the bindings without touching the equation. The simplest case is at the usage site:
part highAltitudeBrewer : Brewer {
calc targetTemp : TargetBrewTemp {
in ambient = ambientTemp - 5.0; // override: simulate cooler effective ambient
return result : Real;
}
}
The equation is unchanged; only the input binding moved. The same idea extends to overriding attribute values along a feature chain (boiler.power = 1500.0) or substituting a whole calc def via subclassing and :>>-redefinition. Trade studies, sensitivity sweeps, and “what if the heater were 1.5 kW” exploration all work this way.
Why this matters. Most useful analyses in a system model are not “evaluate the model once”. They are “evaluate it many times with different parameters and compare”. Once you separate equations (the calc bodies) from bindings (the values flowing in), you can vary the bindings without forking the model. Authors who do not see this distinction tend to copy entire packages just to change one number.
Calculations Over Sequences
Once a part has multiplicity, calcs over sequences become the natural way to ask system-level questions. The sequence operators from earlier (->select, ->collect, ->forAll) are first-class inside calc bodies:
calc def WorstHeadroom {
in stations : Brewer[*];
return : Real = stations->collect {in s; s.boilerLimit - s.targetTemp}->reduce min;
}
This calc takes a sequence of brewers and returns the smallest headroom across them all. The ->collect builds a sequence of headroom values; ->reduce min folds that sequence to a single number by applying min pairwise. (min and max in the standard library are two-argument functions, so a sequence is reduced with them rather than aggregated by them.) The whole thing is one expression.
->forAll and ->exists are useful in constraint bodies, not just calcs:
constraint def AllStationsSafe {
in stations : Brewer[*];
stations->forAll {in s; s.waterTemp >= 90.0 and s.waterTemp <= 96.0}
}
This says “every brewer is in the safe range”. One named constraint scales with however many brewers exist.
Why this matters. Without sequence operators in calc and constraint bodies, an author writes parallel attribute trees (“worstHeadroom1, worstHeadroom2, …”) or tries to extract aggregations into the structural model. The collection operators put the aggregation where it belongs – in the equation – and let the system grow without rewriting the calc.
Calculations as ODE Right-Hand Sides
When a calc def captures the time-derivative of a state variable, you can flag that intent by subtyping GetDerivative from the standard library’s StateSpaceRepresentation package:
import StateSpaceRepresentation::*;
calc def HeaterRhs :> GetDerivative {
in t : Real;
in temp : Real;
in heaterPower : Real;
in lossCoeff : Real;
in thermalMass : Real;
return dT_dt : Real = (heaterPower - lossCoeff * temp) / thermalMass;
}
HeaterRhs is still an ordinary calc def. The supertype :> GetDerivative carries the contract: this calc returns the time-derivative of one named state variable. There is no special metadata, no magic string, no tool-specific annotation – just a stdlib supertype.
A second pattern, GetOutput, captures algebraic outputs as a function of state and inputs (for example, “the sensor reading is the boiler’s temp plus a fixed offset”). Together, GetDerivative and GetOutput describe a state-space block in the spec’s standard way; they are usually grouped under an action def :> ContinuousStateSpaceDynamics. All three come from StateSpaceRepresentation.
We cover ODE authoring properly in Chapter 16, which shows the coffee machine evolving over time with a heater turning on and off. For now, the only thing to take away is that the spec gives you a language shape for derivatives and algebraic outputs – you do not need to leave SysML to describe them.
Why this matters. Older sysml-rs material taught ODE authoring through
@ToolVariable { derivative = "..." }metadata. That pattern is removed. The spec-standard pattern iscalc def Foo :> GetDerivative { ... }, and the runtime picks up the contract from the supertype. As a SysML author, the change is: write a calc def, type its body the way you would type any equation, give it the right supertype, and the rest is the runtime’s problem.
Constraints: Reusable Rules
A constraint def defines a reusable boolean rule. Its body is an expression that must evaluate to true for the model to be considered valid.
constraint def BrewTempInRange {
in temp : Real;
temp >= 90.0 and temp <= 96.0
}
This constraint takes a temperature and checks that it falls within the safe brewing range. It does not enforce anything on its own – it just defines the rule. Think of it as a named predicate: a question that can be answered true or false.
Constraints and calculations look similar. The difference is intent:
- A
calc defcomputes a value. Its body is an assignment:result = <expression>. - A
constraint defchecks a condition. Its body is a boolean expression with no assignment.
Both are reusable. Both take typed parameters. But a calculation answers “what is the value?” while a constraint answers “is this acceptable?”
How Constraints Find Their Values
Constraint bodies often reference attributes by short name – waterTemp, cupPresent – and you might wonder how the language decides which waterTemp. Two scoping rules cover the cases you will hit:
-
In a part context. A constraint asserted inside a part resolves names by walking the owner chain outward.
waterTempfirst looks for an attribute namedwaterTempon the part the assertion lives in, then on its owner, and so on. The first match wins. -
In a requirement context. A constraint inside a
requirement defresolves names against the requirement’s declaredsubject,actor, and parameter list. The satisfy site (covered in Chapter 10) supplies the concrete bindings.
Mixing the two scoping models is the single biggest source of “constraint always passes” or “constraint cannot find variable” failures in real models. If you assert a constraint in a part and the body references specification.inputSupply.voltage, every name on that path must resolve through the owner chain – there is no implicit “specification” sitting at the top level of the package. If the model intended specification to be a parameter, it belongs as an in parameter on the constraint or as a parameter on the surrounding requirement.
A short rule of thumb: constraints inside parts read attributes by name from the enclosing part. Constraints inside requirements read attributes by name from the requirement’s parameters and subject. If you find yourself reaching across packages to name a value, you probably want a parameter.
Asserting Constraints
A constraint definition by itself is just a template. To apply it, you use assert constraint inside a part or other context:
part def Brewer {
attribute waterTemp : Real;
attribute ambientTemp : Real;
calc targetTemp : TargetBrewTemp {
in ambient = ambientTemp;
return result : Real;
}
assert constraint tempCheck : BrewTempInRange {
in temp = waterTemp;
}
}
The assert constraint line does two things. First, it creates a usage of BrewTempInRange inside Brewer, binding the constraint’s temp parameter to the brewer’s waterTemp attribute. Second, the assert keyword declares that this constraint must hold – it is not just a check you could run, it is a stated requirement on this part.
An asserted constraint is a commitment. It says: in any valid instance of Brewer, waterTemp must be between 90.0 and 96.0. When the constraint is evaluated – either by a static check or as the model runs – it produces a verdict that is either true (the assertion holds) or false (it does not), with the operand values that contributed. The same machinery powers require constraint inside a requirement; we pick that up in Chapter 10.
You can also assert a constraint inline, without referencing a named definition:
assert constraint {
waterTemp >= 90.0 and waterTemp <= 96.0
}
This works for one-off checks, but named constraints are better for anything you might reuse or need to trace to a requirement.
Asserting That Something Must Not Hold
The opposite of assert constraint is assert not constraint. It says: this combination must never hold.
part def Brewer {
attribute dispenseRequested : Boolean;
attribute cupPresent : Boolean;
assert not constraint {
dispenseRequested and not cupPresent
}
}
This reads cleanly as “it must never be the case that a dispense was requested with no cup present”. You could write the same rule as a positive assertion – assert constraint { not dispenseRequested or cupPresent } – but the negated form usually reads better when the unsafe combination is what you care about.
assert not is a separate language form, not just a textual transformation. Use it whenever the rule you want to express is “this must never be true.”
Building the Coffee Machine’s Logic
The coffee machine from previous chapters has structure, connections, actions, and states. Now add the quantitative logic that makes it checkable.
The Temperature Calculation
The brewer needs to know what temperature to target. That depends on ambient conditions – a cold room means more heat loss, so the target should be higher. This is exactly what calc def is for:
package CoffeeMachineAnalysis {
import ScalarValues::*;
import CoffeeMachineDomain::*;
calc def TargetBrewTemp {
in ambient : Real;
return target : Real;
target = if ambient < 18.0 ? 94.0 else 93.0
}
}
This is a simple two-branch formula. In a real system, it might be a polynomial curve fit or a lookup table. The point is that it lives in one place with a clear name, typed inputs, and a typed output.
The Safety Interlock
A coffee machine should not dispense hot water unless a cup is present. This is a safety interlock – a boolean rule that must be satisfied before the brew action proceeds:
package CoffeeMachineAnalysis {
import ScalarValues::*;
import CoffeeMachineDomain::*;
calc def TargetBrewTemp {
in ambient : Real;
return target : Real;
target = if ambient < 18.0 ? 94.0 else 93.0
}
constraint def SafetyInterlock {
in cupDetected : Boolean;
in brewCommand : Boolean;
not brewCommand or cupDetected
}
}
Read the constraint body carefully: not brewCommand or cupDetected. This is the logical implication “if brewCommand, then cupDetected.” It evaluates to true in three cases: no brew command was given, a cup is detected, or both. It is false only when a brew is commanded with no cup present – exactly the unsafe condition.
You could write the same rule using implies:
constraint def SafetyInterlock {
in cupDetected : Boolean;
in brewCommand : Boolean;
brewCommand implies cupDetected
}
Both forms mean the same thing. implies reads more naturally when the rule is “A requires B”; not A or B reads more naturally when you want the user to see both branches.
Applying Both to the Machine
Now bring the calculation and constraint into the coffee machine structure:
package CoffeeMachineSystem {
import ScalarValues::*;
import CoffeeMachineDomain::*;
import CoffeeMachineAnalysis::*;
part coffeeMachine : CoffeeMachine {
attribute ambientTemp : Real;
attribute cupPresent : Boolean;
attribute brewRequested : Boolean;
part brewer : Brewer {
calc targetTemp : TargetBrewTemp {
in ambient = coffeeMachine.ambientTemp;
return target : Real;
}
assert constraint tempSafe : BrewTempInRange {
in temp = waterTemp;
}
}
assert constraint interlock : SafetyInterlock {
in cupDetected = cupPresent;
in brewCommand = brewRequested;
}
}
}
Two things are asserted here. Inside brewer, the tempSafe constraint checks that the actual water temperature stays within range. At the machine level, the interlock constraint checks that no brew can happen without a cup. Both are named, both are traceable, and both can be verified independently.
The targetTemp calculation inside brewer binds to the machine’s ambient temperature. It does not enforce anything – it computes the target. The tempSafe constraint checks that the actual temperature (measured by a sensor, represented by waterTemp) stays in range. These are complementary: the calculation says what the temperature should be, and the constraint says what range is acceptable.
Calculations with Multiple Inputs
A calculation can take as many inputs as needed. Here is one that estimates the total brew cycle time:
calc def BrewCycleTime {
in grindSeconds : Real;
in heatSeconds : Real;
in extractSeconds : Real;
return total : Real;
total = grindSeconds + heatSeconds + extractSeconds
}
And a more interesting one that adjusts extraction time based on grind size:
calc def AdjustedExtractTime {
in baseSeconds : Real;
in grindFactor : Real;
return adjusted : Real;
adjusted = baseSeconds * grindFactor
}
You can compose calculations by using one calculation’s output as another’s input. This lets you build up complex formulas from simple, testable pieces.
Constraints with Multiple Conditions
A single constraint can check several conditions at once using logical operators:
constraint def ReadyToBrew {
in waterLevel : Real;
in minWaterLevel : Real;
in temp : Real;
in cupPresent : Boolean;
cupPresent and waterLevel >= minWaterLevel
and temp >= 90.0 and temp <= 96.0
}
This combines three checks: cup is present, water level is sufficient, and temperature is in range. It is a single named rule that answers a clear engineering question: is the machine ready to brew?
When a compound constraint like this gets long, consider splitting it. You already have BrewTempInRange and SafetyInterlock as separate constraints. A composite constraint can reference them conceptually while keeping the logic explicit.
When to Use What
Expressions, calculations, and constraints serve different purposes. Here is when to reach for each:
Use a bare expression when you need a one-time value – an attribute default, a transition guard, or a simple formula that appears in exactly one place.
Use a computed attribute when the value is derived from other attributes in the same part, and you do not need parameters or a name to refer to it elsewhere.
Use calc def when a computation is reusable, has clear inputs and outputs, or is complex enough to deserve a name. If you find yourself writing the same arithmetic in two places, extract it into a calculation.
Use constraint def when a boolean rule represents an engineering requirement – a range check, a safety condition, or an interlock. If someone might ask “where is that rule defined?” it should be a named constraint.
Use assert constraint to commit to a rule in a specific context. An unasserted constraint is just a definition sitting on a shelf. Assertion is what makes it binding.
Use assert not constraint when the rule you want is “this combination must never happen.” The negated form often reads better than its positive equivalent.
Reading Constraints as Math
Every expression you write in SysML v2 has a clean mathematical reading. Reviewers and stakeholders often see the math form before they see the source. Knowing the operator-to-notation correspondence makes constraints easier to read in both directions:
| Source | Math notation |
|---|---|
>=, <=, != | ≥, ≤, ≠ |
and, or, not | ∧, ∨, ¬ |
implies | ⇒ |
^ or ** | superscript: xⁿ |
/ | fraction: a / b |
abs(x) | ` |
sqrt(x) | √x |
brewer.boiler.power | subscripted identifier |
So temp >= 90.0 and temp <= 96.0 reads as 90.0 ≤ temp ≤ 96.0, and not brewCommand or cupDetected reads as ¬brewCommand ∨ cupDetected. When a constraint is hard to read in source form, try writing it the way it would render mathematically – often the cleaner phrasing is the one that translates directly. Prefer abs(x) over an ad-hoc conditional, prefer implies over not A or B when the rule is “A requires B”, and prefer pow(x, 2) over x ** 2 when the constraint is going to be reviewed verbally.
Common Mistakes
Writing constraints that do not evaluate to boolean. A constraint body must be an expression that produces true or false. If you accidentally write an arithmetic expression (like a + b) instead of a comparison (like a + b <= limit), the model will not check.
Forgetting to assert. Defining a constraint def does nothing until you assert it somewhere. If you define SafetyInterlock but never write assert constraint ... : SafetyInterlock, the rule exists in the model but is never checked. This is the most common oversight when adding constraints.
Duplicating logic instead of naming it. If the expression waterTemp >= 90.0 and waterTemp <= 96.0 appears in three places, a change to the range requires three edits. Define it once as a constraint def and reference it wherever needed.
Using calculations where constraints belong. If you write a calc def that returns a boolean, ask yourself whether it should be a constraint def instead. Calculations compute values; constraints check conditions. Using the right one makes the model’s intent clearer to reviewers.
Referencing an attribute that has no assigned value. When a constraint walks a chain like specification.inputSupply.busbarCurrentRating, every step must resolve to a bound value. If specification is declared but never given a binding (no part instance, no :>> redefinition, no satisfy site supplying it), the chain cannot evaluate. The fix is almost always to make the missing binding explicit – either declare the constraint inside a part that owns those features, or add the missing values as in parameters and let the satisfy site supply them. The error you see at evaluation time will name the unbound variable; treat it as a model-completeness signal, not a bug in the constraint.
What Comes Next
The coffee machine now has checkable logic: a temperature calculation that adapts to ambient conditions, a safety interlock that prevents dispensing without a cup, and the language vocabulary – expanded operators, equations, sequence calculations, computed attributes, and asserted constraints both positive and negated – to write the rest of the system.
In Chapter 10, you will turn these engineering rules into formal requirements and trace them from stakeholder intent through design to verification evidence. The constraints you defined here will become the “satisfy” targets that link requirements to the model.
In Chapter 16, you will let the same model run – ODE state variables advancing in time, calc defs serving as right-hand sides, asserted constraints firing per-tick as live monitors. The :> GetDerivative pattern from earlier in this chapter is the doorway into that.