◆Kitchen Twin Wiki

Engineering wiki

A three-star kitchen, built like a factory twin

This site is a digital twin of The French Laundry's kitchen in Yountville, California, running a full dinner service. It is modelled the way a manufacturing plant is: covers booked are the build plan, every recipe is a bill of materials with a routing, stations are work centers, and a discrete-event simulation drives the brigade, the machines and the stock you see in 3D. Everything runs in your browser from a static GitHub Pages site.

The numbers below are read live from the same /data files the twin runs on.

–courses on the tasting menu
–recipes ( sub-assemblies, plates)
–raw ingredients
–bill-of-materials lines
–routing operations
–pieces of equipment
–work centers (stations)
–brigade members in roles
–cited publications
–of records cite a publication

Architecture

The system is a pipeline with one source of truth. Sourced JSON in /data is turned into a production plan and into the simulation's input; the simulation owns the clock; the 3D scene, the dashboard and Studio only read its state and send it commands.

Data flows from /data into the MRP plan and the simulation adapter; the simulation drives the 3D scene and the dashboard; Studio compiles edits into simulation input and lighting. layout, equipment, storage and ingredients → geometry bindSim base model KitchenData + cfg look: sun, sky, mood, dining room /data 9 JSON files every record cited or est model/mrp.js BOM explosion, load sim/adapter.js routings → task chains panel.js production plan drawer sim/engine.js discrete-event job shop sim/sim.js clock, seek, events, forecast scene/ (Three.js) photoreal twin dashboard/ live operations studio/compile.js edits → sim input
The three pages share these modules. index.html wires all of them; studio.html adds the Studio layer; dashboard.html mounts the dashboard on its own.
Sourced dataPlanning and simulationViewsStudio

The frame loop

The simulation owns time. Each animation frame advances it by the wall-clock delta times the chosen speed, then the scene and the dashboard mirror the new state. Because every view reads the same state, the 3D cooks, the Gantt chart and the HUD never disagree.

Per frame: requestAnimationFrame, sim.tick, engine.advance, binding.update, dashboard.update, render. twin.onFrame(dt)requestAnimationFrame sim.tick(dt)dt × speed, 1× to 300× engine.advancepop events, dispatch binding.updatemirror state at 6 Hz renderHUD 4 Hz, drawer 10 Hz
From src/main.js. Cooks and plates are tweened between syncs, so motion stays smooth at 60 fps while the scene only re-reads the simulation six times a second.

Stack

The twin is deliberately small: one runtime dependency, no framework, no backend and no binary assets.

LayerChoiceWhy
LanguageModern JavaScript (ES2022 modules), CSSThe same simulation and planning code runs in the browser and in Node without a compile step.
3D renderingThree.js 0.186 (WebGL 2), with its post-processing addons: GTAO, Unreal bloom, bokeh depth of field, FXAA, output passPhysically based materials, real-time shadows and a mature post-processing chain, in the browser.
UIPlain DOM and Canvas 2D charts (src/dashboard/charts.js)No framework to download; charts draw thousands of Gantt bars cheaply on a canvas.
SimulationHand-written discrete-event engine with a binary-heap event queue and a seeded PRNG (mulberry32)Deterministic, dependency-free and fast enough to run a whole night in about a tenth of a second.
BuildVite 8, multi-page build (four HTML entries)Bundles JSON imports, splits shared chunks, and serves the dev site with hot reload.
TestsNode 22's built-in test runner (node --test)No test framework needed; the engine and planner are pure modules.
CI and hostingGitHub Actions; GitHub Pages via actions/deploy-pagesEvery pull request is validated, tested and built; every merge to main publishes.
InterchangeOpenUSD (.usda) written in the browserOpens in usdview, NVIDIA Omniverse and Isaac Sim with prims named by the data ids.

Repository layout

Each top-level folder under src/ was owned by one workstream and talks to the others only through the data ids and small APIs.

