Skip to content

doe: the metric-choice figure and the worked-study figures behind pid-book #270 - #81

Open
kgdunn wants to merge 20 commits into
mainfrom
claude/factorial-table-omars-addition-ufpj8j
Open

kgdunn wants to merge 20 commits into
mainfrom
claude/factorial-table-omars-addition-ufpj8j

Conversation

@kgdunn

@kgdunn kgdunn commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Parallel to pid-book #270, which adds the sections these figures illustrate. Merge this one first, so the book's PDF build can find the images.

What is added

doe/omars-metric-choice.py and its PNG

The figure reads candidate cell measures down the three-factor column of the OMARS trade-off table: the five alphabetic optimality criteria (A, D, E, I, G), the largest absolute correlation between any two second-order terms, and, in a third row, the power to detect a main effect, a quadratic and a two-factor interaction of one noise standard deviation at the 5% level. Each point is the best value attainable at that run count, so the curves are frontiers rather than the output of a search. Three series per panel, for one, two and three centre runs, in blue, purple and dark brown (the brown chosen after checking the three remain distinct under simulated colour-vision deficiency).

Panels are grouped so the pairs are visible. Columns: the left pair averages, the middle pair takes the matching worst case, the right pair does neither. Rows: the top summarises X'X, the middle is prediction variance over the region apart from the last panel, the bottom is power. All nine panels share one x-axis range and tick set. The Box-Behnken design (green star) and the definitive screening design (orange circle) are marked on every panel, in the colours the trade-off staircase already uses for them.

Five correlation-map insets sit on the max |r| panel, on a common shading scale, each outlined and connected in the colour of its centre-run series. The three on the left are the smallest design at each centre-run count (9, 10 and 11 runs); they share the same four half-rows, so together they isolate what the centre runs do to the map: the quadratic-to-interaction correlation falls from 0.707 to 0.645 to 0.606 while the quadratic-to-quadratic correlation rises from zero to 0.167 to 0.267. The remaining two, at 24 runs with two centre runs and 31 runs with one, show the map at moderate and near-zero entanglement.

doe/omars_metric_choice_data.py

The enumeration behind the literals in the figure script, committed so the numbers can be re-derived rather than trusted. Finding the best value at each size means enumerating every OMARS foldover of that size. That is tractable because a foldover is described entirely by how many times each sign pattern of {-1, 0, +1} appears in its half: every second-order term takes the same value on a half-row and on that row negated. Main effects are orthogonal, so their variance is 1/(2 n_j) directly; quadratics and interactions come from the even block; the full model is attempted only where h >= k(k+1)/2, and any rank-deficient cell is rejected. --verify rebuilds twelve cells from naive numpy model matrices and compares: largest absolute difference 4.9e-07.

Two further checks that the data passes: all enumerated cells are monotone in the run count for the five alphabetic criteria, which is what the Loewner-order argument predicts, and G never falls below its Kiefer-Wolfowitz floor of p/N, touching it exactly at three of the sizes.

doe/omars-worked-study-tradeoff.py, its PNG, and doe/omars_worked_study_data.py

For the new book section "A worked OMARS study". The same four-factor study on the fed-batch bioreactor simulator (process_improve.simulation.batch) is run 200 times at 13, 17, 21, 27 and 31 OMARS runs and with the 27-run Box-Behnken and face-centred central composite designs, each campaign scored by the true titer at the recipe its fitted model recommends, minus the titer of the current recipe. The figure shows the median, the 10th-to-90th percentile band and the worst campaign for each design, against the best possible gain of 2.006 g/L. Two hundred campaigns per design take minutes, so the values are carried as literals; the data script regenerates them (--quick for a 20-seed smoke test).

Six more worked-study figures, and doe/omars_worked_study_common.py

