Optimizing Reorder Points with a Noisy Simulator
A genetic algorithm picks reorder points for five products. When I re-measured its best answers with 200 runs, they were 11 to 15 percent worse than it said.
- Demo simopt.netlify.app
Written in September 2025, when I finished the optimizer behind the simulation-optimization demo. Rerun on 8 October 2026 with the same code; the numbers below are from that rerun.
The inventory simulator tells you what one set of reorder points costs. The next question is which reorder points to choose. With five products and a noisy simulator there is no formula, so I put a genetic algorithm (GA) around the simulation. My earlier ANN metamodel paper (code) took the other route: learn a cheap model of the simulation first.
The setup
Five products with fixed parameters. The GA only chooses the reorder point r of each one.
| Product | Policy | Customers/period | Lead time (mean) | Range for r |
|---|---|---|---|---|
| A | (s,S), S = 50, backorders | 10 | 3 | 0 to 49 |
| B | (r,Q), Q = 15, lost sales | 8 | 2 | 0 to 69 |
| C | (s,S), S = 40, backorders | 6 | 4 | 0 to 39 |
| D | (r,Q), Q = 20, lost sales | 12 | 2.5 | 0 to 59 |
| E | (s,S), S = 30, backorders | 5 | 3.5 | 0 to 29 |
For an (r,Q) product the upper bound is the expected lead-time demand plus three standard deviations plus 25 percent.
GA settings are the API defaults: population 40, crossover rate 0.9, mutation rate 0.3 (adaptive), tournament of 2, elitism 2, 15 percent random immigrants per generation. It stops after at least 25 generations once the best cost has not improved for 10 generations (a change below 0.0005), or at the time limit.
The experiment
I ran the GA three times with different seeds. Then I re-measured each best solution with 200 fresh simulation runs, and did the same for the midpoint of every search range as a naive baseline.
| Run | Reorder points (A to E) | Cost the GA reported | Cost re-measured (200 runs) |
|---|---|---|---|
| Midpoints | 24, 34, 19, 29, 14 | 399.2 | |
| GA, seed 1 | 27, 6, 21, 11, 23 | 329.3 | 366.3 |
| GA, seed 2 | 27, 11, 28, 3, 19 | 322.9 | 360.2 |
| GA, seed 3 | 33, 13, 16, 14, 24 | 327.8 | 376.9 |
Each GA run stopped on convergence after 27 to 39 generations, 1,052 to 1,512 evaluations and 7 to 9 seconds.
What the numbers say
The GA’s own score is optimistic. It judges each candidate on 3 noisy runs, so the winner is partly the candidate that got lucky draws. Re-measured, every winner was 11 to 15 percent more expensive than reported. The real gain over the naive midpoints is 6 to 10 percent, not the 18 percent the GA’s own numbers suggest.
Different seeds, different answers, similar cost. Product D’s reorder point came out as 11, 3 and 14. The cost surface is flat and noisy, so the GA stops on whichever plateau it finds first.
The cost function pushes lost-sales products toward stockouts. B and D end with low reorder points and fill rates of 36 to 51 percent. The model has no revenue, so losing a sale (cost 5) is cheaper than serving it (about 5.1). I explain this in the simulation note.
What went wrong
The seed does not make a run repeatable. The GA seeds its own random choices, but every fitness evaluation calls the simulator with base_seed=None. Calling the API twice with "seed": 1 gave [41, 3, 29, 7, 17] and [39, 12, 34, 12, 20]. It also means two candidates are never compared on the same demand. The fix is common random numbers: pass the same seeds to every candidate’s replicates. That makes runs repeatable and the comparison fairer.
/simulate ignores its seed field. It is accepted and then dropped.
My “30 seconds” change did nothing. In September 2025 I raised the time limit from 20 to 30 seconds in optimization.py, but the API request model still defaults to 20, so the API never used it. With these defaults it does not matter: every run converged in under 10 seconds.
Reporting the search score as the result. The demo shows the GA’s own best cost. It should re-measure the final candidates with many more runs before showing a number.
How to repeat this
The code is not public yet; the API and the demo are.
# One GA run (about 8 seconds). Expect different reorder points each time; see above.
curl -s -X POST https://api-simopt.oploy.eu/optimize \
-H "Content-Type: application/json" -d '{"seed": 1}'
# Re-measure a solution with 50 runs (the API's maximum per call).
curl -s -X POST https://api-simopt.oploy.eu/simulate \
-H "Content-Type: application/json" \
-d '{"reorder_points": {"Product A": 27, "Product B": 6, "Product C": 21, "Product D": 11, "Product E": 23},
"replicates": 50, "horizon": 60}'
- Run the optimizer a few times and note
best_costandbest_solution. - Re-measure each
best_solutionwith four calls to/simulateat 50 replicates and average theobjectivevalues. - Compare: the re-measured cost should come out about 10 to 15 percent above
best_cost.
The fill rates of backordered products (A, C, E) are wrong until the fix in the simulation note is deployed. The idea of replacing the expensive simulator with a learned model is in my ANN metamodel paper.