michelin-kitchen-twin/ ├── index.html # the 3D twin with HUD, dock and operations drawer ├── studio.html # Studio: dining room, menu, season, light and mood ├── dashboard.html # full-screen operations dashboard ├── wiki.html # this page ├── vite.config.js # multi-page build, Pages base path /michelin-kitchen-twin/ ├── data/ # sourced master data, the single source of truth │ ├── restaurant.json # plant: kitchen, annex, garden, seats, design team │ ├── menu.json # tasting menu: courses, choices, supplements, eating times │ ├── service.json # build plan: covers, seatings, pacing, variability, what-ifs, KPIs │ ├── items.json # raw materials: yield, storage, shelf life, price, seasons │ ├── products.json # sub-assemblies and plates, each with a BOM and a routing │ ├── resources.json # work centers, equipment, storage, roles, shifts, staff │ ├── layout.json # floor plan: zones, equipment placements, benches │ ├── terms.json # kitchen ↔ factory vocabulary │ └── sources.json # bibliography cited by every record ├── src/ │ ├── main.js # wires sim + scene + dashboard + plan for index.html │ ├── data.js # bundles /data for the browser │ ├── panel.js # production plan drawer and equipment details │ ├── model/ # mrp.js (BOM explosion, load), validate.js │ ├── sim/ # engine.js, sim.js, adapter.js, mrp.js, scenarios.js, rng.js, data/, │ │ # optimize.js, precomputed.js, precomputed.data.js │ ├── scene/ # Three.js twin: index, bind, quality, room, equipment, appliances, │ │ # cooks, items, ingredients, materials, nav, plan, sky, tour, clock, usd │ ├── dashboard/ # dashboard.js, plan.js, optimizer.js, charts.js, terms.js, page.js │ ├── studio/ # catalog, config, compile, look, stage, ui, main │ └── wiki/ # this page's script and styles ├── scripts/ # validate, plan, sim-report, precompute (Node CLIs over the same modules) ├── tests/ # model, sim, studio and optimizer tests (node --test) ├── docs/ # data model, simulation, Studio, NVIDIA path, PRD and PR-FAQ └── .github/workflows/ # ci.yml (PRs and main), pages.yml (deploy)

The research story

Nobody handed over blueprints. The French Laundry does not publish its floor plan, its brigade or its prep lists. Everything in this twin was pieced together from what the restaurant, its architects, the trade press, its cookbook and its diners have put in public, and each fact is tagged with where it came from.

Why this kitchen

The brief was a three-star kitchen, as realistic as possible. That made the choice a research question: which three-star kitchen has the most published detail? The French Laundry won clearly. Its 2014 to 2017 rebuild was covered by the architects (Snøhetta), by Architectural Record, and by Foodservice Equipment & Supplies, whose article lists the full equipment schedule down to the make of the slicer. Thomas Keller's 1999 cookbook gives exact quantities for the signature dishes. And diners have written course-by-course accounts of the tasting menu, with timings.

Five kinds of source, five parts of the twin

SourceWhat it gave the twinWhere it lives
Architecture press
Architectural Record, Snøhetta
The building: the vaulted ventilated ceiling, ribbon window, terrazzo floor, white surfaces, the annex's butchery and produce roomsThe plant, layout and 3D scene
Trade press
FE&S "Clean Slate"
Kitchen and annex areas, the two Bonnet island suites, every named appliance, and the decision to have no walk-in coolersEquipment, storage, work centers
The cookbook, via reproductions
The French Laundry Cookbook (1999)
Exact quantities, temperatures and times for four dishesBills of materials and routings
Diner reviews
The Chihuo, Tiny Urban Kitchen
The course sequence, supplements, how long the meal lasts, and a headcount of the kitchen at serviceMenu, pacing, brigade size
Journalism and reference
Press Democrat, AP, Wikipedia, Tasting Table
Seats, stars, price, the garden's share of the produce, the cellarRestaurant facts, demand

Things we learned along the way

There are no walk-in coolersAlmost every restaurant kitchen has one. This one replaced them with reach-ins throughout, some custom-built 102 inches tall. So the twin stores stock in reach-ins at each station, not in a cold room.FE&S
There are no hoods eitherA curved, skylit, ventilated ceiling about 20 ft high at the peak, dropping to 9 ft over the cooking suites, does the job. It is the vault you see in the 3D view.FE&S, Architectural Record
The light is tuned to the dining roomThe kitchen's LED colour temperature matches the dining room's, so a plate looks the same at the pass as on the table.Architectural Record
A fryer that isn't on the listCoffee and Doughnuts needs doughnuts fried at 325°F, but the equipment schedule names no fryer. The twin fries them in a pot on the pastry suite's induction hob, and says so.Food Network recipe, FE&S
The garden across the streetThe restaurant's own culinary garden supplies about 30% of its vegetables, and diver scallops arrive within 24 hours of harvest.AP, 2012
Sources disagree, and that's recordedThe trade press says 62 seats after the rebuild; Wikipedia and Tasting Table say 60. The cellar holds 16,000 bottles, or 15,000 according to Snøhetta. The twin picks one and keeps the other in a note.restaurant.json
Some dishes start three days earlyOnce the recipes were in, the planner showed corned veal tongue needs about 76 hours from brining to plate, and foie gras au torchon about 49. That is why real kitchens make them days ahead.Computed by the planner
"At least a dozen" cooksThe restaurant has over 100 staff but publishes no kitchen roster. One diner counted at least a dozen people in the kitchen during service, so the twin's brigade is 15: 13 cooks and 2 dishwashers.Tiny Urban Kitchen, estimate

