manufacturing.batch-size
Economic batch quantity: the EOQ for a batch made at a finite production rate, in whole units.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 20 tests, run in TypeScript, Python and Rust.
What it does
The economic batch quantity (EBQ, also called the economic production quantity or EPQ): the batch size at which yearly set-up cost equals yearly holding cost when stock is made in-house at a finite rate rather than delivered all at once:
EBQ = sqrt( 2 * D * S / (H * (1 - D / P)) ) = sqrt( 2 * D * S * P / (H * (P - D)) )
For example
economic_batch_quantity(1,000, 2,000, £5.00, £0.50, half-up)→ 200 demand 1000, rate 2000, set-up £5, holding 50p: EBQ is exactly 200 (EOQ would be 141)economic_batch_quantity(1,000, 2,000, £5.00, £0.50, up)→ 200 a perfect square stays put when rounding upeconomic_batch_quantity(10,000, 40,000, £100.00, £2.00, half-up)→ 1,155 demand 10000, rate 40000, set-up £100, holding £2: sqrt(1333333.33) = 1154.70 rounds half-up to 1155
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def economic_batch_quantity(annual_demand: int, annual_production_rate: int, setup_cost: Money, holding_cost: Money, mode: RoundingMode) -> int
| annual_demand | int | units used per year, not negative |
| annual_production_rate | int | units the process makes per year while running; more than annualDemand |
| setup_cost | Money | the fixed cost of setting up one batch, whatever its size |
| holding_cost | Money | the cost of holding one unit for a year; same currency, more than zero |
| mode | RoundingMode | how the square root becomes whole units: half-up, half-even, down or up |
| returns | int | the batch size in whole units |
Your code names it in one line, in the file that uses it
from fune.manufacturing.batch_size import economic_batch_quantity # manufacturing.batch-size@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .math_integer_sqrt import integer_sqrt ← from math.integer-sqrt ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency ← from money.amount ^1.0.0 · built alongside by fune
MAX_SAFE = 2**53 - 1
MAX_I64 = 2**63 - 1
def economic_batch_quantity(
annual_demand: int,
annual_production_rate: int,
setup_cost: Money,
holding_cost: Money,
mode: RoundingMode,
) -> int:
"""Economic batch quantity, sqrt(2DSP / (H(P - D))), in whole units.
The root is never taken in floating point: the floor is an exact integer
square root and the rounding decision compares whole numbers.
"""
if isinstance(annual_demand, bool) or not isinstance(annual_demand, int) or annual_demand < 0 or annual_demand > MAX_SAFE:
raise ValueError("annualDemand must be a whole number of units, not negative, received %r" % (annual_demand,))
if (
isinstance(annual_production_rate, bool)
or not isinstance(annual_production_rate, int)
or annual_production_rate <= annual_demand
or annual_production_rate > MAX_SAFE
):
raise ValueError(
"annualProductionRate must be a whole number greater than annualDemand, received %r"
% (annual_production_rate,)
)
assert_same_currency(setup_cost, holding_cost)
if setup_cost.minor < 0:
raise ValueError("setupCost must not be negative, received %d" % setup_cost.minor)
if holding_cost.minor <= 0:
raise ValueError("holdingCost must be greater than zero, received %d" % holding_cost.minor)
d, p, s = annual_demand, annual_production_rate, setup_cost.minor
eight = 8 * d * s * p
if eight > MAX_I64:
raise ValueError(
"annualDemand, setupCost and annualProductionRate are too large: 8 x D x S x P must stay within 2^63 - 1"
)
two = 2 * d * s * p
m = holding_cost.minor * (p - d)
n = integer_sqrt(two // m)
odd = 2 * n + 1
if mode == "down":
return n
if mode == "up":
return n if n * n * m == two else n + 1
if mode == "half-up":
return n + 1 if odd * odd * m <= eight else n
if mode == "half-even":
half = odd * odd * m
if half < eight:
return n + 1
if half > eight:
return n
return n if n % 2 == 0 else n + 1
raise ValueError('unknown rounding mode "%s"' % (mode,))Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add manufacturing.batch-size
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./manufacturing.batch-size-1.0.0-python.fune, or fetch it from a terminal with fune pull manufacturing.batch-size@1.0.0:python.
The whole function, every language, is one file too: manufacturing.batch-size-1.0.0.fune, 16,906 bytes, sha256 3df9331cc9df8c218c15180f723cc8186675afb8e740d946e9b518896efdd2e5. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before manufacturing.batch-size
after — your function gets the result and the arguments, and returns the final result.
# fune: after manufacturing.batch-size
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace math.integer-sqrt in manufacturing.batch-size
# fune: replace math.round-div in manufacturing.batch-size
# fune: replace money.amount in manufacturing.batch-size
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show manufacturing.batch-size --steps.
# fune: step manufacturing.batch-size after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| demand 1000, rate 2000, set-up £5, holding 50p: EBQ is exactly 200 (EOQ would be 141) | 1,000, 2,000, £5.00, £0.50, half-up | → | 200 |
| a perfect square stays put when rounding up | 1,000, 2,000, £5.00, £0.50, up | → | 200 |
| demand 10000, rate 40000, set-up £100, holding £2: sqrt(1333333.33) = 1154.70 rounds half-up to 1155 | 10,000, 40,000, £100.00, £2.00, half-up | → | 1,155 |
| the same rounded down is 1154 | 10,000, 40,000, £100.00, £2.00, down | → | 1,154 |
| a rate close to demand makes the batch much larger: sqrt(180000) = 424.26 | 1,200, 1,500, £45.00, £3.00, half-up | → | 424 |
| the same rounded up is 425 | 1,200, 1,500, £45.00, £3.00, up | → | 425 |
| an exact half, sqrt(6.25) = 2.5, rounds half-up to 3 | 25, 50, £0.01, £0.16, half-up | → | 3 |
| an exact half, sqrt(6.25) = 2.5, rounds half-even to 2 | 25, 50, £0.01, £0.16, half-even | → | 2 |
| an exact half, sqrt(12.25) = 3.5, rounds half-even to 4 | 49, 98, £0.01, £0.16, half-even | → | 4 |
| below the half, sqrt(6) = 2.449, rounds half-up to 2 | 24, 48, £0.01, £0.16, half-up | → | 2 |
Show the other 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| zero demand needs no batch | 0, 100, £5.00, £0.50, half-up | → | 0 |
| a free set-up gives a batch of zero | 1,000, 2,000, €0.00, €0.50, up | → | 0 |
| a production rate equal to demand is an error | 1,000, 1,000, £5.00, £0.50, half-up | → | error: annualProductionRate must be a whole number greater than annualDemand |
| a zero holding cost is an error | 1,000, 2,000, £5.00, £0.00, half-up | → | error: holdingCost must be greater than zero |
| a negative set-up cost is an error | 1,000, 2,000, -£5.00, £0.50, half-up | → | error: setupCost must not be negative |
| negative demand is an error | -1, 2,000, £5.00, £0.50, half-up | → | error: annualDemand must be a whole number of units, not negative |
| fractional demand is an error | 10.5, 2,000, £5.00, £0.50, half-up | → | error: annualDemand must be a whole number of units, not negative |
| mixed currencies are an error | 1,000, 2,000, £5.00, €0.50, half-up | → | error: currency mismatch |
| 8DSP beyond 2^63 - 1 is an error | 1,000,000,000, 2,000,000,000, £100,000.00, £0.50, half-up | → | error: 8 x D x S x P must stay within 2^63 - 1 |
| an unknown rounding mode is an error | 1,000, 2,000, £5.00, £0.50, nearest | → | error: unknown rounding mode |
More from the author
`D` is annual demand, `P` the annual production rate (what the process would make in a year of running), `S` the set-up cost of one batch and `H` the cost of holding one unit for a year.
**Why not just EOQ.** Because stock is used while the batch is still being made, the peak stock is `Q * (1 - D/P)`, not `Q`, so the batch is larger than the economic order quantity by `sqrt(P / (P - D))`. With demand 1,000, rate 2,000, set-up £5 and holding 50p, EOQ (`inventory.eoq`) says 141; the EBQ is 200. As `P` grows the EBQ falls back to the EOQ.
**Whole units, rounded exactly.** As in `inventory.eoq`, the root is never taken in floating point. The floor comes from `math.integer-sqrt` applied to `floor(2DSP / (H(P - D)))`, and the rounding decision compares whole numbers, so an exact half (sqrt(6.25) = 2.5) goes by `mode`, not by the language:
- `down`: the floor; `up`: the floor plus one unless the ratio is a perfect square - `half-up`: plus one when `(2n + 1)^2 * H(P - D) <= 8DSP` - `half-even`: as half-up, but an exact half goes to the even neighbour
**Limits.** `8 * D * S * P` must stay within 2^63 - 1 and the ratio under the root within 2^53 - 1 (the range `math.integer-sqrt` takes); both are far beyond real plants. Money is `Money` in minor units, set-up and holding in the same currency; the currencies cancel, so the answer is a plain unit count.
Zero demand gives 0. The production rate must be greater than demand (at or below it the process can never build stock and the formula has no answer), holding cost must be positive, and neither demand nor set-up cost may be negative.
Sources: E. W. Taft, "The most economical production lot", The Iron Age 101, 1918; any operations-management text, e.g. Slack, Brandon-Jones and Johnston, *Operations Management*, chapter on inventory planning (the EBQ model).
Files
| Path | Bytes |
|---|---|
| README.md | 2,196 |
| impl/python.py | 2,345 |
| impl/rust.rs | 3,232 |
| impl/typescript.ts | 2,287 |
| vectors.json | 3,935 |