Week 2 · 2105623 Optimization of Chemical Processes
17 August 2026
Today
Lecture 120 minutes, activities 30, breaks 20, administration 10. Tool of the week: OpenSolver, because periods laid across columns read naturally in a spreadsheet.
Learning objectives · CLO 1 and CLO 4
A unit on every symbol, bounds on every variable — and find the ambiguities in somebody else’s.
Index set, balance equation, capacity constraint, linking constraint, and apply each one.
Write an inventory balance linking consecutive periods, and state your start-versus-end convention.
Separately from held stock, without a binary variable, and say why a horizon-end condition is mandatory.
Map every spreadsheet range to its algebraic symbol, and reproduce a stated optimum.
Period by period, and use it to justify where a debottlenecking budget goes.
Recap of Week 1
1Verbalthe problem in prose
2Specificationwe are here
3Algebraand here
4CodeWeeks 7 and 8
5InterpretationWeeks 7 and 8
Week 1 established
The four parts of any problem, the standard form, the classification table, the tool stack, and one solved LP: x* = (6, 3) t/week at z* = 3,300.00 USD/week with two binding resources.
And the geometry
An LP with a finite optimum has an optimal vertex. That argument is not repeated today. Every model in this session is an LP and the same picture applies, unchanged.
The modeling block is stages 2 and 3, done four times on four kinds of structure. Weeks 2 to 5 add no new mathematics at all.
Lecture block 1 · 35 minutes
One document, four patterns,
and one small model in which all four are visible at once.
Why a document, and not algebra
Why this and not algebra
Algebra hides assumptions. The symbol It looks precise, but it does not say whether inventory is measured at the start or the end of the period. The specification has to say so in words, where a reader can disagree with it.
A specification that requires guessing will be guessed at, and the guess will be plausible, silent and unlabeled.
The code is a translation of this document. If the specification is right, writing the algebra and then the model file is close to mechanical.
This is what Activity 1 asks you to fill in
| Section | What goes in it | The question it forces you to answer |
|---|---|---|
| Sets and indices | every dimension of the problem, named, with its membership | What is repeated, and over what? |
| Parameters | symbol, meaning, unit, source of the number | Which numbers are fixed before the model is solved, and who owns them? |
| Decision variables | symbol with full index, unit, bounds | What is genuinely under your control? |
| Objective | one sentence in words, with its unit | What counts as better, in what currency, over what period? |
| Constraints | numbered, in words | What must hold, and is each one hard or soft? |
| Assumptions | numbered alongside the constraints | What did you decide to ignore, and would a reviewer accept it? |
Assumptions are written as numbered restrictions, not as prose. “Everything produced is sold” is a modeling decision with a cost attached, and numbering it is what lets somebody challenge it.
Specification, row 2
| Symbol | Meaning | Unit | Where the number comes from |
|---|---|---|---|
| dt | demand in period t | t/month | sales forecast, revision date recorded |
| Ct | production capacity | t/month | line rate times available hours |
| ct | unit production cost | USD/t | standard cost plus the electricity contract |
| h | inventory holding cost | USD/t/month | working capital plus storage |
| b | backlog penalty | USD/t/month | expedited freight plus contractual damages |
| I0 | opening inventory | t | the stock ledger at the planning date |
| ρp | reactor hours per tonne of p | h/t | the process, measured and not assumed |
A parameter is anything fixed before the problem is solved. If you can change it, it is a variable; if you cannot, it is a parameter. Confusing the two is the commonest stage-2 error.
h and b carry per month in their unit. That is what makes a backlog charged again in every period until it is cleared — and it is exactly the modeling decision that drives the answer at half past two.
Specification, row 3
| Symbol | Meaning | Unit |
|---|---|---|
| xt ≥ 0 | production in period t | t |
| It ≥ 0 | inventory carried out of t | t |
| Bt ≥ 0 | backlog carried out of t | t |
Bounds are free performance and free safety
An unbounded variable is the commonest cause of an unbounded model, and an unbounded integer variable will cripple a solver’s presolve in Week 11. Write the physical bound even when you believe the constraints already imply it. It costs one line and it documents the physics.
A variable is not a formula
It is a variable that the balance equation ties to production and demand. Do not write it as an expression. Keeping it a variable is what lets you bound it, price it, and read its dual.
Thirty seconds, every time
The check, applied to the inventory balance
It−1 [t] − Bt−1 [t] + xt [t] − dt [t] = It [t] − Bt [t]
Every term is a tonnage. If any one of them had carried a rate, the equation would be wrong, and it would still solve.
And to the objective
ct [USD/t] · xt [t] + h [USD/t/month] · It [t] · 1 [month] ⇒ [USD]
The “one period” in the holding term is implicit. Writing it out is what stops you putting a weekly rate into a monthly model.
The class of error units catch
Mixing a stock (tonnes) with a flow (tonnes per month), or mixing two time bases. Neither produces an error message. Both produce a plan that is confidently wrong by a factor of four or five.
The same relation repeated over a family
Template
i ∈ I and then xi ⟶ xi,t
| Dimension | Index | Set |
|---|---|---|
| product or grade | p | P |
| resource or operation | r, s | R, S |
| period | t | T, ordered |
| node or site | i, j | N |
| scenario | ω | Ω |
Ordered sets matter
A time set must be ordered, because the model needs to know what t − 1 means. In Pyomo, pyo.Set(initialize=T, ordered=True), then m.T.prev(t). In a spreadsheet, “the column immediately to the left” is the same statement — and it is why periods run across columns.
What goes in equals what goes out plus what accumulates
Template
accumulation = inflow − outflow
It−1 + xt − dt = It
The most expensive class of error
A missing balance is worse than a wrong one. The model still solves. It still reports a plan. It has simply stopped conserving material, and nothing on the screen says so.
Patterns 1 and 2 together
Written once, expanded over the ordered set T. One line of specification becomes six rows in the spreadsheet, and the six rows are what make it a plan rather than six unrelated decisions.
A shared resource limits a sum
Template
Σi ari xi ≤ br for every r ∈ R
Renewable, or consumed once
xt ≤ Ct for every t versus Σt xt ≤ C
Line hours that reset every month give one inequality per period. A fixed allocation of catalyst consumed once over the horizon gives one inequality summed over t.
They behave completely differently, and nothing in the English of a problem statement tells you which is meant. The specification has to say.
A variable in one block constrains another
Template
any constraint whose index
spans more than one block
Temporal linking · block 2
The inventory balance carries stock from t − 1 into t, so the periods cannot be planned one at a time.
Resource linking · block 3
One shared reactor is used by both products, so the products cannot be planned one at a time.
Remove both and the model falls apart into a stack of independent one-period problems that nobody needs a solver for.
Why they are worth naming
| 1 Index set | 2 Balance | 3 Capacity | 4 Linking | |
|---|---|---|---|---|
| Refinery blend | feedstocks c, properties q | volume balance: blend = Σ components | feedstock availability, over c | quality specs, over q |
| Staff roster | staff e, shifts t | only if headcount carries across periods | cover per shift; hours per person | rest rules, over consecutive t |
| Battery dispatch | hours t, |T| = 24 | state-of-charge balance, over t | power limit and energy capacity per t | the state-of-charge balance itself |
| Warehouse network | plants i, warehouses j, customers k | node balance at each warehouse | plant supply; warehouse throughput | the warehouse node balance |
The honest nuance. The roster’s balance row is arguable. If staff are simply assigned to shifts, the cover requirement is a capacity constraint with a ≥ sense and there is no balance at all. A balance appears only if a pool of people is carried across periods. Both readings are defensible — and the specification is where you say which one you chose.
All four patterns, in a model small enough to hold in the head
A finishing shop attached to the electrolyte plant packages two grades for zinc-air cell assembly, a Standard grade and a HighPurity grade. Both pass through the same three operations in order: mixing, ion-exchange purification, and filling. The three operations have different weekly hour allocations because they are shared with other campaigns. HighPurity is worth more per tonne but spends four times as long in purification. Everything finished is sold.
| Operation | Standard, h/t | HighPurity, h/t | Available, h/week |
|---|---|---|---|
| Mixing | 2 | 3 | 120 |
| Ion-exchange purification | 1 | 4 | 104 |
| Filling | 1 | 1 | 44 |
| Contribution margin, USD/t | 520 | 790 |
Stage 2, restrictions in words. R1. On each operation, the hours consumed cannot exceed the hours allocated. R2. Production cannot be negative. R3. There is no demand limit, because everything finished is sold — and R3 is an assumption, numbered so that it can be challenged. Stage 3. max Σp cp xp subject to Σp asp xp ≤ bs for every s, with xp ≥ 0 and [h/t]·[t/week] = [h/week] on every row.
Busiest and binding are different words
| Operation | Used, h | Avail., h | Slack, h | Dual, USD/h |
|---|---|---|---|---|
| Mixing | 108 | 120 | 12 | 0 |
| Purification | 104 | 104 | 0 | 90 |
| Filling | 44 | 44 | 0 | 430 |
The answer
24 t of Standard and 20 t of HighPurity, for 28,280.00 USD of contribution margin in the week.
Verified two ways in the notebook: vertex enumeration in NumPy, and a finite-difference check on every dual.
The engineering reading
Buy an overtime shift on filling first, not on the operation that looks busiest.
Mixing consumes 108 hours, more than either other operation in absolute terms, and it is worth exactly zero at the margin.
And the limitation we spend the rest of the day removing
This plans one week in isolation. It has no memory. It cannot build stock now for a peak later, and it cannot say that a shortfall this week must be delivered next week.
Activity 1 · 15 minutes · pairs, paper
You may not write a single equation
for the next eight minutes. Not one.
Write what the symbols mean, what units they carry, and what the constraints say in English.
Activity 1 · 15 minutes · pairs, paper
Aurora Specialty Chemicals makes two grades of electrolyte, Standard and HighPurity, on one shared reactor train. The reactor is available 640 hours per month. Standard needs 1.0 reactor hours per tonne and HighPurity needs 1.4. Marketing has issued a demand forecast for the next four months: for Standard, 180, 210, 260 and 190 tonnes; for HighPurity, 90, 120, 150 and 110 tonnes. Production cost differs by grade and rises in month 3, when electricity is dearest. Anything finished but not shipped in the month it is made is stored in a single warehouse that holds 120 tonnes, at 7 USD per tonne per month. Thirty tonnes of Standard are already in store. The plant wants the cheapest four-month plan that meets the forecast.
Write only the specification table
Sets and indices · parameters with symbol and units · decision variables with symbol, units and bounds · the objective in words with its units · the constraints in words.
Writing algebra is forbidden. So is writing any spreadsheet formula.
10
minute break
Then: what a time index does to a model, the inventory balance and its start-versus-end convention, backlog priced separately from held stock, and the whole six-period model assembled one family at a time
Lecture block 2 · 40 minutes
One index, added to every variable,
turns a decision into a plan.
Pattern 4, drawn
Without the amber arrows these are six independent problems. Each would be solved on its own, and nothing at all would be gained by solving them together. The optimization would be arithmetic.
The amber arrows are the linking constraint of pattern 4. Everything a multiperiod model can do about seasonality, a demand spike or a cost profile passes through them.
Pattern 2, with the boundary drawn around a period
Accumulation equals inflow minus outflow, around a period
It−1 − Bt−1 + xt − dt = It − Bt for every t ∈ T
with I0 given and B0 = 0.
This is ambiguity 1 from Activity 1
Convention A — used here
It is the stock after period t has produced and shipped.
It−1 + xt − dt = It
Holding is charged on what is left over at the close of the period, so material that arrives and ships inside the same period is never charged holding.
Convention B
Ĩt is the stock before period t produces and ships, so Ĩt = It−1.
Ĩt + xt − dt = Ĩt+1
Holding is charged on the opening stock, so the 20 t on hand at the planning date is charged in period 1 — and the closing stock of the last period is not charged at all.
Both are correct. They are not the same model: they differ by one period of holding charge at each end of the horizon, so they return different total costs and can return different plans. What is wrong is failing to say which one you used, because the next engineer to touch the sheet will assume the other.
Three modeling decisions, one slide
Why split the net position into two variables
A single free variable st = It − Bt would be simpler, but it could not be priced: holding costs h, shortage costs b, and b ≠ h. Splitting into It ≥ 0 and Bt ≥ 0 keeps the model linear while pricing the two directions differently.
Complementarity comes free
Because h > 0, b > 0 and this is a minimization, no optimal solution ever has It > 0 and Bt > 0 at once — carrying stock and owing stock at the same time costs money and changes nothing. No binary variable is needed. The cost structure enforces it.
The horizon-end condition
B|T| = 0
This is what makes the plan honest. Without it, the model pushes all the difficult demand past the end of the horizon and reports a cost that cannot be achieved.
Capacity: per period, or across the horizon
xt ≤ Ct for every t versus Σt xt ≤ C
Line hours reset each month: one inequality per period. A fixed catalyst allocation consumed once: a single inequality. Today’s model uses the first.
Everything so far, on one slide
Minimize total cost over the horizon
min Σt ( ct xt + h It + b Bt ) [USD] over x, I, B ≥ 0
| Constraint family | Pattern | |
|---|---|---|
| It−1 − Bt−1 + xt − dt = It − Bt | for every t | balance, P2 + P4 |
| xt ≤ Ct | for every t | capacity, P3 |
| B|T| = 0 | once | horizon end |
| xt, It, Bt ≥ 0 | for every t | bounds |
| Size, for six periods | |
|---|---|
| Decision variables | 18 |
| Constraints | 13 |
Three variable families times six periods. Six balances, six capacities, one horizon-end condition.
Everything is linear and continuous, so this is an LP and the Week 1 vertex argument applies unchanged. The whole model is patterns 1 to 4 and nothing else.
The sheet you are about to complete
| Algebraic symbol | Range | Pyomo component |
|---|---|---|
| t ∈ T | C5:H5 |
Set(..., ordered=True) |
| dt, Ct, ct | C6:H8 |
Param(m.T, ...) |
| h, b, I0, B0 | C9:H10, C11:C12 |
scalar Param |
| x, I, B ≥ 0 | C16:H18 |
three Var(m.T, ...) |
| balance ∀ t | C28:C33 |
Constraint(m.T, rule=...) |
| xt ≤ Ct | C34:C39 |
Constraint(m.T, ...) |
| objective | C24 |
Objective(sense=minimize) |
The transposition. In Week 1 the model ran down the rows. Here periods run across columns, so each constraint family becomes a block of rows. Only the layout changed.
Activity 2 · 15 minutes · pairs, one computer
The number is on the board.
Forty-two thousand and eighty.
If your sheet does not say that, your sheet is wrong — and finding out why is more useful than anything I could tell you in the next eight minutes.
Activity 2 · 15 minutes · pairs, one computer
Data already on the sheet
Six monthly periods. Demand 120, 150, 180, 210, 160, 130 t. Line capacity 170 t/month throughout. Production cost 42, 42, 45, 48, 46, 43 USD/t. Holding 6 USD/t/month, backlog 45 USD/t/month, opening stock 20 t, and B6 = 0.
OpenSolver setup
Objective $C$24, Min · changing cells $C$16:$H$18 · balance $C$28:$C$33 = $E$28:$E$33 · capacity $C$34:$C$39 ≤ $E$34:$E$39 · $C$40 = $E$40 · engine CBC · tick Sensitivity Analysis.
W02_multiperiod_planning_STUDENT.xlsx. Complete the yellow cells: the balance row for each period, and the objective. Run Solver.Target: total cost 42,080.00 USD.
10
minute break
Then: what the plan you just solved actually says, the measured value of planning ahead, a second product on the same reactor, where the spreadsheet stops being the right tool, and HW1
Lecture block 3 · 45 minutes
What the plan says, what planning ahead is worth,
and where this tool stops.
Activity 2, revealed
| Period | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Demand dt, t | 120 | 150 | 180 | 210 | 160 | 130 |
| Capacity Ct, t | 170 | 170 | 170 | 170 | 170 | 170 |
| Unit cost ct, USD/t | 42 | 42 | 45 | 48 | 46 | 43 |
| Production xt, t | 130 | 170 | 170 | 170 | 160 | 130 |
| Inventory It, t | 30 | 50 | 40 | 0 | 0 | 0 |
| Backlog Bt, t | 0 | 0 | 0 | 0 | 0 | 0 |
Total cost 42,080.00 USD = 41,360.00 production + 720.00 holding + 0 backlog. Opening stock 20 t. Capacity binds in periods 2, 3 and 4 only.
Why the plan pre-builds. Periods 3 and 4 together demand 390 t against a combined capacity of 340 t — a shortfall of 50 t that physically must be made earlier. Closing inventory at the end of period 2 is exactly 50 t: the shortfall, and nothing more. It is not price arbitrage. Producing in period 2 at 42 and holding two months costs 42 + 2(6) = 54 USD/t, worse than making it in period 4 at 48. The plant builds stock in spite of the price signal.
The practical output of the whole model
| Period | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Capacity slack, t | 40 | 0 | 0 | 0 | 10 | 40 |
| Dual, USD/t | 0 | 6 | 9 | 12 | 0 | 0 |
Where the debottlenecking budget goes
Period 4 first, at 12 USD per tonne of extra capacity. Nowhere near periods 1, 5 or 6, where extra capacity is worth exactly zero.
A capital request must name the period, not just the resource.
Binding in a period is not binding across the horizon
Total capacity over six periods is 1,020 t against total demand of 950 t, with 20 t of opening stock. In aggregate the horizon is comfortable. It is only period by period that the plan is tight, and only in three periods of six.
An annual capacity number would have reported no problem at all.
Pyomo returns a negative dual for a ≤ constraint in a minimization, so the magnitude is the cost reduction from one extra tonne. The OpenSolver sensitivity report gives the same magnitudes, and every one is reproduced in the notebook by re-solving with one extra tonne.
The most persuasive number of the week
The myopic benchmark is a rule with no look-ahead: in each period, produce only what is needed now, up to capacity.
| Period | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Production, t | 100 | 150 | 170 | 170 | 170 | 170 |
| Backlog, t | 0 | 0 | 10 | 50 | 40 | 0 |
This is not a straw man
A plant that runs a monthly production meeting, looks at this month’s order book, and fills it, is executing exactly this rule. Nobody in that meeting is being careless. The horizon simply is not on the table.
The comparison
8.4 percent of cost, from no capital, no new equipment and no new people.
Both plans make the same total tonnage: 930 t. The myopic rule falls behind in period 3 and then pays 45 USD/t/month in periods 3, 4 and 5 on a backlog that peaks at 50 t.
Same tonnage, different timing
The teal bars sit above demand in periods 1 and 2 and below it in period 4. The rust bars sit on demand until period 3 and then sit on the capacity line for the rest of the horizon, never catching up until the last month. Same 930 tonnes. 3,860.00 USD apart.
Resource linking
What changes
Every variable and every balance gains a product index: xp,t, Ip,t, Bp,t. The balance and the line capacity are written once per product and are otherwise unchanged.
One genuinely new constraint appears, indexed over time only:
Σp ρp xp,t ≤ Rt for every t
That is pattern 3 and pattern 4 in the same row: a shared resource limiting a sum, whose index spans both product blocks.
| Additive | Binder | |
|---|---|---|
| Line capacity, t/month | 170 | 150 |
| Holding, USD/t/month | 6 | 8 |
| Backlog, USD/t/month | 45 | 60 |
| Reactor ρp, h/t | 1.0 | 1.4 |
| Opening stock, t | 20 | 10 |
Reactor hours available: 330 per month
Just-in-time would need 246.0, 304.0, 362.0, 406.0, 328.0, 270.0 h. Periods 3 and 4 are impossible without shifting work earlier, so the reactor constraint will bind.
The coupling constraint is what makes this one problem
Measured
The uncoupled plan is not a plan at all: it needs 366.0 reactor hours in period 4 and only 330 exist.
Without the shared rows the model is block diagonal
It separates exactly into one independent LP per product. The reactor links the blocks, destroys separability, and creates the real decision: in a period when reactor time is short, which product gets it.
600.86 USD over six months
That is what the plant pays for owning one reactor train instead of two, and it is the figure to weigh against annualized capacity. Sharing is cheap here, because inventory smooths the peaks.
The shadow price of reactor time is periodic, not constant: 0, 5.7143, 9.2857, 12.2857, 0, 0 USD/h. Only periods 2, 3 and 4 bind. Re-solving with 331 hours in period 4 gives 83,918.5714 USD — exactly 12.2857 below the base case, which is the definition of the dual, not a coincidence.
The argument is about auditability, not arithmetic
| Today: 6 periods, 1 product | 52 periods, 3 products, shared reactor | |
|---|---|---|
| Changing cells | 18 | 468 |
| Balance rows | 6 | 156 |
| Capacity rows | 6 | 156 |
| Reactor rows | 0 | 52 |
| Fits on a screen | yes | no |
3 × 3 × 52 = 468 changing cells; 3 × 52 = 156 balance rows; 3 × 52 = 156 capacity rows; 52 reactor rows. These are the closing note on the workbook’s Readme sheet.
None of that is hard. It is simply not auditable
No diffing, no unit tests, no scenario loop. There is no way to prove that the formula in one column of 52 is the same as in the other 51 — and one mis-anchored $ produces a plan the sheet reports as feasible and the plant cannot execute.
Adding a fourth product means rebuilding the sheet. Adding a warehouse limit means inserting 52 rows in the middle and repairing every reference below. Re-solving under 20 demand scenarios means 20 copies of the workbook.
The plan from here
Week 3 stays in the spreadsheet, because a cost matrix is a spreadsheet. Week 4 is the crossover, because pooling multiplies a flow by a quality and a spreadsheet cannot handle it at all. In Pyomo, 6 periods and 52 periods are the same six lines of code.
Two implementations, one number
Spreadsheet · W02_multiperiod_planning
C5:H5 six period headers
C6:H8 demand, capacity, unit cost
C16:H18 changing cells, 18 of them
C28:C33 balance rows, one per period
C34:C39 capacity rows, one per period
C40 B6 = 0
C24 objective, minimize
Adding a period means inserting a column and repairing every formula that crosses it.
Pyomo · W02_modeling1_linear_multiperiod
m.T = pyo.Set(initialize=T, ordered=True)
m.d = pyo.Param(m.T, initialize=...)
m.x = pyo.Var(m.T, domain=NonNegativeReals)
m.I = pyo.Var(m.T, domain=NonNegativeReals)
m.B = pyo.Var(m.T, domain=NonNegativeReals)
m.balance = pyo.Constraint(m.T, rule=...)
m.capacity = pyo.Constraint(m.T, rule=...)
m.cost = pyo.Objective(..., sense=pyo.minimize)
Adding a period means adding one entry to T.
Both routes report 42,080.00 USD, and that agreement is the verification. If the spreadsheet and the notebook ever disagree, the spreadsheet is wrong.
Due Week 4
The instruction that carries the most marks
Write the specification before any algebra.
A correct model with no specification loses marks. A specification that states its assumptions and its start-versus-end convention earns them even where the algebra slips.
Two habits to carry into it
A unit on every symbol, and a bound on every variable. Both are checkable by a reader in thirty seconds, and both catch the errors a solver never reports.
Summary
| Quantity | Value |
|---|---|
| Warm-up optimum | (24, 20) t/week, 28,280.00 |
| Its duals, mix / pur. / fill | 0 / 90 / 430 USD/h |
| Six-period optimal cost | 42,080.00 = 41,360.00 + 720.00 + 0 |
| Production plan, t | 130, 170, 170, 170, 160, 130 |
| Inventory plan, t | 30, 50, 40, 0, 0, 0 |
| Capacity duals, USD/t | 0, 6, 9, 12, 0, 0 |
| One extra tonne in period 4 | 42,068.00 USD |
| Myopic cost, and the gap | 45,940.00 USD |
| Value of look-ahead | 3,860.00 USD, 8.4 % |
| Two products, uncoupled / coupled | 83,330.00 / 83,930.86 USD |
| Cost of sharing the reactor | 600.86 USD |
| 52 periods, 3 products | 468 cells, 156 + 156 rows |
Five things, and reading
C24 reads 42,080.00.Readme sheet.Reading
Rao, Ch. 3, on the formulation of linear programs.
Williams, Ch. 3 and 4. Chapter 4 is the single best treatment of the inventory balance in print, with a long discussion of exactly the modeling choices Activity 1 exposed as ambiguities.
Edgar, Himmelblau and Lasdon, Ch. 7, for the process framing.
Week 3 · 24 August — Networks: transportation, transshipment and process superstructures.
The balance equation you wrote
around a period today
is written around a node next week,
and almost nothing else changes.
Same four patterns. New index set. See you Monday.
Before you go · 45 seconds
Five questions. Anonymous — no name, no email, no login.
The fourth question is the one I actually use.
The two most common answers open next week’s session.
oxidized-challenge-ed9.notion.site

Scan now — I will wait