From an article to a moving kitchen

  1. Read and extract. Each source was read for facts the model could use: a dimension, an appliance, a quantity, a time. Each fact became a field in a JSON file with a src list naming where it came from.
  2. Translate into factory terms. Recipes became bills of materials, method steps became routings with a station, appliance, cook and time, and the brigade became a labor pool with skills. See Data and sources.
  3. Fill the gaps honestly. Where nothing was published (portions for dishes without a recipe, most operation times, positions on the floor, ingredient prices), a typical professional value went in, tagged est.
  4. Check it. A validator checks every cross-reference, and a test checks that planning four servings of Oysters and Pearls reproduces the cookbook's quantities.
  5. Run it. The planner explodes covers into grams and minutes, the simulation plays the night, and the 3D scene draws what the simulation is doing.

The research, the data model and the planner were built by one Claude session in the first hours of the project, and handed to the 3D, simulation and Studio sessions as a shared contract of ids. See How it was built.

Sourced versus estimated

Counted live from /data. A record is "sourced" when it cites a publication, "estimated" when its only tag is est; many cite a publication for some fields and estimate others.

The main estimates are the brigade size and shifts, where equipment stands, portions for courses with no published recipe, and ingredient prices. The full list is in Limits and estimates, and the bibliography with how often each source is cited is under Data and sources.

Data and sources

The French Laundry was chosen because it is the three-star kitchen with the most published detail: a 2017 rebuild documented by its architects and by the trade press, a published equipment schedule, a cookbook, and course-by-course reviews. Every record in /data has a src list that points into sources.json, or est where it is a modelling estimate.

Kitchen concepts as manufacturing master data

KitchenManufacturingWhere
The restaurant and its kitchenPlantrestaurant.json
Tasting menu, choices, supplementsProduct catalogue, configure-to-order optionsmenu.json
Covers booked tonightMaster production scheduleservice.json
IngredientsPurchased raw materials (item master)items.json RM-*
Mise en placeSub-assemblies, work in processproducts.json INT-*
Plated coursesFinished goodsproducts.json FG-*
Recipe quantitiesBill of materialsbom in each product
Method, station, appliance, timingsRouting with setup and run timesrouting in each product
StationsWork centersresources.json WC-*
Ranges, combis, hearth, PacoJetMachines with parallel slotsresources.json EQ-*
Reach-ins, humidor, aging coolerStorage locationsresources.json SL-*
The brigadeLabor pool, skills matrix, shiftsresources.json roles, staff
Floor planPlant layoutlayout.json

The ids are the contract between every part of the system. The simulation keys its machines by EQ-*, the scene places a model at each EQ-* position, the dashboard names constraints by role and machine, and the USD export writes a prim per id. npm run validate checks every cross-reference before CI will build.

Bibliography

Rendered from data/sources.json, with how many ingredient, recipe, equipment and station records cite each one.

Four recipes reproduce Thomas Keller's published quantities exactly (Oysters and Pearls, the salmon cornet batter, Coffee and Doughnuts, butter-poached lobster), and a test checks the planner reproduces the Oysters and Pearls numbers. The rest of the menu uses typical fine-dining portions, marked est. See Limits and estimates.

Production planning (MRP)

src/model/mrp.js runs a classic material requirements planning explosion, the same code in the browser drawer and in npm run plan.

Covers become plates, plates explode into batches of sub-assemblies, then raw materials grossed up for yield, then labor and machine minutes compared with capacity. Coversthe build plan Demandplates per optionand supplement ExplodeBOM level by level,in whole batches Raw materialsgrossed up bytrim yield Loadlabor and machineminutes Capacityvs rostered shifts
demandForCovers() → explode() → loadSummary(), plus leadTimeMinutes() for items that take days (corned tongue about 76 hours, foie gras torchon about 49).
  • Option shares (beef or veal tongue) and supplement uptake (caviar, foie gras) are applied as expected values and rounded up.
  • Sub-assemblies are made in whole batches, so 31 lobsters' worth of demand blanches two batches of 20.
  • Raw materials are grossed up by yieldPct: 900 g of shelled corn needs 2 kg on the cob.
  • Each routing step becomes labor minutes for a role, scaled by laborFraction because a cook is not hands-on while the combi runs, and machine minutes for a piece of equipment.

