How far can you trust the numbers?
Every case below runs the app's own engine (src/plan.js, src/constrained.js,
src/invest.js) on a problem with a known answer — an exact relationship, a conservation law,
or the published example — and checks the result against a stated tolerance. The results shown were
recorded with node tierwise/tests/validation.test.js; press Re-run to compute them
again in your browser, from the same code, using the same HiGHS solver.
—
Cases
Numerical-stability lessons
A few things learned the hard way while building the LP, kept here because the mistakes were more instructive than the successes:
- Every balance needs a stock. Lead, release and transit times are fractions of a month, split across two months by weight. Without a stock to absorb the fraction, the LP is forced into exact month-by-month equalities between flows with different weights, and the cheapest way to satisfy them is to oscillate shipments between a high and a low value every other month (an IIR-filter instability, not a bug in the sense of wrong code — a legitimate optimum of a badly posed problem). Every site carries a small stock (finished stock at every production site and warehouse, input stock at every CMO that buys, a buffer at every consumption point) specifically to remove this degree of freedom.
- Scale the quantities. The example network's flows are in the hundreds of thousands of packs a month; solved directly, HiGHS's numerical tolerances (tuned for values near 1) make the interior-point method far less reliable. Every quantity in the LP is in units of 10,000 pack-equivalents instead.
- No HiGHS LP-format variable name may start with
efollowed by digits — the reader parsese12as a floating-point exponent, not a variable. Every variable prefix in the code was chosen to avoid this (the re-routing variable isg, not the more obviouse). - The solver fallback cascade exists for a reason. The interior-point method without crossover is
several times faster than simplex on a problem this size, but occasionally fails numerically on an
unusual network.
solve()tries interior-point without crossover, then with crossover, then the dual simplex, and only reports failure if all three do. - The warm-up and run-out exist to stop the horizon's edges from distorting the plan. Without them, the LP would either start with no pipeline filled (starving the first months) or end by building stock it will never use (since nothing after the horizon needs it). The warm-up has the first month's demand and fills the slowest pipeline; a site may make at most 110% of what the first month needs, so the plan does not quietly pre-build stock before it even starts.
- The capacity investment optimiser needed a different model, not a longer timeout. The first
attempt wrapped the month-by-month LP (~20,000 rows) in a mixed-integer search over build decisions; even
one binary variable took over a minute to find any answer better than "build nothing", because every
branch re-solves the whole relaxation. Raising the time budget made this slower, not more useful. The fix
was a smaller, purpose-built annual-resolution model (
src/invest.js), not a bigger timeout. - A quieter bug: annual rates look like annual totals until you check. Early in building that annual model, a pack of unmet demand was costed once per year-slot rather than once per pack — the model was comparing a one-time capex against roughly a twelfth of the real annual cost of a shortfall, so it almost always chose not to invest, even when investing was worth tens of millions of dollars. The "builds when the margin clearly beats the capex" case below exists specifically to catch a regression of this kind.
Limits — what the numbers don't cover
- One product, fixed sourcing shares. The requirement plan and the plan within capacity both assume a single product and sourcing shares that do not change over the horizon.
- Inventory only at warehouses in the requirement plan; the plan within capacity adds stock at every site, but only to absorb timing, not as a general multi-echelon inventory policy.
- The investment optimiser is a screening model, not the operational plan. It represents each year by its annual total, not month by month, so it will not catch a shortfall that is real within a year but averages out over it. Candidates are expansions of existing sites only; a greenfield site is not yet a candidate.
- All figures in the example network are illustrative — a fortified milk powder chain built to exercise latitude, seasonality and multi-tier sourcing, not a real company's data.
Reproduce
node tierwise/tests/plan.test.js # 61: the engine, tracing, capacity to add, custom tiers node tierwise/tests/graph.test.js # 41: sites and routes, the tier rule, column order, problems, parameters, undo node tierwise/tests/geo.test.js # 16: the world projection, great-circle routes over the date line, map views node tierwise/tests/io.test.js # 49: CSV / Excel / JSON round trips, routes tables, custom parameters, errors by row node tierwise/tests/i18n.test.js # 19: every text, page key and engine message in Spanish and Danish node tierwise/tests/tour.test.js # 443: every step in three languages, and every claim the tutorial makes cd tierwise && npm install --no-save highs@1.15.3 node tests/constrained.test.js # 41: the plan within capacity on chains with known answers, and the example node tests/invest.test.js # 23: the investment optimiser on chains with known answers, and the example node tests/validation.test.js # this report → tests/validation-results.json