Cookwala / SDK and scenarios / 020

Check a simmer trace against its envelope: inside, left it, missed the target

An envelope is the safe band for the medium. The check tolerates the warm-up, then requires every reading to stay in the band and, once the recipe target is reached, near it.

Audience: developer · Goal: Robots for people · Level: beginner

Read first: /docs/CORE/ /docs/QUICKSTART/

Steps

  1. Warm-up to 88, then 92 and 94 degC with a target of 94 plus or minus 3: inside the band, target held. checkEnvelope

  2. The same pot reaches 99 degC: that is a boil, not a simmer. checkEnvelope

  3. Inside the band but it drifted away from the target after reaching it. checkEnvelope

  4. The band itself, from the vocabulary. getOps

Outcome

You can tell an envelope violation from a missed target, and you know the warm-up is not a violation. Next: scenario 021 moves the boiling band with altitude.

Command line

printf '[{"t": 0, "tempC": 60}, {"t": 30, "tempC": 88}, {"t": 60, "tempC": 92}, {"t": 90, "tempC": 94}]' > trace.json && cookwala envelope cw.op.simmer trace.json --target 94 --tolerance 3
printf '[{"t": 0, "tempC": 60}, {"t": 30, "tempC": 90}, {"t": 60, "tempC": 94}, {"t": 90, "tempC": 99}]' > trace.json && cookwala envelope cw.op.simmer trace.json --target 94 --tolerance 3

Recorded output

Status: ok

[1] checkEnvelope: {"envelopeOk": true, "targetOk": true, "reason": "ok"}
[2] checkEnvelope: {"envelopeOk": false, "targetOk": false, "reason": "left_envelope"}
[3] checkEnvelope: {"envelopeOk": true, "targetOk": false, "reason": "missed_target"}
[4] getOps: {"name": "ops", "version": "0.2.0", "entries": [{"id": "cw.op.dispense", "label": {"en": "Dispense / measure", "ar": "قياس وإضافة"}, "definition": "Measure an ingredient by mass, volume or count and add it to a target.", "classes": ["cw.opfamily.handle"], "paramsSchema": {"type": "object", "properties": {"precision": {"type": "number"}}}, "sensors": ["cw.sense.mass"], "links": {"ieee1872": "IEEE 1872.1-2024 Task (to be mapped)"}}, {"id": "cw.op.transfer", "label": {"en": "Transfer", "ar": "نقل"}, "definition": "Move food or a vessel's contents from one place to another.", "classes": ["cw.opfam…

Code

Run: python scenarios/out/020-simmer-trace-inside-and-outside/python.py

# Scenario 020: Check a simmer trace against its envelope: inside, left it, missed the target
# An envelope is the safe band for the medium. The check tolerates the warm-up, then requires every reading to stay in the band and, once the recipe target is reached, near it.
# Run a hub first: python hub/cookwala_hub.py --recipes examples
import json, os
from cookwala.client import CookwalaClient, CookwalaProblem

def load(path):
    with open(path, encoding="utf-8") as f:
        return json.load(f)

def pick(obj, path):
    for part in path.replace("]", "").replace("[", ".").split("."):
        obj = obj[int(part)] if part.isdigit() else (len(obj) if part == "length" else obj.get(part))
    return obj

c = CookwalaClient(os.environ.get("COOKWALA_HUB", "http://localhost:7878"))


# Step 1: Warm-up to 88, then 92 and 94 degC with a target of 94 plus or minus 3: inside the band, target held.
ok = c.check_envelope("cw.op.simmer", [{"t": 0, "tempC": 60}, {"t": 30, "tempC": 88}, {"t": 60, "tempC": 92}, {"t": 90, "tempC": 94}], {"value": 94, "tolerance": 3})
assert pick(ok, "envelopeOk") == True, pick(ok, "envelopeOk")
print("ok", "envelopeOk", "=", True)
assert pick(ok, "targetOk") == True, pick(ok, "targetOk")
print("ok", "targetOk", "=", True)
assert pick(ok, "reason") == "ok", pick(ok, "reason")
print("ok", "reason", "=", "ok")

# Step 2: The same pot reaches 99 degC: that is a boil, not a simmer.
left = c.check_envelope("cw.op.simmer", [{"t": 0, "tempC": 60}, {"t": 30, "tempC": 90}, {"t": 60, "tempC": 94}, {"t": 90, "tempC": 99}], {"value": 94, "tolerance": 3})
assert pick(left, "envelopeOk") == False, pick(left, "envelopeOk")
print("ok", "envelopeOk", "=", False)
assert pick(left, "targetOk") == False, pick(left, "targetOk")
print("ok", "targetOk", "=", False)
assert pick(left, "reason") == "left_envelope", pick(left, "reason")
print("ok", "reason", "=", "left_envelope")

# Step 3: Inside the band but it drifted away from the target after reaching it.
missed = c.check_envelope("cw.op.simmer", [{"t": 0, "tempC": 86}, {"t": 30, "tempC": 94}, {"t": 60, "tempC": 88}], {"value": 94, "tolerance": 3})
assert pick(missed, "envelopeOk") == True, pick(missed, "envelopeOk")
print("ok", "envelopeOk", "=", True)
assert pick(missed, "targetOk") == False, pick(missed, "targetOk")
print("ok", "targetOk", "=", False)
assert pick(missed, "reason") == "missed_target", pick(missed, "reason")
print("ok", "reason", "=", "missed_target")

# Step 4: The band itself, from the vocabulary.
ops = c.get_ops()
assert pick(ops, "entries[32].id") == "cw.op.simmer", pick(ops, "entries[32].id")
print("ok", "entries[32].id", "=", "cw.op.simmer")
assert pick(ops, "entries[32].envelope.medium") == "water", pick(ops, "entries[32].envelope.medium")
print("ok", "entries[32].envelope.medium", "=", "water")
assert pick(ops, "entries[32].envelope.tempC.min") == 85, pick(ops, "entries[32].envelope.tempC.min")
print("ok", "entries[32].envelope.tempC.min", "=", 85)
assert pick(ops, "entries[32].envelope.tempC.max") == 96, pick(ops, "entries[32].envelope.tempC.max")
print("ok", "entries[32].envelope.tempC.max", "=", 96)

print("scenario complete")

scenarios/020-simmer-trace-inside-and-outside.json

Previous: 019 Next: 021