At 62 covers every station has headroom; at 120 the fish commis needs more prep time than the shift allows. Those are the constraints the simulation then shows live.

Simulation engine

src/sim/engine.js is a discrete-event simulation of the service as a job shop. Time jumps from event to event on a binary heap; between events nothing is computed, which is why a full night of prep and service simulates in about 0.1 seconds.

What flows through it

  1. Reservations [demand]: covers split into parties of one to six, arriving on the chosen seating curve, with no-shows and a finite set of tables. A party that waits longer than its patience walks away.
  2. Prep [pre-production]: the menu is exploded into prep batches with a safety factor. Prep tasks consume raw stock and produce components that service tasks then consume. Items whose lead time cannot fit the day (veal stock, corned tongue, foie torchon) are carry-over stock from a previous day.
  3. Tickets [work orders]: one per table per course. Expo fires each ticket early by the planned lead time of its slowest dish, and staggers the other dishes so they all finish together (just-in-time offset scheduling).
  4. Tasks [routed operations]: each dish is a chain of steps with a station, an appliance type, a role, a duration and a hands-on time, so a combi stays busy after the cook walks away.
  5. Quality gate [final inspection]: QC rejects and plates held too long at the pass are re-fired; waste and food cost are tracked.
  6. Guests [a second production line]: parties are seated, eat each course at their own pace, and leave; every stage has a queue and a KPI.

Dispatching

Whenever something changes, ready tasks are sorted by class (re-fires first, then service, then prep, then pot wash) and by due date, with prep ordered by latest start. Each task then needs, in order: its inputs in stock (or still being prepped, in which case it waits), a free position at its station, a free slot on an appliance of the right type (the least-loaded unit wins), and a cook with that role or skill. Whatever blocks a task is logged, and the blocker with the most waiting in the last half hour is named on the HUD as the constraint, in theory-of-constraints terms.

Randomness and determinism

Operation times are means with lognormal spread (more for hands-on work, less for machine cycles), and appliances break down on an MTBF/MTTR model. All randomness comes from three seeded mulberry32 streams, so the same covers, seating and seed replay the same night to the second. Seeking backwards rebuilds the engine and replays from the seed; that is what makes share links exact.

KPIs

Ticket time p50, p90 and p95 per course; guest wait and course gap against target; throughput and work in process; utilization of cooks, stations and appliances on a 15-minute rolling window; OEE (availability × performance × quality) per appliance; first-pass yield; stockouts, substitutions and comps; food cost, waste and energy; and the bottleneck read-out.

API

import { createSim } from './src/sim/sim.js';
const sim = createSim({ data, covers: 62, seed: 1, seating: 'tfl-staggered' });
sim.play(); sim.setSpeed(60);
twin.onFrame((dt) => sim.tick(dt));       // the sim owns the clock
sim.seek(t); sim.stateAt(t);              // backwards seeks replay from the seed
sim.reset({ covers: 120, staffOut: [...] });   // re-plan the night
sim.on('ticket.fired' | 'op.start' | 'op.end' | 'staff.move' | 'machine.state'
     | 'inventory.change' | 'plate.passed' | 'plate.refire', cb);
sim.events; sim.taskLog; sim.forecast;    // event log, Gantt intervals, full-run result

The same modules run headless in Node: npm run sim:report -- 124 prints the KPIs for 124 covers.

Scenarios and optimizer

Every what-if is a config patch applied to the same seed, so a comparison isolates the change. src/sim/scenarios.js runs a whole service headlessly with runHeadless(), compares two runs on every KPI with compare(), sweeps covers from 20 to 200 to find rated capacity, and writes the shift report as CSV.

What-ifPatch
Full houseDouble the covers
Short-staffedOne cook removed from the schedule (staffOut)
Combi ovens downForced breakdown of both combis at 20:15 for 30 minutes (faults)
Supply shockLobster delivered 40% short (supply)
Rush seatingEveryone arrives within 30 minutes

The capacity sweep marks the knee as the first cover count where guest wait p90 passes 10 minutes, and names the resource that blocked work the most at that load.

The optimizer

A tasting menu has a target length (195 minutes here). When guests sit longer than that, the overrun is the twin's equivalent of missed takt time. src/sim/optimize.js searches two levers that shorten it:

  • An extra cook, cloned from one of the brigade with the same roles, station and shift (one candidate per line role). In the engine this is cfg.extraStaff: [{ like: staffId }].
  • A re-sequenced fire: expo calls each course earlier, by a larger share of its slowest dish's lead time (fireLead, 1× to 1.5×, today 0.9×) plus a longer fixed buffer (leadSec, 75 to 225 s), so plates reach the pass as guests are ready.