One script per PNG, following the directory's convention, each importing the study from the common module:

  • omars-worked-study-recipe: the current recipe's temperature schedule with the four corners of the hold-temperature by shift-day range, beside titer against day for twenty replicate batches over the disturbance-free batch.
  • omars-trade-off-column-k4: the four-factor column of the OMARS trade-off table on its own, at every odd run count from 9 to 31, with the staircase's fills, the estimability frontier outlined at 21 runs and the Box-Behnken design closing the column at 27. Every cell is read from the library.
  • omars-worked-study-plan: the thirty batches in run order as a level grid, one row per factor, with the cassette boundary and the four centre runs outlined.
  • omars-worked-study-titer: titer at harvest by run and cassette, centre runs starred, each cassette's centre-run mean drawn, the replicate mean as a reference.
  • omars-worked-study-effects: the fifteen coefficients of the full second-order model fitted in one step, with 95% intervals on 15 df, the three terms the staged analysis selects filled. The script asserts that the terms significant at 5% are exactly those three, which the chapter states.
  • omars-worked-study-surface: the fitted four-term model beside the true response over hold temperature and shift day at pH 7.1 and the high feed rate, on shared contour levels, with the current, recommended and best recipes marked on both.

omars_worked_study_common.py reproduces the study exactly as the chapter's code blocks run it (same factors, disturbance level, design seed, pair split, run order and disturbance seeds) and asserts the values the chapter prints (titer range 4.290 to 9.116 g/L, cassette effect -0.1272, recommended hold 29.54 °C, true titers 7.436 / 8.376 / 9.442 g/L) before any figure is drawn. A change in the library that moved any of those stops every figure script rather than silently redrawing it.

Conventions followed

Matplotlib, Okabe-Ito palette matching the other OMARS figures in doe/, scripts committed alongside the PNGs in the same directory, run from that directory to write the image next to themselves.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm

kgdunn and others added 3 commits August 9, 2026 21:52
process-improve renamed the single-cell accessor to get_omars_trade_off_table_entry
and the module to omars_trade_off. The figure reads every value from that function,
so the script follows the rename.

The rendered PNG is byte-identical, which confirms the rename changed no values.
Reads six candidate cell measures down the three-factor column of the OMARS
trade-off table: the five alphabetic optimality criteria and the largest
absolute correlation between two second-order terms. Each point is the best
value attainable at that run count, found by listing every OMARS foldover of
the size rather than by searching, so the curves are frontiers.

The panels are grouped so the pairs are visible: the left column averages,
the middle takes the matching worst case, the right does neither; the top row
summarises X'X and the bottom row is prediction variance over the region. The
five criteria are monotone in the run count at all three centre-run counts,
which the correlation is not, and insets on the last panel show the
correlation maps behind three of its points.

The enumeration takes hours, so its output is carried in the script as
literal data. Twelve of the cells were re-derived from naive numpy model
matrices and agree to 5e-7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
claude added 12 commits August 14, 2026 21:32
The last panel had room for one more, and the three insets it carried all came
from the one-centre-run series. The new one is the twenty-four-run design with
two centre runs, at max |r| of 0.250, so the insets now sample two of the three
series and the spacing across the panel is even.

The inset dictionaries are keyed by centre-run count as well as run count,
since a run count alone no longer identifies which design is shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The inset at thirteen runs did not carry its weight: it was a third example of
a one-centre-run design, next to two others. Replacing it with the smallest
design at each of the three centre-run counts, at nine, ten and eleven runs,
turns three of the five insets into a controlled comparison. Those designs
share the same four half-rows, so only the centre count separates them, and
the maps show what the centre runs trade: the quadratic-to-interaction
correlation falls from 0.707 to 0.645 to 0.606 while the
quadratic-to-quadratic correlation rises from zero to 0.167 to 0.267.

Each inset border and its leader line now take the colour of the series the
design belongs to, so the reader can tell the three apart without labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The staircase showed run budgets from 9 to 57 with no fixed reference at either
end, so a reader could not see where the standard designs sit on it. A DSD row
now sits above the budgets and a Box-Behnken row below them, ruled off from the
budgets and from each other, which is the span the OMARS family covers: the
definitive screening design is its smallest member and the Box-Behnken design is
among its largest.

An anchor row is a named design rather than a budget, so its run count changes
from column to column and is carried inside the cell rather than read off the
row label. Both anchor rows come from the library's _reference_entry, so they
cannot drift away from it any more than the budget cells can.

The frontier outlines shift down a row to account for the DSD row above them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The DSD and Box-Behnken rows added in the previous commit duplicated what the
table already said. A definitive screening design has 9, 13 or 17 runs, which
are budgets on the table, so its row repeated those cells; it is now a DSD
marker inside the cell it lands in.

