Skip to content

owenloh/PourDynamics

Repository files navigation

PourDynamics

A state-space simulation of pour-over coffee extraction.

PourDynamics is a research platform for building a controllable, physics-based map from brewing inputs to chemical outputs — so that the direction of a flavour change can be predicted before you ever pick up the kettle.

$$ y = f(x), \qquad J = \frac{\partial y}{\partial x} $$

We are not trying to simulate "coffee" to four decimal places. We are simulating control axes. The project succeeds if a simulated input change predicts the sign and ordering of the real-world TDS and sensory change — magnitudes get fitted from brew logs later.

Inputs x grind / PSD, dose, flow rate Q(t), temperature T(t), bloom, brewer geometry, paper, water chemistry, cup-pull time
Outputs y TDS, extraction yield (EY), per-compound cup concentrations c_i(t)
Deliverable the local sensitivity Jacobian J = ∂y/∂x — which knobs matter, which are coupled, which independently steer flavour

Sensory (acidity, sweetness, bitterness, body, clarity…) is a downstream linear projection sensory = W·c, fit separately from tasting data. The simulator itself is truncated at chemistry.


The physics, in one picture

The skeleton is a 2D-axisymmetric Doyle–Fuller–Newman (DFN) model — the Li-ion battery framework, ported to a coffee bed and extended from 1D to 2D:

2D-axisymmetric bed (r, z)            ×        1D radial particle (per cell)
advection–dispersion on a Darcy field          compound diffusion out of each bean
        "where the water goes"                       "what dissolves"

Two co-equal pillars couple to produce the cup:

 EXTRACTION pillar    V0 ─ V1 ─ V2 ─ V3 ─ V4 ─ V5 ─ V6 ─ V7 ─ V8 ─ V10 ┐
 (what dissolves)      radial diff → PSD → compounds → bed → Q → T → ψ  │
                                                                        ├─► COUPLED MODEL
 HYDRODYNAMICS pillar  H0 ─ H1 ─ H1_3D ─ H2 ─ H-trans ─ H4 ─ … ─ H7 ────┘   (2D ADE on the
 (where water goes)    paper → cone-Darcy field u(r,z,t) → seepage          Darcy field →
                                                                            TDS, EY, c_i(t))

Guiding principles:

  • Regimes emerge, never switch. Diffusion, dissolution, and film transfer all stay in the equations; per-compound parameters decide which mechanism dominates. We never hard-code the rate-limiting step.
  • The bed is solved as a direct 2D field, not a bundle of parallel 1D streamtubes (that older "multi-stream" approach is retired — independent 1D columns miss the suspended-cone soak and under-extract ~4×).
  • Immersion and percolation are the same simulator. A French-press steep is just a brew with Q = 0; bloom and inter-pour pauses are zero-flow stretches.

See 01 Physics/00 Modeling Philosophy.md for the full framing and 02 Model Versions/Model Roadmap.md for the version-by-version build order.


Repository layout

Folder What's in it
00 Vision/ The fixed research goal (Project Thesis.md) — do not modify
01 Physics/ Modeling philosophy, governing equations, assumptions, scope, compound classes, architectural decisions
02 Model Versions/ V0 → V10 roadmap + per-version notes; the Coupled Model and Hydrodynamics branch
03 Simulations/ The simulation code — the engine/ package, the legacy coffee_sim/ package, tests, notebooks, and rendered results/
04 Experiments/ Brew-log protocol + module-isolation studies
05 Analysis/ Jacobians, sensitivity sweeps, validation against brew logs
06 References/ Literature, parameter sources, glossary
99 In-Tray.md Raw thoughts pending distillation

The repo is an Obsidian vault — internal navigation uses [[wiki links]]. It is also a working Python codebase under 03 Simulations/.


Quickstart — brew a cup, get the figure

The engine lives in 03 Simulations/engine/ and is a pure function y = brew(x). The fastest way in is the brew-card helper, which starts from the lab-standard recipe, overrides only the knobs you name, and renders the standard 8-panel figure with a differential sensory radar.

cd "03 Simulations"
pip install numpy scipy matplotlib          # fresh environments only

# CLI — render a brew card to PNG
python -m engine.brew_card out.png \
    --temp 70 --flow 1.0 --bloom 30 --bloom-time 30 --water 170 --yield 150
# Or from Python — the underlying pure function
from engine import brew, standard_v60_flat
y = brew(standard_v60_flat())
print(f"TDS {y.chemistry.tds_pct:.2f}%   EY {y.chemistry.ey_pct:.1f}%")

Anything you don't name stays at the lab standard. The full input/output contract, physics decisions, and explicit exclusions are in 03 Simulations/engine/DESIGN.md.

The standard figure (with streamlines) takes ~1–2 min per brew, so run long sweeps in the background.


Validation philosophy

No module is trusted until it reproduces a known analytic limit. Every version must clear five gates before the next one starts:

Check Pass criterion
Analytic limit < 1 % deviation on the canonical case (Crank series, ADE Gaussian plume)
Mass conservation ∫ source = ∫ sink + Δstorage, < 1e-9 relative drift
Reduces to Vn–1 < 0.5 % deviation when the new feature is switched off
Limit checks D→∞ instant equilibration, D→0 no extraction, Q→∞ no contact, b→1 full bypass
Isolation experiment sign of ∂y/∂x matches measurement

Tests live in 03 Simulations/tests/. The current engine validates against the older, separately-validated coffee_sim engine to < 0.2 % across grind / dose / temperature sweeps.

cd "03 Simulations"
python -m pytest tests/                      # run the test suite
python -m engine.validate_vs_old            # engine-vs-engine validation harness

For contributors (and resident AI agents)

This repo is a living research notebook with a few load-bearing rules, spelled out in CLAUDE.md:

  1. Never modify 00 Vision/Project Thesis.md. The vision is fixed.
  2. Truncate at chemistry. The simulator targets (TDS, EY, c_i(t)); sensory mapping is downstream.
  3. Regimes emerge, never switch. Keep all mechanisms in the equations.
  4. 2D-axisymmetric DFN is the skeleton — not 0D ODEs, not full CFD.
  5. "Ignored" vs "Deferred" is sacred — see 01 Physics/05 Scope - Ignored and Deferred.md. Ignored = out of scope forever; Deferred = revisit only when residuals demand it.

Default stack: Python (numpy, scipy, numba, matplotlib). Non-dimensionalize before coding any numerical scheme, and no new feature ships without a test that pins its behaviour.

When in doubt, ask: does this change improve our estimate of J, or is it cosmetic?

Releases

No releases published

Packages

 
 
 

Contributors

Languages