Every candidate re-runs the whole night on five seeds and is ranked on the average, so one lucky night cannot win. The two best cooks and the two best fire settings are then combined, since the levers are independent and often stack. Guards flag any plan that wins on meal time by hurting something else: first-pass yield falling more than a point, ticket p90 or table-wait p90 rising more than two minutes, or more guests walking away. The recommendation is the least invasive plan that passes the guards and meets the target: re-sequencing before extra labor, smaller changes first.

Optimizer: generate candidates, run each on five seeds, combine the best, apply guards, recommend the least invasive plan that meets the target. Candidates+1 cook per role, fire grid Run × 5 seedsheadless, averaged Combinebest 2 cooks × best 2 fires Guardsyield, tickets, waits Recommendleast invasive, on target

Precomputed scenarios

Optimizing one night is a few hundred full-service runs, so the site ships the answers. npm run precompute (scripts/precompute.mjs) optimizes 40, 62, 90 and 120 guests and writes src/sim/precomputed.data.js: each night's KPIs, every candidate ranked, and the recommended plan. src/sim/precomputed.js keys it by a hash of the simulation input, so if /data or the engine changes the site falls back to live runs, and tests/optimizer.test.mjs fails until the file is regenerated. The same file holds each precomputed night's end time, so createSim({ cache }) skips the forecast run when one of them is loaded.

The table below is read from the precomputed file shipped with this site (average of five seeds).

Where you see it

The Service scenarios and optimizer panel (src/dashboard/optimizer.js) sits in the dashboard, on dashboard.html and in the Operations drawer of the 3D twin. The Scenarios button in the dock opens the drawer at that panel. Pick a guest count to see its ranked plans and load one onto the twin; a night that is not precomputed can be optimized live, on fewer seeds. A loaded plan is part of the share link (cook, fire, lead).

3D scene

src/scene/ builds a photoreal model of the 2017 kitchen (the curved skylit vault, terrazzo floor, the fritted ribbon window onto the garden), the support annex, the culinary garden and a glimpse of the dining room. There are no downloaded models or textures: geometry is generated from /data at load, and every material's texture maps are painted on canvases at runtime from a seeded generator, so they look identical on every load.

FileBuilds
index.jsRenderer, camera, controls, lighting rig, post-processing, the public API and the render loop
plan.jsTurns layout.json and resources.json into positions for every station, machine, cabinet and cook spot
room.jsThe building, vault, annex, dining glimpse, courtyard and garden
equipment.js, appliances.jsThe equipment schedule at real dimensions: Bonnet suites, Hestan range, stock pot ranges, RATIONAL combis, Rotisol rotisserie, hearth, reach-ins and more, each with live flames, glowing elements, steam, door state and a status LED
materials.jsProcedural PBR: brushed and polished steel with anisotropy, enamel, cast iron, brass, copper, terrazzo, tile, wood, glass
ingredients.jsA container for every RM-* and INT-* in its storage location, whose level drains as the simulation draws stock
cooks.jsArticulated brigade figures in whites that walk, face their work and gesture while busy
nav.jsA walkable-floor grid with A* path finding, so cooks walk the aisles around equipment the way an AGV moves on a factory floor
items.jsPlates, pans, trays and pots carried between stations and run to the dining room
sky.js, clock.js, tour.jsThe dusk sky, the "Sense of Urgency" clock over the pass, and the camera shots for the tour
bind.jsMirrors simulation state into the scene: cooks walk to their task's station, appliances follow their state, plates move, shelves drain
quality.js, usd.jsQuality levels and the Pro Max accumulator; the OpenUSD exporter

The steel reflects the kitchen itself: a cube-camera reflection probe is captured from the aisle once the scene is built. Static geometry is merged into a few draw calls per material, and the cooks' limbs are batched per figure.

Scene API

Everything is driven through ids from the data. The simulation, Studio and the dashboard use the same calls.

const twin = await createKitchenScene(el, { quality: 'auto' });
twin.setApplianceState('EQ-HEARTH', 'busy', { tempC: 420, slotsBusy: 3 });
twin.addCook(id, { name, role, stationId }); await twin.moveCook(id, 'WC-PASS');
twin.addItem(id, { kind: 'plate', dishId }, at); twin.moveItem(id, to, { carriedBy });
twin.setInventoryLevel('RM-LOBSTER', 0.4);
twin.setOverlay('utilization' | 'traffic' | 'both'); twin.highlight(ids);
twin.flyTo('EQ-COMBI-KITCHEN'); twin.startTour(); twin.followNextPlate();
twin.setLook({ sun, sky, interior, accent, exposure, haze, season });  // Studio
twin.setQuality('auto' | 'mobile' | 'balanced' | 'high' | 'ultra');
twin.exportUSD();

Quality modes

Every level shows the same picture: the same materials, in-kitchen reflections, filmic tone mapping, glow on flames and lamps, and soft shadows from the evening sun and the vault lights. Higher levels add sharpness and depth, not a different look.

LevelForPixelsAnti-aliasingExtras
Low mobilePhones, tablets, older laptops1×FXAABloom, 2K shadows
Medium balancedEveryday laptops, integrated GPUsup to 1.5×4× MSAABloom, 2K shadows
High highApple Pro and Max chips, discrete GPUsup to 2×4× MSAAGround-truth ambient occlusion, 4K shadows
Pro Max ultraStillsup to 2.5×4× MSAA + accumulationGTAO, 4K shadows, progressive supersampling, soft area shadows, depth of field

Auto

Auto reads the GPU name the browser reports and the device memory. Software renderers, coarse-pointer devices and older Intel graphics start on Low; Apple Pro, Max and Ultra chips and discrete cards start on High; everything else starts on Medium. While the view is in use, if frames stay under about 28 fps for three seconds, Auto steps down one level and says so in a caption. It never picks Pro Max and never overrides a level you chose. Your choice is remembered in local storage.

How Pro Max works

While the camera rests, each frame is rendered with a sub-pixel camera offset and with the sun and key light nudged across a disc, following Halton sequences. An accumulation pass blends each frame into a half-float history buffer with a weight of 1/(n+1), so after about 96 frames the image converges to a supersampled still with soft area shadows, like an offline render. Any camera move resets the history.

Views and controls

3D twin (index.html)

ControlWhat it does
HUDService clock, phase (prep or service), covers seated, tickets on the rail, ticket p90 and the current constraint; play or pause and a speed from 1× to 300×
OpeningAn 11-second crane shot from the garden into the kitchen, then, on a first visit, one card offering the site tour (skip both with ?intro=0)
DockFour buttons: Site tour, Kitchen tour, Follow a plate and Operations. Everything else lives in the menu
Menu ☰ mA collapsible left sidebar: the tours, both operations layouts, scenarios, labels, overlay, quality, Studio, this wiki, the research story, Share and OpenUSD
Site tour sAn 11-chapter guided story of the whole twin. It moves the camera, opens the dashboard split or full, flips the factory vocabulary, the utilization and foot-traffic overlays, follows a plate and opens the optimizer, with one caption card per chapter. Chapters auto-advance; ← → step, Space pauses, Esc ends it. ?tour=site starts it
Kitchen tour tA camera tour through 15 shots: the suites, hearth, rotisserie, combis, reach-ins, the pass, the clock, pastry, the garden window, the annex and the plant layout from above
Bird's-eye view oThe whole plant from above
Follow a plate fThe camera follows the next plate from its station to the pass and out to the dining room, and its genealogy card opens: ticket (work order), every routing step with its cook, machine, times and queue wait, the bill of materials with the prep batch (lot) each component came from, food cost and lead time. Click any plate in the kitchen to follow it, or a routing step to fly there. Built by src/sim/genealogy.js from the engine's own records
LabelsStation labels, station and machine labels, or none
OverlayUtilization heat on stations and machines, foot-traffic trails, or both
QualityAuto, Low, Medium, High or Pro Max
Click anythingPicks a machine, ingredient or plate: highlights it, shows its details and selects it in the dashboard
Share, OpenUSD, StudioSee Share links, OpenUSD export and Studio
ScenariosOpens the dashboard at the scenarios and optimizer panel
OperationsThe live dashboard and the production plan in a drawer. Split keeps the 3D twin beside it; Metrics only gives the dashboard the whole screen. The last choice is remembered, and ?ops=split or ?ops=full opens it
SpacePlay or pause

Studio cameras

Dining room, Kitchen, Garden window and Whole restaurant.

Operations dashboard

src/dashboard/ is a control room for the service, drawn with plain DOM and Canvas 2D. It runs full screen at dashboard.html, in the drawer of the 3D twin, and over Studio, always on the same simulation object, so selecting a machine in the dashboard flies the 3D camera to it and vice versa.