The Box-Behnken run counts are not budgets, so the figure adds a row for each
of 15, 27, 46, 54 and 62. Those cells are marked BBD and filled in the
Okabe-Ito bluish green to set them apart from the capability classes. Each
column ends there: below its Box-Behnken cell every remaining row would say
Full again on more runs, so those are left blank and the column closes.

Reading down a column now shows the whole span of the family for that factor
count, from the smallest design that exists to the standard response surface
design that ends it.

The frontier outlines lose the row offset they needed while a DSD row sat above
them, and the figure is taller, since fourteen rows of two-line cells need the
height.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The notes ran down a single column below the table and cost more vertical space
than the table rows they explain. Two columns halve that: the figure is 2880
pixels tall against 3300, with the table itself unchanged.

Matplotlib fills a multi-column legend column by column, so the entries are
ordered to put the three capability classes on the left and the two design
marks, the empty cells and the frontier outline on the right.

Also drops the numpy and Line2D imports, neither of which is used since the
anchor rows were replaced by in-place marks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The note reading "19 runs, 15 parameters, model matrix rank 14" sat below the
frontier, in the busiest part of the axes: its arrow crossed the parameter-count
line and ran down through the definitive screening design markers to reach the
marked point.

It now sits above the frontier and to its left, where the axes are empty, and
its arrow curves in from that side so it does not pass through the bold 21 that
labels the four-factor frontier value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The leader was drawn at the default depth, so the frontier line and its shaded
band covered the part of it that crossed them. It now sits above every series,
and the line is thick enough to read against the shading it passes over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The leader ran from the note down to the marked cross, which sits below the
frontier, so it crossed the orange line and its shaded band to get there. It now
ends at 23.5 runs, above and to the left of the line, and points towards the
cross rather than touching it. The nearest approach is about five runs clear of
the frontier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
Stopping the leader at 23.5 runs left it too far short to read as pointing at
anything. It curves again, and now ends at 21.4 runs, a little over one run
above the frontier line at that point and close enough to the marked cross to
be unambiguous, without touching the line or its shaded band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The six existing panels answer "which measure should a cell of the trade-off
table report" in the language of optimal design theory. Power is the measure a
practitioner is most likely to reach for, and it was missing.

The third row adds power for each of the three term types. It scores the full
second-order model rather than the main-effects-and-quadratics model the six
panels use, because the interactions have to be in the model for the middle
panel to exist, so the row starts at the estimability frontier of 13 runs. All
three curves are monotone in the run count, which puts power with the five
alphabetic criteria rather than with max |r|.

The row shows the ordering is main effect, then interaction, then quadratic,
not what a reader would guess: at one sigma a main effect clears 0.8 power at
15 runs and an interaction at 19, while a quadratic does not reach it anywhere
below 31. It also shows that centre runs cost main-effect and interaction power
but buy quadratic power at the small sizes, which is a direct answer to what
the extra centre runs are for.

Two standard designs are now marked in every panel where they are defined. The
definitive screening design sits exactly on the frontier for all six enumerated
measures at its size, and is absent from the power row since 9 runs cannot fit
a ten-parameter model. The Box-Behnken sits on the frontier for A, D, I,
max |r| and quadratic power, and off it for E, G, main-effect power and
interaction power: it spends its runs on curvature.

Presentation changes that came with it:

- Green and orange now belong to the two anchor designs, matching the colours
  they carry in the trade-off table, so the second and third centre-run series
  move to reddish purple and dark gold-brown. The choice was checked under
  simulated deuteranopia and protanopia; a deep violet was rejected because it
  collapses onto the blue series.
- All nine panels share one x range and tick set, so a run count sits at the
  same position in every panel.
- One frame colour for every panel. The max |r| panel used to carry a
  vermillion frame to mark it as the measure that is not monotone; among nine
  panels that reads as a highlight, and the reverses label says it in words.
- A white patch behind the corner notes, which were colliding with a gridline
  in the I panel and with an inset leader in the max |r| panel.

