This notebook proves deliveredEnergyBoundedBySupply for every value of its unbound features with verify_holds(), contrasts that with verify_satisfaction()'s point evaluation of the model’s existing assert satisfy claims, shows the checking loop catching a fully broken variant of the lemma as violated and a merely weakened variant as undecided with a real witness, and records the proof as engineering evidence with its own real limits stated plainly.
Notebook 01 stated deliveredEnergyBoundedBySupply and confirmed it is really part of ch08-cumulative.sysml. This notebook runs analysis against that construct and, further down, adds the one new model element this chapter’s own judgment record needs: a ReviewRecordRef tag anchoring AS-C08 to deliveredEnergyBoundedBySupply itself. See Ch8-01 for the construct itself.
from pathlib import Path
import opensysml
from toaster.report import format_diagnostics
conn = opensysml.connect(version="v0.9.0")
source = Path("../../models/ch08-cumulative.sysml").read_text()
model = conn.load_from_content(source, strict=False)
assert model.ok, f"Model failed: {format_diagnostics(model.diagnostics)}"The model loaded above already carries both deliveredEnergyBoundedBySupply (notebook 01) and the asC08Tag this notebook introduces further down, so nothing needs building before the analysis runs.
A require constraint referencing an undefined attribute still fails to load, the same language-tier control every chapter carries; this notebook’s own new negative controls, further down, check the model-checking loop itself rather than the language tier.
# A require constraint referencing an undefined attribute fails.
bad_source = """
package P {
private import ScalarValues::*;
part def T { attribute x : Real default = 5.0; }
requirement def R {
subject t : T;
require constraint { t.missingAttr <= 10.0 }
}
}
"""
bad = conn.load_from_content(bad_source, strict=False)
assert not bad.ok, "Expected failure for undeclared attribute"
print(f"Negative control ok: bad.ok={bad.ok}")Negative control ok: bad.ok=False
verify_satisfaction() evaluates every assert satisfy / assert not satisfy declaration the model carries, each at the one set of values its subject happens to have. These are the same three real claims Chapter 3 and Chapter 6 already declared: nothing about them changes in Chapter 8.
verdicts = model.verify_satisfaction()
for v in verdicts:
status = "holds" if v.holds else "FAILS"
extra = f" ({v.error})" if v.error else ""
print(f" [{status}] {v.element}{extra}")
by_element = {v.element: v for v in verdicts}
assert by_element["not satisfy timely by slow"].holds is True
assert by_element["satisfy heatGenerationReq by rated"].holds is True
assert by_element["not satisfy heatGenerationReq by weak"].holds is True
print("\nEach of the three claims above is observed at one fixed set of attribute values.") [holds] not satisfy timely by slow
[FAILS] satisfy timely (satisfaction satisfy timely: require condition evaluation failed: no value for feature toaster)
[holds] satisfy heatGenerationReq by rated
[holds] not satisfy heatGenerationReq by weak
Each of the three claims above is observed at one fixed set of attribute values.
These three claims are not all the same kind of check. timely compares Toaster::cycleTime, still a settable attribute with no relation deriving it from anything: not satisfy timely by slow holding shows the evaluation mechanics working correctly, not a finding about the toaster’s actual timing. heatGenerationReq compares HeatGenerator::power, a chosen physical rating: comparing a rated value against a threshold is a legitimate feasibility check of a design choice, which is why satisfy heatGenerationReq by rated and not satisfy heatGenerationReq by weak are real findings about those two candidates. The fourth verdict above is TimelyToastTest’s own verify timely; objective, not a satisfy claim about a specific subject: it has no subject to evaluate against, which is why it fails here with an evaluation error. conformance.satisfaction_claims_evaluated(), run through conformance.report() below, correctly skips this exact case entirely rather than reporting it as a finding: a verify relationship is not itself a claim about one subject, and the check’s own docstring says so.
from toaster import conformance
report = conformance.report(model, stage=(8, 1))
for r in report["project"]:
print(r)
result = next(r for r in report["project"] if r.check_id == "satisfaction-claims-evaluated")
assert result.status == "passed"
assert result.findings == []
print("\nsatisfaction-claims-evaluated now reports passed on a real, non-vacuous check: "
"every claim this check can evaluate evaluates to what it says it evaluates to.")Result(check_id='port-type', status='passed', findings=[], applies_from=(5, 3), reason=None, unblock_when=None)
Result(check_id='satisfaction-claims-evaluated', status='passed', findings=[], applies_from=(3, 1), reason=None, unblock_when=None)
satisfaction-claims-evaluated now reports passed on a real, non-vacuous check: every claim this check can evaluate evaluates to what it says it evaluates to.
Every verdict above is verify_satisfaction() evaluating a claim at one fixed set of values, the run engine’s own kind of answer. deliveredEnergyBoundedBySupply asks a different question: does the lemma hold for every value its unbound features could take? toaster.modelcheck.verify_holds() wraps sysml-toolkit’s real verify --solve command (Z3 underneath) to answer exactly that, over a small companion restatement of the construct. Two separate, real limits of this toolchain are why a companion file is used rather than the committed model or the construct’s own original elements directly: toaster.modelcheck’s own text parser cannot yet read a verdict line for a constraint that is also the subject of an assert satisfy declaration, which the committed model has (DEFERRED.md D-029); and, independent of that parser gap, sysml-toolkit’s Z3 backend never actually composes efficiencyBounded and deliveredEnergy into this lemma’s own check at all, whether by same-scope membership, inheritance, or a chained calc call (DEFERRED.md D-030, D-031, both confirmed below). The lemma below is therefore a hand-restated real-arithmetic fact of the same shape as the original relation, not a solver-checked reference to it.
import os
from pathlib import Path
from toaster import modelcheck as mc
from toaster.tools import resolve_library, resolve_sysmlv2, resolve_z3, tool_env
BINARY = resolve_sysmlv2()
LIB = resolve_library()
Z3 = resolve_z3()
os.environ["PATH"] = tool_env()["PATH"] # later cells call the CLI without z3=; it finds z3 on PATH
# A fixed scratch directory and fixed filenames (not tempfile.NamedTemporaryFile's own
# randomized name) keep this notebook's own printed output, including any error message
# that embeds a companion file's path, byte-for-byte reproducible run to run.
SCRATCH_DIR = Path("companion-check-scratch") # repo-relative, portable across machines
SCRATCH_DIR.mkdir(exist_ok=True)
def _write_companion(name: str, content: str) -> Path:
p = SCRATCH_DIR / name
p.write_text(content)
return p
# Restates deliveredEnergyBoundedBySupply exactly as committed in ch08-cumulative.sysml
# (https://github.com/Open-MBEE/toaster/blob/main/models/ch08-cumulative.sysml),
# with a minimal HeatGenerator stub, and no assert satisfy declaration (D-029).
COMPANION_POSITIVE = """\
package ConservationCheck {
private import ScalarValues::*;
private import SI::*;
private import ISQ::*;
private import MeasurementReferences::*;
abstract part def HeatGenerator {
attribute power : ISQ::PowerValue;
attribute efficiency : DimensionOneValue;
}
part heatGenCheck : HeatGenerator;
attribute heatGenCheckDuration : ISQ::DurationValue;
assert constraint deliveredEnergyBoundedBySupply {
(heatGenCheck.efficiency >= 0.0 and heatGenCheck.efficiency <= 1.0
and heatGenCheck.power >= 0.0 [SI::W] and heatGenCheckDuration >= 0.0 [SI::s])
implies (heatGenCheck.power * heatGenCheckDuration * heatGenCheck.efficiency)
<= heatGenCheck.power * heatGenCheckDuration
}
}
"""
def _ascii(reason: str) -> str:
"""The CLI's own reason text may carry an em dash; this display copy swaps it for a
plain double hyphen so what this notebook prints stays plain ASCII. Only the printed
copy changes; the verdict objects themselves keep the CLI's own text."""
return reason.replace("\u2014", "--")
companion_path = _write_companion("conservation_check.sysml", COMPANION_POSITIVE)
pos_verdicts = mc.verify_holds(str(companion_path), lib=str(LIB), binary=str(BINARY), z3=str(Z3), solve=True)
pos_verdict = pos_verdicts[0]
print(f"[{pos_verdict.status}] {pos_verdict.element} ({_ascii(pos_verdict.reason)})")
assert pos_verdict.status == "satisfied"
# "satisfied" can come from interval propagation alone, not necessarily Z3 (DEFERRED.md,
# the note beside D-029): this checks the reason text actually names z3, not just the
# status, confirming the solver itself resolved this one.
assert "z3" in pos_verdict.reason
assert mc.holds(str(companion_path), lib=str(LIB), binary=str(BINARY), z3=str(Z3), solve=True) is True
print("\nProved for every value of heatGenCheck.efficiency, heatGenCheck.power and "
"heatGenCheckDuration the antecedent admits, not evaluated at one.")[satisfied] deliveredEnergyBoundedBySupply (z3: holds for all values of unbound features)
Proved for every value of heatGenCheck.efficiency, heatGenCheck.power and heatGenCheckDuration the antecedent admits, not evaluated at one.
A proof is only worth trusting if the same machinery can also report a real violation, not just agree with whatever is asked of it. The cell below restates the full negation of the lemma: efficiency in range and power, duration non-negative, and delivered energy strictly exceeds supplied energy (A and not B, not A implies not B, which is a different, weaker statement built from the same pieces and genuinely undecided here, since it is vacuously true wherever the antecedent itself fails, e.g. efficiency = 2). A and not B is the one Z3 must actually resolve as unsatisfiable to report violated: given the same bounded hypothesis, delivered energy can never strictly exceed supplied energy, so no assignment can make this conjunction true, and Z3 must reason about a product of three bounded unbound features to see that, not fold a literal constant the way 1 == 2 would.
# Negative control: the FULL negation of the lemma (A and not B), which Z3 must resolve
# as unsatisfiable to report violated -- not "A implies not B" (a different, weaker
# statement, confirmed undecided in this toolchain: see the markdown above).
COMPANION_NEGATIVE = """\
package ConservationCheckBroken {
private import ScalarValues::*;
private import SI::*;
private import ISQ::*;
private import MeasurementReferences::*;
abstract part def HeatGenerator {
attribute power : ISQ::PowerValue;
attribute efficiency : DimensionOneValue;
}
part heatGenCheck : HeatGenerator;
attribute heatGenCheckDuration : ISQ::DurationValue;
assert constraint deliveredEnergyExceedsSupply {
(heatGenCheck.efficiency >= 0.0 and heatGenCheck.efficiency <= 1.0
and heatGenCheck.power >= 0.0 [SI::W] and heatGenCheckDuration >= 0.0 [SI::s])
and (heatGenCheck.power * heatGenCheckDuration * heatGenCheck.efficiency)
> heatGenCheck.power * heatGenCheckDuration
}
}
"""
negative_path = _write_companion("conservation_check_broken.sysml", COMPANION_NEGATIVE)
neg_verdicts = mc.verify_holds(str(negative_path), lib=str(LIB), binary=str(BINARY), solve=True)
neg_verdict = neg_verdicts[0]
print(f"[{neg_verdict.status}] {neg_verdict.element} ({_ascii(neg_verdict.reason)})")
assert neg_verdict.status == "violated"
assert mc.holds(str(negative_path), lib=str(LIB), binary=str(BINARY), solve=True) is False
print("\nThe loop catches a fully broken lemma: violated, not undecided, not silently accepted.")[violated] deliveredEnergyExceedsSupply (z3: unsatisfiable -- no assignment can make this hold)
The loop catches a fully broken lemma: violated, not undecided, not silently accepted.
A fully broken lemma is not the only interesting failure mode. A merely weakened variant, loosening the antecedent’s own bound from <= 1.0 to <= 1.2 (the same change Ch8-03 uses to demonstrate staleness), no longer holds for every value or fails for every value: it holds when efficiency <= 1.0 and fails when efficiency is between 1.0 and 1.2. verify_holds() correctly reports this as undecided, with a genuine Z3-found witness satisfying it, not satisfied and not violated; holds() correctly refuses to collapse that into a clean True or False, raising ModelCheckInconclusiveError instead. This is the real, three-way distinction this toolchain draws that a learner should see directly: a property can definitely hold everywhere, definitely fail everywhere, or genuinely not be settled either way by what’s stated, and only the first of those is something to build on without further work.
# A realistically weakened variant: the same lemma, its own bound loosened from 1.0 to
# 1.2. Neither a tautology nor a contradiction any more: undecided, with a witness.
COMPANION_WEAKENED = """\
package ConservationCheckWeakened {
private import ScalarValues::*;
private import SI::*;
private import ISQ::*;
private import MeasurementReferences::*;
abstract part def HeatGenerator {
attribute power : ISQ::PowerValue;
attribute efficiency : DimensionOneValue;
}
part heatGenCheck : HeatGenerator;
attribute heatGenCheckDuration : ISQ::DurationValue;
assert constraint deliveredEnergyBoundedBySupply {
(heatGenCheck.efficiency >= 0.0 and heatGenCheck.efficiency <= 1.2
and heatGenCheck.power >= 0.0 [SI::W] and heatGenCheckDuration >= 0.0 [SI::s])
implies (heatGenCheck.power * heatGenCheckDuration * heatGenCheck.efficiency)
<= heatGenCheck.power * heatGenCheckDuration
}
}
"""
weakened_path = _write_companion("conservation_check_weakened.sysml", COMPANION_WEAKENED)
weak_verdicts = mc.verify_holds(str(weakened_path), lib=str(LIB), binary=str(BINARY), solve=True)
weak_verdict = weak_verdicts[0]
print(f"[{weak_verdict.status}] {weak_verdict.element} ({_ascii(weak_verdict.reason)})")
assert weak_verdict.status == "undecided"
assert "z3" in weak_verdict.reason
try:
mc.holds(str(weakened_path), lib=str(LIB), binary=str(BINARY), solve=True)
raise AssertionError("expected holds() to raise ModelCheckInconclusiveError")
except mc.ModelCheckInconclusiveError as exc:
print(f"holds() correctly refuses to answer: {exc}")
# Clean up the fixed scratch files now that every demo in this notebook has used them.
for p in (companion_path, negative_path, weakened_path):
p.unlink(missing_ok=True)[undecided] deliveredEnergyBoundedBySupply (result is indeterminate over unbound features -- z3: satisfiable, e.g. heatGenCheck.efficiency = 0, heatGenCheck.power = 0 [W], heatGenCheckDuration = 1 [s])
holds() correctly refuses to answer: undecided, not proven either way: deliveredEnergyBoundedBySupply (companion-check-scratch/conservation_check_weakened.sysml:15:9)
The proof above is engineering evidence, not a passing test result: it is what would have to be re-checked if HeatGenerator’s bound or deliveredEnergy’s definition ever changed, by hand, since nothing in this toolchain checks that the restated copy stays in sync with either. Recording it as a ReviewRecord states, in a reader’s terms, what makes this evidence appropriate, sufficient and trustworthy (Hawkins et al. 2011, SS3.1-3.4), the same way earlier chapters recorded their own judgment sites.
Before AS-C08 states its claim, the next cells give it a real anchor in the model: a ReviewRecordRef tag naming deliveredEnergyBoundedBySupply as what AS-C08 is about.
# what is being claimed, and what, specifically, it is about
subject_ref = "ToasterDemo::deliveredEnergyBoundedBySupply"
AS_C08_TAG = """\
metadata asC08Tag : ReviewRecordRef about deliveredEnergyBoundedBySupply {
identifier = "AS-C08";
}
"""
print(AS_C08_TAG)metadata asC08Tag : ReviewRecordRef about deliveredEnergyBoundedBySupply {
identifier = "AS-C08";
}
This fragment is the same text now committed in models/ch08-cumulative.sysml onward, alongside the ReviewRecordRef definition (carried forward since Chapter 2); loading the cumulative model (the model.ok cell above, unchanged) is what makes this a real, checkable tag rather than an assertion the reader has to trust.
TOASTER_INCREMENT = AS_C08_TAGAS-C08 states the claim next: what the lemma establishes, and about which model element.
from toaster.evidence import ReviewRecord, hash_content, validate_record
claim = (
"deliveredEnergyBoundedBySupply, a hand-restated real-arithmetic lemma of the same "
"shape as HeatGenerator's efficiencyBounded constraint and deliveredEnergy's own "
"definition, holds for every value of efficiency in [0,1] and every non-negative "
"power and duration a companion restatement admits."
)
model_ref = "ToasterDemo::deliveredEnergyBoundedBySupply"
print(claim)deliveredEnergyBoundedBySupply, a hand-restated real-arithmetic lemma of the same shape as HeatGenerator's efficiencyBounded constraint and deliveredEnergy's own definition, holds for every value of efficiency in [0,1] and every non-negative power and duration a companion restatement admits.
What standard is this claim checked against? verify_holds() reports a single verdict for deliveredEnergyBoundedBySupply, proved by Z3 over the unbound features the companion restatement carries, not merely evaluated at one point.
scope = (
"The lemma governs the companion restatement's own heatGenCheck.efficiency, "
"heatGenCheck.power and heatGenCheckDuration features; it is not a solver-checked "
"reference to HeatGenerator's own efficiencyBounded or deliveredEnergy (DEFERRED.md "
"D-030, D-031), only a hand-restated copy of the same shape."
)
criteria = (
"verify_holds() reports a single satisfied verdict for deliveredEnergyBoundedBySupply, "
"with the reason text naming z3 (not propagation alone), proved for all values of the "
"unbound heatGenCheck.efficiency, heatGenCheck.power and heatGenCheckDuration features."
)
print(scope)
print(criteria)The lemma governs the companion restatement's own heatGenCheck.efficiency, heatGenCheck.power and heatGenCheckDuration features; it is not a solver-checked reference to HeatGenerator's own efficiencyBounded or deliveredEnergy (DEFERRED.md D-030, D-031), only a hand-restated copy of the same shape.
verify_holds() reports a single satisfied verdict for deliveredEnergyBoundedBySupply, with the reason text naming z3 (not propagation alone), proved for all values of the unbound heatGenCheck.efficiency, heatGenCheck.power and heatGenCheckDuration features.
What is this claim taking as given? The proof rests on the companion file restating deliveredEnergyBoundedBySupply correctly, and on the lemma’s own hypothesis (non-negative power and duration), which the model states here but does not enforce as a standing constraint on HeatGenerator or ApplyHeat elsewhere.
premises = []
assumption_refs = []
print("No prior ReviewRecord is assumed; this claim rests on the proof itself.")No prior ReviewRecord is assumed; this claim rests on the proof itself.
What supports the claim, and how? The Z3-derived verdict above, cited directly, is the evidence; the rationale states what kind of check produced it and why that is a materially different kind of evidence from a point evaluation, while being explicit about what it does not establish.
evidence_refs = [f"verify_holds: {pos_verdict.element} {pos_verdict.status} ({_ascii(pos_verdict.reason)})"]
rationale = (
"verify_holds() calls sysml-toolkit's real verify --solve command, which runs Z3 "
"over the unbound features of a companion restatement of this lemma (see the "
"narration above for why a companion file is used) and reports "
"deliveredEnergyBoundedBySupply as satisfied: proved for all values the restatement "
"admits, not read back from one entered value. This is a materially different kind of "
"evidence from an evaluate-only verdict: verify_satisfaction() could only ever check "
"a relation at whichever single power, duration and efficiency a candidate happens to "
"carry. It is also, deliberately, a narrower claim than 'this proves HeatGenerator's "
"own conservation property': see counterevidence."
)
print(evidence_refs)
print(rationale)['verify_holds: deliveredEnergyBoundedBySupply satisfied (z3: holds for all values of unbound features)']
verify_holds() calls sysml-toolkit's real verify --solve command, which runs Z3 over the unbound features of a companion restatement of this lemma (see the narration above for why a companion file is used) and reports deliveredEnergyBoundedBySupply as satisfied: proved for all values the restatement admits, not read back from one entered value. This is a materially different kind of evidence from an evaluate-only verdict: verify_satisfaction() could only ever check a relation at whichever single power, duration and efficiency a candidate happens to carry. It is also, deliberately, a narrower claim than 'this proves HeatGenerator's own conservation property': see counterevidence.
What could be wrong, and what is still open? A record that hides its own weak points is not more trustworthy, it is less checkable.
counterevidence = (
"This proof is NOT a solver-checked reference to HeatGenerator's own "
"efficiencyBounded constraint or deliveredEnergy calc: this toolchain's Z3 backend "
"does not compose two separately declared assert constraints, whether sibling or "
"inherited (DEFERRED.md D-030), and cannot reason through a chained calc invocation "
"like heatGenCheck.deliveredEnergy(...) (D-031). Confirmed directly: loosening "
"efficiencyBounded's own literal bound to <= 1.5, or doubling deliveredEnergy's own "
"definition by a factor of 2.0, in the real committed model changes neither the "
"original elements' own verdicts nor this lemma's verdict at all, because the lemma "
"restates its own copy of both rather than referencing either. The companion file "
"used by verify_holds also restates the construct rather than checking the "
"committed model directly, because toaster.modelcheck's own text parser does not "
"yet handle the extra annotation the CLI prints for assert satisfy declarations "
"(DEFERRED.md D-029)."
)
residual_uncertainties = (
"Whether efficiency, power and duration ever take values outside the bound in a "
"real candidate is not addressed by this proof; it establishes only that the "
"restated lemma respects conservation wherever its own bound is honored. If "
"HeatGenerator's own efficiencyBounded or deliveredEnergy is ever edited, this "
"record's content_hash (computed from the whole model file) does go stale, which "
"forces a re-review, but nothing automatically re-checks that the restated copy "
"still matches the edited original; that check would be manual. No physical heat "
"generator has been checked against this property; HeatGenerator remains an "
"abstract carrier with no concrete realization of its own."
)
print(counterevidence)
print(residual_uncertainties)This proof is NOT a solver-checked reference to HeatGenerator's own efficiencyBounded constraint or deliveredEnergy calc: this toolchain's Z3 backend does not compose two separately declared assert constraints, whether sibling or inherited (DEFERRED.md D-030), and cannot reason through a chained calc invocation like heatGenCheck.deliveredEnergy(...) (D-031). Confirmed directly: loosening efficiencyBounded's own literal bound to <= 1.5, or doubling deliveredEnergy's own definition by a factor of 2.0, in the real committed model changes neither the original elements' own verdicts nor this lemma's verdict at all, because the lemma restates its own copy of both rather than referencing either. The companion file used by verify_holds also restates the construct rather than checking the committed model directly, because toaster.modelcheck's own text parser does not yet handle the extra annotation the CLI prints for assert satisfy declarations (DEFERRED.md D-029).
Whether efficiency, power and duration ever take values outside the bound in a real candidate is not addressed by this proof; it establishes only that the restated lemma respects conservation wherever its own bound is honored. If HeatGenerator's own efficiencyBounded or deliveredEnergy is ever edited, this record's content_hash (computed from the whole model file) does go stale, which forces a re-review, but nothing automatically re-checks that the restated copy still matches the edited original; that check would be manual. No physical heat generator has been checked against this property; HeatGenerator remains an abstract carrier with no concrete realization of its own.
Assembling the record from the parts above, the same way a construction-zone cell assembles a model fragment from its own named pieces.
from toaster.query import get_review_record_refs
from toaster.judgment_store import save_record
record = ReviewRecord(
identifier="AS-C08",
kind="asserted_solution",
claim=claim,
subject_ref=subject_ref,
model_ref=model_ref,
content_hash=hash_content(source),
scope=scope,
criteria=criteria,
premises=premises,
assumption_refs=assumption_refs,
evidence_refs=evidence_refs,
rationale=rationale,
counterevidence=counterevidence,
residual_uncertainties=residual_uncertainties,
disposition="pending",
dependency_freshness="current",
engineering_conclusion="supported",
record_kind="worked_example",
)
errors = validate_record(record, model=model)
tag = next((t for t in get_review_record_refs(model) if t["identifier"] == record.identifier), None)
print(f"Model tag: {tag}")
print(f"Validation errors: {errors}")
assert errors == [], f"Validation errors: {errors}"
save_record(record)
print(f"Record valid: identifier={record.identifier!r} engineering_conclusion={record.engineering_conclusion!r}")
conn.close()Model tag: {'tag': 'ToasterDemo::asC08Tag', 'identifier': 'AS-C08', 'annotated_element': 'ToasterDemo::deliveredEnergyBoundedBySupply'}
Validation errors: []
Record valid: identifier='AS-C08' engineering_conclusion='supported'
The tag printed above is now part of the loaded model, and validate_record confirms the Python record and the model’s own tag agree about what AS-C08 is actually about. The claim printed above, the proof it points to, and the record’s own counterevidence stating plainly what that proof does and does not establish are three distinct things this notebook watched connect: a written lemma, a real solver’s verdict on it, and a record that never overstates what the verdict actually covers.
Try the chapter exercise in exercises/ch08/exercise.ipynb: it works through the same verify_satisfaction() and stale-detection pattern on its own, separate coffee-maker exercise model.