PanelShows
Top barClock, phase, transport (doors, play, +5 min, speed, scrubber), re-plan by covers, seating and seed, and the Kitchen or Factory vocabulary toggle
KPI tiles and bannerHeadline KPIs and the named constraint
The kitchen, liveA floor-plan view with every cook and station state
Ticket railOpen tickets per table and course, with their age
Throughput and WIPPlates per hour on a 15-minute rolling window, open tickets and queued tasks
Ticket time by coursep50 and p90 against the 15-minute target
Station GanttThe last 75 minutes of hands-on work for cooks or machines, colored by course group, prep and pot wash. Click a bar to fly the 3D camera to that machine or station; a bar that cooked a plate also opens the plate's genealogy
InventoryService stock and the pantry, draining live
AppliancesOEE = availability × performance × quality, and load
BrigadeUtilization by station
Plan the nightGuests, orders, stock and timing tabs that build a config patch and re-run the night from the same seed
Guests: the dining-room lineIn the house, waiting and walked away; course gaps against target
What if…One-click scenarios with before and after on every KPI. Pin this night as the baseline keeps a run; every later night (a scenario, a re-plan or an optimizer plan) is then compared against the pinned one, KPI by KPI
Service scenarios and optimizerPrecomputed nights for 40 to 120 guests, the optimizer's ranked plans, and a button to load one onto the twin
Rated capacityThe 20 to 200 cover sweep and its knee
Mise en place and orderingThe MRP table and the shift report as CSV
Kitchen logEvery ticket fired, seating, stockout and breakdown, in plain words

The Kitchen or Factory toggle swaps every panel title between kitchen language and manufacturing language from data/terms.json: ticket rail becomes work-order queue, brigade becomes labor, mise en place becomes MRP.

Studio

Studio is where you shape the restaurant behind the twin. Every change other than lighting recompiles the simulation input, re-runs the whole service headlessly in about a tenth of a second, and shows the impact against the house setup for the same season.

The 3D stage fills the screen. Customize e floats the editing panel over it on the left; Run the night r puts the panel away, frames the whole restaurant and plays the full service on your setup from just before the doors open, with its results across the top and the operations dashboard one click away. ?mode=run opens Studio in Run mode.

TabControlsFlows into
Dining roomPresets, counts and shapes of two- to eight-tops, covers, seating curve, doors and last seating, eating pace, patience, no-shows, seed3D tables and chairs; the sim's tables, reservations and guest behaviour
MenuTasting or à la carte; courses, dishes and guest-choice shares; pricesThe sim's courses, dishes, prep and ingredients; check average and food cost
SeasonSpring, summer, fall, winterOut-of-season dishes swap; produce is repriced and delivered short; sun, sky, foliage, produce and flowers
Light and moodSix moods, time of day, colour temperature, brightness, exposure, haze, weather, candlesThe 3D light rig only

compile.js is a pure function from the Studio state and the adapted base model to the simulation's data and config. look.js places the sun with the NOAA solar-position formulas for Yountville (38.4° N) and converts colour temperatures with a black-body fit. stage.js layers the dining room, candlelight, flowers, seated guests and weather onto the same photoreal twin.

Share links and replay

Because the simulation is deterministic, a link carries a moment, not a recording. Opening it rebuilds the same night from the seed and seeks to the same second.

ParameterMeaning
coversGuests booked
seatingSeating scenario from service.json: tfl-staggered, two-turns or rush
seedRandom seed for the night
tHours after doors open
speed, playPlayback speed, and play=0 to open paused
cook, fire, leadOptimizer levers: extra cooks cloned from these staff ids, the fire lead share, and the fire buffer in seconds
intro, qualityintro=0 skips the crane shot; quality forces a level

Example: ?covers=120&seating=rush&t=1.5 opens a rush seating for 120 covers an hour and a half after the doors open, and ?covers=120&cook=ST-SOUS-CHEF-1&fire=1.5&lead=225 opens 120 covers on the optimizer's plan. A re-plan from the dashboard keeps the address bar in sync. Studio encodes its whole setup (dining room, menu, season, look) as base64 JSON in the hash, #s=…, so "62 seats, winter à la carte, candlelit" is a link.

OpenUSD export

The USD button writes the kitchen as an OpenUSD stage (.usda) in the browser and downloads it; nothing is uploaded. Prims are named after the data ids, so EQ-COMBI-KITCHEN in the data is /TheFrenchLaundry/Equipment/EQ_COMBI_KITCHEN in Omniverse.