Companion to the OMARS designs section of the Design and Analysis of
Experiments chapter in kgdunn/pid-book.
The figure carries its numbers as literals because the exhaustive enumeration
behind them is far too slow to run at plot time. Until now that enumeration
lived nowhere, so the literals had to be taken on trust and could not be
re-derived if a definition changed.

This module is where they come from. Run it with no arguments and it prints
POWER_C and ANCHORS exactly as the figure spells them, ready to paste back.
Run it with --verify and it re-derives a sample of the six enumerated panels
the naive way, building the actual N x p model matrix for every candidate with
numpy, and compares against the BEST and MAX_R literals; nothing is shared
between the two paths except the definition of an OMARS foldover, so agreement
is evidence. Twelve cells agree to 4.88e-07.

The power enumeration is cheap for a structural reason worth recording: main
effects are orthogonal to everything in an OMARS design, so their coefficient
variance is 1 / (2 n_j), and everything else lives in an even block built
straight from the sign-class counts. No N x p matrix is ever formed, so a run
count takes seconds where the six-measure enumeration takes hours.
What a design of each size buys on the fed-batch bioreactor, in grams per
litre of product at the recipe the fitted model recommends, over two hundred
simulated campaigns per design. OMARS designs of 13 to 31 runs, with the
27-run Box-Behnken and face-centred central composite designs alongside.

The figure carries its numbers as literals because two hundred campaigns per
design take minutes; omars_worked_study_data.py regenerates them, and its
--quick flag gives a twenty-seed smoke test.

The reading it supports: below 27 runs the outcome is closer to a lottery than
to a smaller version of the same study. The 13-run design leaves the median
campaign where it started; the 17-run design gains at the median but its worst
campaign loses 3.2 g/L; the 21-run design the search returned varies the feed
rate in the same twelve runs as the 17-run design and finds it in only a third
of campaigns. At 27 runs the worst of two hundred campaigns loses 0.35 g/L.

Companion to the section "A worked OMARS study" in the Design and Analysis of
Experiments chapter of kgdunn/pid-book.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
@kgdunn kgdunn changed the title doe: the six-measure figure behind the OMARS trade-off cell choice doe: the metric-choice figure and the worked-study trade-off figure behind pid-book #270 Sep 2, 2026
The book section "A worked OMARS study" had one figure, the run-count
trade-off, and was otherwise prose, code and tables. These six give each
of its text-heavy subsections a picture: the current recipe and its
replicate batches, the four-factor column of the OMARS trade-off table on
its own, the thirty-batch plan in run order with the cassette boundary
and the centre runs, the harvest titers by cassette, the full
second-order model fitted in one step with the staged analysis's three
terms marked, and the fitted model beside the true response with the
current, recommended and best recipes on both.

The study is reproduced once, in omars_worked_study_common.py, exactly as
the chapter's code blocks run it, and that module asserts the values the
chapter prints before any figure is drawn, so the figures cannot drift
from the text. The effects script additionally checks that the terms
significant at 5% in the one-step fit are the three the staged analysis
selects, which the chapter states.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
@kgdunn kgdunn changed the title doe: the metric-choice figure and the worked-study trade-off figure behind pid-book #270 doe: the metric-choice figure and the worked-study figures behind pid-book #270 Sep 2, 2026
The four-factor column now carries its run counts and axis title above
the cells, as a header. The effects and recipe figures put their legends
in a lightly bordered box and move the labels that collided. The titer
figure changes axis: run order showed nothing the plan grid does not, so
the harvest titers are now read against the feed rate, the largest
effect the study finds, one panel per cassette, with the mean at each
feed level as a short bar and the centre runs starred. The stems and
the heavy centre-run mean lines are gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
The effects plot's x-axis extends further left so the legend box sits
clear of the interaction intervals. The recipe figure's legend entries
wrap onto two lines, so both boxes are narrower.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
…is found

The book section carried a table beside this figure whose last two
columns repeated the figure and whose other three, the fraction of
campaigns in which the feed rate, the temperature-by-shift interaction
and the temperature quadratic were declared active, had no picture. The
figure now has two rows on a shared run-count axis: the gain in titer as
before, and below it those three fractions as lines, with the Box-Behnken
and central composite designs at 27 runs marked in each line's colour.
The table can then be dropped from the section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1daHPaXLs7PPRsq8CmMMm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants