Food and cooking, read from a cook's source
This essay is not food-safety or medical advice. It describes published guidance as figures on a chart, rules over the facts a cook records and arithmetic with published conversions, and it reads a program that states them. Whether a food is safe to eat is decided by the cook, with a thermometer that reads the food, and whether an ingredient is safe for a person with an allergy is decided by that person and their doctor. A dish the program does not mark unsafe is not thereby safe, and a dish it does not mark as holding an allergen holds none only of the ingredients that were recorded. Nothing here is a diagnosis or a treatment. Ask a clinician about an allergy.
The minimum temperatures in this essay are read from FoodSafety.gov's chart, "Cook to a Safe Minimum Internal Temperature" (https://www.foodsafety.gov/food-safety-charts/safe-minimum-internal-temperatures), whose page reads "Date Last Reviewed November 21, 2024". They were read from a copy of that page downloaded on 2026-11-09, because an automated fetch of the agency's sites was refused. The chart rounds its Celsius figures, and where the essay says that a figure is converted exactly, that is what the program does with the chart's Fahrenheit ones.
In 1880 Karl Eberth first visualized Salmonella in the Peyer's patches and spleens of typhoid patients. Four years later Georg Gaffky grew the pathogen in pure culture, and the name Salmonella was not used until 1900, when Joseph Leon Lignières proposed it in honor of Daniel Elmer Salmon (1850 to 1914), an American veterinary surgeon. A cook's thermometer is much of what stands between a kitchen and such an organism: the same article says that Salmonella perishes after being heated to 55 degrees Celsius for 90 minutes or to 60 for 12, and that high-fat, high-liquid foods such as peanut butter can raise its heat resistance. The kitchen's practice is a table of the temperatures at which the time is short, and a rule about how long a dish may sit where a bacterium can grow.
The food library for DKE, the Deterministic Knowledge Engine, writes down some of what a cook is taught in DKE Python, and this essay teaches the subject by reading it. It is two halves that share nothing but the subject. The first records four kinds of fact: a standard, a minimum internal temperature named by a kind of food; a dish, with its kind, the temperature its core reached and the minutes it spent in the danger zone; an item, an ingredient with the allergens it holds; and a line, which puts an item in a dish. The library concludes, for each dish, whether it is undercooked, held too long, unsafe or unknown, and which of nine allergens it holds. The second half calculates what a cook calculates: Fahrenheit to Celsius, a recipe scaled by servings, a baker's percentage, a volume in cups as a mass, food energy and kilocalories as kilojoules. Every program below runs as shown once the library's source file has been compiled into a store, and every output is copied from a run. The programs name their own subjects and print only those, so that a store which already holds other dishes gives the same lines.
1. Two scales and a chart
The chart is in Fahrenheit and the library is in Celsius, and the two scales have a history. The Fahrenheit scale is based on one proposed in 1724 by Daniel Gabriel Fahrenheit (1686 to 1736), on which water freezes at 32 degrees and boils at 212, at standard atmospheric pressure, 180 degrees apart. The Celsius scale was developed 18 years later, and the freezing and boiling points of water were originally defined to be 100 degrees apart on it. The conversion is subtract 32 and multiply by five ninths, and the library does it in that order:
c = (fahrenheit - 32.0) * 5.0 / 9.0Only for a temperature so far beyond any oven that five times it would pass the largest number a real can hold does it divide by nine first.
The chart's rows give the minimum internal temperature of a food: beef, bison, veal, goat and lamb steaks, roasts and chops 145 degrees Fahrenheit (63 Celsius) with a rest time of 3 minutes, and those meats ground, and sausage, 160 (71); poultry of every kind, whole or in parts or ground, 165 (74); pork steaks, roasts and chops 145 with a rest of 3 minutes and pork ground or sausage 160; raw ham 145 with a rest of 3 minutes and precooked ham to reheat 165, with the note that cooked hams packaged in USDA-inspected plants are reheated to 140 (60); egg dishes such as a frittata or a quiche 160; casseroles, meat and meatless, 165, and casseroles containing meat and poultry 165; leftovers of any type 165; rabbit and venison 160; and fish, whole or fillet, 145 or, in the chart's words, cook until the flesh is no longer translucent and separates easily with a fork. The library's chart() writes eleven standards of these rows, under kinds it names: steak for the steaks, roasts and chops, ground, poultry, egg-dish, casserole for a casserole with meat or poultry, or meatless, ham-raw, ham-precooked for precooked ham reheated, ham-packaged for the chart's note, a cooked ham packaged in a USDA-inspected plant and reheated, leftovers, rabbit-venison and fish.
import food
for f in [145.0, 160.0, 165.0, 32.0, 212.0, -40.0]:
print(f, food.fahrenheit_to_celsius(f))145.0 62.77777777777778
160.0 71.11111111111111
165.0 73.88888888888889
32.0 0.0
212.0 100.0
-40.0 -40.0The chart prints 63 for 145 degrees, and the exact figure is 62.77777777777778. The library keeps the exact one. The difference of a quarter of a degree is the difference between a rule that a reader can reproduce and one that depends on how a table was rounded, and Section 2 shows a dish that falls in it. At 32 the scale gives 0, at 212 it gives 100, and at minus 40 the two scales meet.
2. A dish is judged by its standard
A dish is judged against the standard that its kind names. The rule is a join of the dish with the standard, and the dish is undercooked when its core is below the standard's minimum:
@rule
def undercooked_dish():
for d in Dish, s in Standard:
if d.kind == s and d.core < s.minimum:
d.undercooked = TrueA dish is unsafe for either of two causes, undercooked or held too long, and each has a rule of its own:
@rule
def unsafe_for_cooking():
for d in Dish:
if d.undercooked == True:
d.unsafe = TrueThe second cause is time. The danger zone is the range of temperatures in which bacteria grow, and agencies define it differently. The United States Department of Agriculture and its Food Safety and Inspection Service define it as roughly 4 to 60 degrees Celsius (40 to 140 Fahrenheit), and the article says that food remaining in the zone for more than two hours should not be consumed; in the United Kingdom and Northern Ireland the zone is 8 to 63 degrees (46 to 145). The article's own opening gives yet another range, 4 to 63, and says that food safety agencies define the danger zone differently. It adds that foodborne microorganisms grow much faster at 21 to 47 degrees (70 to 117 Fahrenheit). The library holds both of the agencies' ranges and names them; the limit of the rule is the two hours:
@rule
def held_too_long_dish():
for d in Dish:
if d.minutes > 120.0:
d.held_too_long = TrueThe program records six dishes. Supper is a poultry dish whose core reached 70 degrees, under the 73.89 of the chart's 165. The roast is a steak that reached the chart's printed 63. The quiche is an egg dish at the chart's printed 71, and the stew and the pie are casseroles held for 150 and for 120 minutes. The dinner is recorded with the kind trout. The chart's row for fish names trout among its examples, and the library's kind for the row is fish, so no standard is recorded under the name trout:
import food
ours = ["supper", "roast", "quiche", "stew", "pie", "dinner"]
food.chart()
food.dish("supper", "poultry", 70.0, 30.0)
food.dish("roast", "steak", 63.0, 30.0)
food.dish("quiche", "egg-dish", 71.0, 30.0)
food.dish("stew", "casserole", 80.0, 150.0)
food.dish("pie", "casserole", 80.0, 120.0)
food.dish("dinner", "trout", 70.0, 30.0)
print([n for n in food.unsafe_dishes() if n in ours], [n for n in food.unknown_dishes() if n in ours])
a = why_not(food.Dish.roast.undercooked)
print(a)
b = why_not(food.Dish.pie.held_too_long)
print(b)
c = why_not(food.Dish.dinner.undercooked)
print(c)
food.dish("supper", "poultry", 75.0, 30.0)
print([n for n in food.unsafe_dishes() if n in ours])['quiche', 'stew', 'supper'] ['dinner']
rule `food.undercooked_dish` needs food.Dish[roast].core < food.Standard[steak].minimum, and food.Dish[roast].core is 63.0 (real)
rule `food.held_too_long_dish` needs food.Dish[pie].minutes > 120.0, and food.Dish[pie].minutes is 120.0 (real)
rule `food.undercooked_dish` needs food.Standard[trout].minimum, and nothing is recorded there
['quiche', 'stew']Supper, the quiche and the stew are unsafe. The roast is not, because 63 is not below 62.78, though a reader of the chart would find the two equal. The quiche is, for the opposite reason: 71 is below the 71.11 of 160 degrees Fahrenheit, and it is the same rounding seen from the other side. A cook who reads the printed 71 as a pass should read the exact figure, and a cook who has a thermometer that reads to a degree should take the next one up. The pie spent exactly 120 minutes in the zone and is not held too long: the rule says more than two hours, and why_not says that the minutes are 120.0. The last pair is a correction: supper is written again at 75 degrees, and the mark that rested on the old figure is withdrawn.
The chart's note about ham is a kind of its own. A casserole is one kind whether it holds meat or none, 165 degrees Fahrenheit, and a cooked ham packaged in a USDA-inspected plant is reheated to 140, the 60 Celsius of the chart, where other precooked ham to reheat is 165. A ham whose core reached 62 degrees is above the one and below the other, and the kind the cook writes decides the verdict:
import food
ours = ["lasagne", "bake", "hampack", "hamplain"]
food.chart()
food.dish("lasagne", "casserole", 70.0, 30.0)
food.dish("bake", "casserole", 74.0, 30.0)
food.dish("hampack", "ham-packaged", 62.0, 30.0)
food.dish("hamplain", "ham-precooked", 62.0, 30.0)
print([n for n in food.unsafe_dishes() if n in ours])
a = why_not(food.Dish.hampack.undercooked)
print(a)['hamplain', 'lasagne']
rule `food.undercooked_dish` needs food.Dish[hampack].core < food.Standard[ham-packaged].minimum, and food.Dish[hampack].core is 62.0 (real)The meatless lasagne at 70 is under the casserole's 73.89 and the bake at 74 is not. The packaged ham is not under its 60, and why_not names the comparison that failed, while the same core under the kind ham-precooked is under 73.89 and is unsafe. The program does not know which ham is in the oven; the cook says so by the kind.
3. Unknown is not safe
The dinner is neither in the list of the unsafe nor, in any useful sense, safe. Nothing is recorded for the kind trout, so the rule has no standard to judge it by, and the why_not of the undercooked says that nothing is recorded at the standard. The library keeps a conclusion for that, unknown, and says in plain words that an empty unsafe means that no rule concluded it. A cook who writes fish gets the chart's 145 degrees; one who writes trout gets no verdict, and the library says so. The rule that concludes the unknown is the interesting one:
@rule
def unknown_kind():
for d in Dish:
if none(s in Standard: d.kind == s and s.minimum > -1000.0):
d.unknown = TrueThe test that a kind has a standard is written as a join with a read of the standard's own field. The language says why. A join with == ranges the subject on its right, over the subjects the field names, whether or not anything of its kind is recorded for them (Specification, section 14.2). Written alone, d.kind == s would range a standard over the subject that the kind names even when no standard of that name has been recorded, and the test for a kind with no standard would never find a kind missing. The second premise, s.minimum > -1000.0, reads the standard's own minimum, and a read holds only where a value is recorded. No real minimum temperature is below minus 1000 degrees, so the comparison is true for every standard that exists and false for a name nothing was recorded for. That is why the rule is written with it.
The chart leaves out its rows that give no temperature, and so does the library: raw eggs are cooked until the yolk and the white are firm, shrimp, lobster, crab and scallops until the flesh is pearly or white and opaque, and clams, oysters and mussels until the shells open. The rest time of three minutes for a steak, a roast or a chop is a second fact beside the core temperature, and is not a rule here; the chart says that during the rest the temperature of the food continues to rise and the heat is evenly distributed. A note on the chart, that cooked hams packaged in USDA-inspected plants are reheated to 140 degrees Fahrenheit (60 Celsius), is not in the library either, whose ham-precooked carries the row's 165. And the chart's own advice is about the instrument: a properly calibrated instant-read thermometer, in the center of the thickest part of the food, and in two or three places. A temperature the cook records is only as good as that reading.
4. What a dish holds
An allergy is a different kind of risk, and the library treats it as the other kind of fact. A food allergy is an abnormal immune response to food, and many are caused by hypersensitivities to particular proteins in different foods. In the developed world about 4% to 8% of people have at least one food allergy, and an estimated 15 million people have food allergies in the United States. Reactions vary in severity from mild symptoms involving hives and lip swelling to severe ones, and symptoms typically appear from within a few minutes to a few hours after eating the food. The law names the foods. The Food Allergen Labeling and Consumer Protection Act of 2004 identified eight foods as major food allergens, milk, eggs, fish, crustacean shellfish, tree nuts, peanuts, wheat and soybeans, which at the time of its passage accounted for 90 percent of food allergies and serious allergic reactions in the United States. On April 23, 2021, the FASTER Act was signed into law, and sesame became the ninth, effective on January 1, 2023. The law requires that food labels identify the food source of all major food allergens used to make the food, and the page of the Food and Drug Administration says that it has not established a threshold level for any allergen.
The library has one rule for each of the nine, and they are all the same rule. A dish holds an allergen when a line of it uses an item that holds it:
@rule
def holds_milk():
for d in Dish, l in Line, i in Item:
if l.dish == d and l.item == i and i.milk == True:
d.milk = Trueitem() writes all nine facts of an ingredient, true for the allergens you name and false for the rest. So an item that you described is free from what you did not name, and an item that you never described is unknown, not free from anything. A sponge has four described ingredients, a flour with wheat, a butter with milk, an egg, and a sugar with none, and a line for a fifth, the mystery, that nobody described:
import food
food.item("plain_flour", ["wheat"])
food.item("dairy_butter", ["milk"])
food.item("hen_egg", ["egg"])
food.item("cane_sugar", [])
food.line("sp1", "sponge", "plain_flour")
food.line("sp2", "sponge", "dairy_butter")
food.line("sp3", "sponge", "hen_egg")
food.line("sp4", "sponge", "cane_sugar")
food.line("sp5", "sponge", "mystery")
print(food.allergens_of("sponge"))
food.item("soy_spread", ["soybeans"])
food.line("sp2", "sponge", "soy_spread")
print(food.allergens_of("sponge"))['milk', 'egg', 'wheat']
['egg', 'wheat', 'soybeans']The sponge holds milk, egg and wheat, in the order of the nine, and says nothing of the mystery, which holds nothing the rules can see. This is the library's one honest limit in this half: a dish holds an allergen only of the ingredients that were recorded. The last pair replaces the butter in the second line by a spread of soy, and the milk is withdrawn with the line that brought it; the soybeans are added. A name that is not one of the nine refuses.
5. When two sources disagree
Two probes can say different things about one core, and two suppliers about one ingredient. The library keeps both readings and chooses neither. A reading is written under a second source, beside the first, and every value in force gives its own verdict. unsettled() names every fact you write that two sources give different values. In food, the effect is to err toward unsafe: a rule fires on whichever value meets it, so a dish is unsafe if either value makes it so, and a dish holds an allergen if either reading of an item says that it does.
import food
ours = ["risky", "odd"]
food.dish("risky", "poultry", 80.0, 30.0)
food.dish("odd", "trout", 70.0, 30.0)
print([n for n in food.unsafe_dishes() if n in ours], [n for n in food.unknown_dishes() if n in ours], [s for s in food.unsettled() if s.split(" ")[1] in ours])
update(food.Dish.risky.core, 60.0, "second probe")
update(food.Dish.odd.kind, "poultry", "second label")
print([n for n in food.unsafe_dishes() if n in ours], [n for n in food.unknown_dishes() if n in ours], [s for s in food.unsettled() if s.split(" ")[1] in ours])
update(food.Item.cane_sugar.peanuts, True, "supplier B")
print(food.allergens_of("sponge"), [s for s in food.unsettled() if s.split(" ")[1] in ["cane_sugar"]])[] ['odd'] []
['odd', 'risky'] [] ['dish odd kind', 'dish risky core']
['egg', 'peanuts', 'wheat', 'soybeans'] ['item cane_sugar peanuts']At first the risky poultry, at 80 degrees, is safe by the chart, and the odd dish is only unknown, because nothing is recorded for trout. With a second probe the same poultry reads 60 as well and is unsafe, and unsettled() names dish risky core, so that the reader sees that the unsafe mark rests on one of two readings. The odd dish is given a second label, poultry, and its core of 70 is under the 73.89 of that standard, so it is unsafe too; and it is no longer unknown, because one of its kinds has a standard. Last, a second supplier says that the sugar holds peanuts, and the sponge is shown to hold peanuts as well as egg, wheat and soybeans, with the fact named. No read refuses because sources disagree. A reader who sees the name of the fact knows which source to ask.
6. The arithmetic of a recipe
A recipe is a ratio, and the rest of the library is the arithmetic of ratios. Scaling by servings multiplies a quantity by the new number over the old. A baker's percentage expresses each ingredient's weight as a percentage of the flour's weight, flour always 100 percent, so that the sum of the percentages always exceeds 100. The water's percentage is called the hydration. A recipe with 10 pounds of flour and 5 of water is 100 and 50 percent, and the page offers a common formulation for bread of 100% flour, 60% water, 1% yeast, 2% salt and 1% oil, lard or butter, with hydration of 50 to 57 percent typical for bagels and pretzels and 58 to 65 percent for breads and rolls. The cup is a volume, and the US customary cup is half of a US liquid pint, 236.5882365 millilitres exactly; the cup used in the United States for nutrition labelling is defined in law as 240 millilitres, and Australia, Canada and New Zealand use a metric cup of 250. The page also notes that in Europe recipes normally weigh non-liquid ingredients in grams rather than measuring volume, and a density is what turns a volume into a mass. The library holds no density and takes yours.
v = quantity * to_servings / from_servingsv = 100.0 * part / flourv = cups * 236.5882365 * density_grams_per_mlEach returns that v wherever it is an ordinary real, which is wherever a kitchen will ever take it. Past that, where the answer would pass the largest real or fall below the smallest, the same arithmetic is done on the mantissas and the powers of two apart, so a quantity still scales.
import food
print(food.scale_by_servings(250.0, 4.0, 6.0))
for grams in [500.0, 300.0, 5.0, 10.0, 5.0]:
print(food.bakers_percentage(grams, 500.0))
print(food.volume_to_mass(1.0, 1.0), food.volume_to_mass(2.0, 0.53))375.0
100.0
60.0
1.0
2.0
1.0
236.5882365 250.78353069A quantity of 250 for four servings is 375 for six. A dough of 500 grams of flour with 300 of water, 5 of yeast, 10 of salt and 5 of fat is the page's formulation: 100, 60, 1, 2 and 1 percent, and 164 in all, more than 100 as it always is. A cup of a liquid with a density of 1.0 is 236.59 grams, and two cups of something with an example density of 0.53 are 250.78. The density here is a number made up for the example; the library holds none.
Food energy has a history too. The system of factors for the calculation of the available energy of foods is named for Wilbur Olin Atwater, whose studies at Wesleyan University in Middletown, Connecticut, developed it in the later part of the 19th century and the early years of the 20th. The energy of a food was first measured by burning a dried sample in a bomb calorimeter and measuring the temperature change in the water around it, and the heat of combustion of the components, protein, fat and carbohydrate, was corrected by Atwater for what the body does not absorb, using apparent digestibility coefficients and an estimate of the losses in urine. It became common practice to calculate the energy content of foods using 4 kilocalories a gram for carbohydrates and proteins and 9 for lipids. Annabel Merrill and Bernice Watt of the USDA later improved the system with food-specific factors, in a publication of 1973. The labelling table the library uses gives fat 9 kilocalories a gram (37 kilojoules), ethanol 7 (29), protein 4 (17), carbohydrate 4 (17) and fibre 2 (8), though fibre is not counted by the UK regulations, and the library uses protein, carbohydrate, fat and alcohol only. A food calorie is a kilocalorie and is equivalent to 4.184 kilojoules.
return 4.0 * protein_g + 4.0 * carbohydrate_g + 9.0 * fat_g + 7.0 * alcohol_gimport food
e = food.food_energy(10.0, 50.0, 5.0, 0.0)
print(e, food.kilocalories_to_kilojoules(e))
print(food.food_energy(0.0, 0.0, 0.0, 20.0), food.kilocalories_to_kilojoules(100.0))285.0 1192.44
140.0 418.40000000000003A portion of 10 grams of protein, 50 of carbohydrate and 5 of fat is 285 kilocalories, or 1192.44 kilojoules, and 20 grams of alcohol alone are 140. These are labelling factors, and the page says that direct calorimetry generally overestimates the energy that the body can obtain from food. A flour of zero refuses a baker's percentage with a division by zero.
7. What the library leaves out
The library leaves out most of cooking. It has no cooling curve, no nutrients beyond the four, no cooking losses and no yields, and the rows of the chart without a temperature and the rest time of a steak are not rules. A temperature is a figure from a chart, converted exactly, and the reading of a food's core is the cook's. A dish that is not marked unsafe is not thereby safe, and a dish that is not marked as holding an allergen holds none only of the ingredients that were recorded. The library cannot know that a recorded ingredient is described correctly, or that a label is true, and the first thing a person with an allergy does is ask. It does not know which danger zone you used when you wrote the minutes: the rule reads the minutes and counts the same two hours for both.
Closing
A kitchen's safety is a record of what was learned from illness: a bacterium seen in 1880 and named in 1900, a chart of temperatures at which the time is short, a law that names nine foods on a label. The library keeps these procedures where a reader can run them and ask about them. It converts the chart's figures exactly and shows the dish that falls between the printed figure and the exact one, calls a kind with no standard unknown and never safe, holds two readings of a temperature or an ingredient side by side and errs toward unsafe, and names the fact they disagree about. And because each conclusion names the facts it rests on, a core corrected in one place is corrected in every verdict that rested on it.