/TheFrenchLaundry            Y-up, metres
  /Looks                     UsdPreviewSurface materials
  /Building                  shell, benches, annex, garden
  /Equipment/EQ_HEARTH ...   one Xform per machine with twin:* metadata,
                             render meshes and a box collider
  /WorkCenters/WC_FISH ...   stations with cook positions
  /Brigade/...               cooks at their current positions
  /PhysicsScene              so Isaac Sim can drop robots or people in

It opens in usdview, NVIDIA Omniverse and Isaac Sim. It carries geometry, transforms, materials and colliders, not the simulation. The repository's NVIDIA path explains how the twin would move into Omniverse, Isaac Sim and cuOpt, and is explicit about what is roadmap.

Build, test and deploy

A pull request runs CI (install, validate, test, build); merging to main runs the Pages workflow, which builds dist and deploys it to GitHub Pages. Pull requestone branch per thread CI (ci.yml)validate · test · build Merge to mainwhen CI is green pages.ymlvite build → dist/ GitHub Pagesdeploy-pages
npm install
npm run dev              # dev server at http://localhost:5173 (also /studio.html, /dashboard.html, /wiki.html)
npm run validate         # every id reference in /data resolves; option shares sum to 1
npm test                 # planner, engine and Studio tests (node --test)
npm run plan -- 62       # production plan for 62 covers in the terminal
npm run sim:report -- 124  # simulated KPIs for 124 covers
npm run precompute       # re-run the optimizer and rewrite src/sim/precomputed.data.js
npm run build            # static site in dist/

Vite builds four HTML entries and serves them under /michelin-kitchen-twin/, the repository's Pages path. The tests check that the planner reproduces the published Oysters and Pearls quantities, that the engine is deterministic for a seed, replays identically after a backwards seek, serves every guest and keeps inventory balanced, that each what-if moves the KPIs it should, and that Studio's compile, seasons, share links and sun position behave.

Performance

  • No asset downloads. Geometry and textures are generated at load, so the whole site is a few hundred kilobytes of compressed JavaScript and CSS.
  • Fewer draw calls. Static meshes are merged per material and the brigade's limbs are batched, so the kitchen renders in a small number of draw calls.
  • Event-driven simulation. The engine only computes when something happens, and the scene re-reads it at 6 Hz while tweening in between.
  • Adaptive quality. Auto starts from the GPU and steps down if the frame rate stays low; Pro Max only accumulates while the camera rests.
  • Precomputed answers. The optimizer's results for four guest counts ship with the site, and cached nights skip their forecast run.
  • Measured in the page. src/perf.js times the first 3D frame and the live frame rate; the menu's View section shows them, and npm run perf records them at each quality level on a laptop and a phone viewport. Results and method: docs/performance.md.
  • Headless re-runs. Studio, what-ifs and the capacity sweep re-run the full night without rendering, yielding to the UI between runs.

How it was built

The twin was built by Claude: several sessions working in parallel in one project, each in its own thread, each owning one part of the repository and merging its own pull requests once CI was green. A person set the goal and the bar and steered the work.

WorkstreamOwned
ProductThe PR-FAQ and PRD: who the twin is for, the two-minute demo, what "realistic" means
Research and data modeldata/, the sources, the MRP explosion, the site shell and the Pages deploy
Simulationsrc/sim and src/dashboard: the engine, the guest line, what-ifs, capacity and the dashboard
Scenarios and optimizeroptimize.js, the precomputed library and the dashboard panel
Realistic 3D kitchensrc/scene: materials, the building, cooks, quality levels, OpenUSD export
Studiosrc/studio: dining room, menus, seasons, light and mood

The threads agreed on contracts rather than sharing files: one set of ids in the data, and small APIs (createSim, createKitchenScene, bindSim, mountDashboard, compileStudio). When one thread needed something from another, it asked, and the owner made the change. The full story is in docs/how-it-was-built.md.

Limits and estimates

  • The menu is a composite of documented courses; the real Chef's Tasting Menu changes daily.
  • Portions for courses without a published recipe, operation times not stated in a recipe, brigade size and shifts, equipment positions and slot counts, ingredient prices and eating times are estimates, marked est in the data.
  • The layout follows the published concept and dimensions but is a reconstruction, not a survey.
  • Labor load counts routed operations only; tasting, cleaning down, family meal and the chef's walkthrough are not modelled, so real utilization is higher.
  • Dispatching uses simple priority rules. The optimizer searches staffing and fire-timing levers over whole nights; it does not solve for an optimal schedule of every operation.

An independent, non-commercial portfolio project built from public information. Not affiliated with or endorsed by The French Laundry, Thomas Keller Restaurant Group, the Michelin Guide or NVIDIA.