NEW: The Decision Factory — a novel about decisions under uncertainty. Get it on Amazon
Optimization · · Adam DeJans Jr.

The Atomic Move Defines the Algorithm

Why practical optimization under MOQs, case packs, shared vendors, and nested constraints starts by defining the smallest legal decision change—not by choosing a search algorithm.

optimizationsupply-chaininventorymoqheuristicssimulation-optimizationdecision-sciencesequential-decisions

A lot of optimization discussions start too late.

The team asks whether to use MILP, dynamic programming, Bayesian optimization, local search, reinforcement learning, or some custom greedy heuristic. People compare algorithms before agreeing on what a legal decision change actually is.

That sounds like an implementation detail.

It is often the heart of the problem.

Suppose you are building a buying system. There are thousands of SKUs, multiple regions, upstream and downstream inventory positions, case packs, vendor minimum order quantities, shared budgets, capacity limits, and uncertain demand. You want to improve the current order by repeatedly asking a simple question:

What should I add next?

If every SKU can be increased by one unit independently, the answer is conceptually easy. Evaluate the marginal value of one more unit for each SKU and choose the best legal increment.

Real supply chains rarely work that way.

A SKU may come in packs of 24. A vendor may require $10,000 of total spend. A product family may have a 500-unit minimum. A regional order may be legal only if an upstream commitment is also opened. One SKU may contribute toward several nested thresholds. Some constraints matter only when a branch is activated.

Now the smallest meaningful move may not be one unit.

It may be an entire bundle.

The algorithm you thought was ranking SKUs is actually choosing among legal state transitions.

That distinction changes everything.

Start with the decision state

Let the current decision be represented by a vector q, where q_i is the order quantity for SKU i.

A naive incremental algorithm considers moves like:

q_i <- q_i + 1

or, with case packs:

q_i <- q_i + p_i

where p_i is the pack size.

This works only when that increment is independently feasible.

Suppose SKU A and SKU B belong to the same vendor, and the vendor has a 1,000-unit MOQ. The current order for that vendor is zero.

Adding one case of SKU A creates an illegal order. So does adding one case of SKU B.

But adding 600 units of A and 400 units of B may be legal.

The value of the move cannot be evaluated by asking whether the next case of A is good in isolation. The branch is closed. Opening it requires a bundle large enough to cross the activation threshold.

Once the vendor is already open, the situation changes. If the order is at 1,200 units, adding another case of A may be perfectly legal by itself.

The same SKU can therefore have different atomic moves depending on the current state.

That is the key idea:

The smallest legal decision change is state-dependent.

Closed branches and open branches are different problems

Many supply chain constraints have an activation structure.

A vendor, product family, lane, warehouse, production line, or contract can be thought of as closed when nothing is assigned to it. Once used, some threshold or fixed requirement must be satisfied.

For a group m with threshold Q_m, a simplified rule might be:

  • closed: all descendant quantities equal zero
  • open: total qualifying quantity is at least Q_m

If a branch is open and safely above its threshold, local increments may be small. You can add a pack, move a few units, or change one SKU.

If the branch is closed, the admissible move is not a small increment. It is an opening bundle.

That bundle must satisfy the local threshold and any parent thresholds that become active because of the move.

This becomes especially important with nested structures.

Imagine this hierarchy:

  • Supplier
    • Brand family
      • Product group
        • SKU

The supplier has a dollar MOQ. The brand family has a unit MOQ. Each SKU has its own case pack.

Opening one SKU may require satisfying all three levels at once.

A one-SKU-at-a-time heuristic can conclude that nothing is attractive because every isolated increment is infeasible. The real opportunity may be a coordinated bundle of several SKUs that collectively opens the path.

The search algorithm is not failing because it is greedy.

It is failing because its move definition does not match the constraint structure.

The atomic move should follow the binding constraint

A useful design rule is to let the atomic move adapt to the constraint that currently blocks progress.

Inside an already-open branch, the move might be:

  • add one case of a SKU
  • shift one pack between regions
  • move one unit of budget from one item to another
  • extend one buying horizon by one step

For a closed branch, the move might be:

  • open a vendor with a feasible SKU bundle
  • activate a production line with enough assigned volume
  • open a lane with enough consolidated flow
  • create a regional assortment that satisfies all activation rules

The atomic move is therefore not always one variable change.

It is the smallest economically meaningful transition from one feasible state to another.

This is a more useful way to think about practical heuristics than arguing about whether greedy methods are good or bad.

A greedy algorithm with the right moves can be extremely strong.

A sophisticated search method with meaningless moves can spend most of its time exploring garbage.

A nested MOQ example

Suppose a supplier serves three SKUs.

  • SKU A: pack size 100
  • SKU B: pack size 50
  • SKU C: pack size 25

The supplier MOQ is 1,000 units.

The current order is zero.

You estimate the risk-adjusted economic value of different quantities using uncertain demand scenarios. Small quantities of each SKU look attractive, but none individually justifies buying 1,000 units.

A naive marginal ranking may look like this:

  1. next pack of A: high value
  2. next pack of C: high value
  3. next pack of B: moderate value

But none of those moves is feasible from the current state.

The real candidates are bundles such as:

  • 600 A, 250 B, 150 C
  • 400 A, 400 B, 200 C
  • 800 A, 100 B, 100 C

Each candidate opens the supplier and satisfies the MOQ.

The algorithm should compare the incremental value of the full opening bundle against the resources it consumes.

Once one bundle is accepted and the supplier is open, the move set changes. Now the next candidate can be one more pack of A, B, or C.

The search moves from coarse activation decisions to fine allocation decisions.

This coarse-to-fine behavior is not a hack. It reflects the economics of the system.

Generate opening bundles deliberately

The hard part is often generating good opening bundles without enumerating every combination.

For small groups, you can solve a local integer program. The local subproblem asks for a feasible bundle that crosses the required threshold while maximizing some estimate of incremental value.

For structured groups, dynamic programming can work well. Quantities are discretized by pack size or contribution to the threshold, and the algorithm constructs high-value combinations near the opening requirement.

For very large groups, practical candidate generation may use several strategies:

  • build a bundle from the highest marginal-value items until the threshold is met
  • generate multiple bundles under different economic weights
  • seed from the current forecast, historical mix, or recent accepted order
  • create conservative and aggressive opening candidates
  • use a local MILP to generate the best few alternatives
  • perturb the bundle and retain diverse candidates

You usually do not need every possible opening bundle.

You need a useful set of legal moves that exposes economically different choices to the global search.

This is an important separation of concerns.

The local bundle generator answers:

How could this branch be opened?

The global optimizer answers:

Which opening or incremental move should receive scarce resources next?

Rank moves by adjusted marginal value

Once legal moves exist, the search can compare them globally.

For a candidate move a from state q, evaluate something like:

Delta(a | q) = U(q + a) - U(q)

where U is the economic utility of the decision.

That utility might include expected margin, lost-sales cost, holding cost, markdown exposure, expedite risk, service consequences, and a risk adjustment for bad outcomes.

If cash, capacity, cube, labor, or other resources are scarce, raw marginal value is not enough.

A useful adjusted score is conceptually:

adjusted value = marginal utility - resource shadow charges

The shadow charges can come from Lagrangian multipliers, explicit business hurdle rates, or other estimates of opportunity cost.

A move that creates $20,000 of expected value but consumes all remaining budget may be worse than several smaller moves that create more total value together.

The global search should compare legal moves on the economics that matter across the whole system.

This is where a priority queue or max-heap can be useful. Candidate moves are ranked by adjusted marginal value. The best move is selected, the state changes, and only affected candidates are recomputed.

But there is an important warning.

Marginal values are state-dependent too.

After opening a vendor, the value of nearby moves changes. After consuming budget, the opportunity cost of cash changes. After allocating inventory to one region, the value of another region changes. After satisfying a parent MOQ, previously expensive child moves may become cheap.

A stale ranking can become a bad ranking quickly.

Recompute locally, not globally

The obvious implementation recomputes every candidate after every accepted move.

That is often unnecessary.

A move usually affects a limited neighborhood:

  • the same SKU across scopes or echelons
  • the same vendor
  • the same MOQ subtree
  • shared budget or capacity resources
  • products competing for the same uncertain demand

Maintain a dependency map.

When a move is accepted, identify the affected family, subtree, resource pool, or coupled decision block. Recompute those candidates and leave unrelated parts of the heap alone.

This is one of the practical differences between a prototype and a production optimizer.

The mathematics may be elegant, but runtime is often dominated by repeated state construction, simulation, tree building, data movement, and unnecessary reevaluation.

Profile the full pipeline.

Do not assume the search loop is the bottleneck because it looks like the algorithmic part.

Use common scenarios when comparing moves

If utility is estimated with simulation, candidate comparisons can become noisy.

Suppose opening Vendor A appears $5,000 better than opening Vendor B. If the two candidates were evaluated on different random demand samples, part of that difference may be simulation noise.

Use common random numbers.

Evaluate competing moves on the same demand paths, lead-time realizations, disruptions, and other uncertain events whenever possible.

Then the comparison reflects the decision difference rather than luck in the random draw.

This matters even more for small marginal moves. If the expected improvement is modest relative to simulation variance, the ranking can become unstable.

You can also use a two-stage evaluation process:

  1. screen many moves with a cheaper approximation or fewer scenarios
  2. reevaluate the best candidates with more scenarios before committing

The goal is not maximum simulation precision everywhere.

The goal is enough precision to choose among decisions.

Add decrement and swap moves when the search gets stuck

Pure addition works well when building a solution from zero or from a conservative baseline.

It can also trap the search.

An early opening bundle may later look weak. A vendor may have been opened with the wrong mix. Budget may be locked into a mediocre decision that prevents a better combination elsewhere.

Useful neighborhoods include:

  • decrement a quantity while preserving feasibility
  • close a weak branch and release its resources
  • swap one SKU for another inside an open MOQ
  • shift quantity between scopes
  • replace one opening bundle with another
  • exchange budget between vendors

These moves are more expensive to evaluate, so they do not need to run constantly.

A practical pattern is:

  1. build with fast additive moves
  2. improve with targeted swaps and decrements
  3. run a final repair and consolidation pass

The first phase creates a strong feasible solution quickly. The second corrects path dependence. The final phase makes the decision executable.

Feasibility repair should not be an afterthought

Many real systems use approximate search because the full coupled problem is too large, too nonlinear, or too expensive to evaluate exactly.

That does not excuse returning an illegal purchase order.

The final decision should pass explicit checks for:

  • pack multiples
  • nested MOQ activation
  • vendor minimums
  • budget
  • capacity
  • lane or dock limits
  • assortment rules
  • echelon balance
  • calendar restrictions

If the search uses relaxed or approximate moves, include a repair step that is itself economically aware.

Do not fix a $100 feasibility violation by destroying $1 million of value because the cleanup script removes quantities alphabetically.

Repair is part of the optimizer.

The move set is also a management choice

There is an organizational consequence to all of this.

The moves your system allows define what kinds of change the business is willing to consider.

If the optimizer can only adjust one SKU at a time, it cannot discover coordinated vendor openings.

If it can only optimize one region at a time, it cannot reason about shared MOQ economics.

If it can only add inventory, it cannot reconsider a bad early commitment.

If it cannot change buying cadence, then cadence has been fixed as policy before optimization begins.

Every search space contains assumptions about decision ownership.

Who is allowed to open a supplier?

Who can move inventory across regions?

Who owns the shared budget?

Who is accountable when a globally better decision makes one local KPI worse?

A technical team can build a globally coupled optimizer and still fail because the organization only authorizes local moves.

That is not a solver problem.

The decision architecture and the organizational architecture disagree.

Debug moves before tuning the algorithm

When a custom optimizer behaves badly, inspect the move system before tuning parameters.

Ask:

  1. Can every important business decision be reached through the available moves?
  2. Are legal coordinated decisions represented as coordinated moves?
  3. Does the move size change when constraints activate or deactivate?
  4. Can the search reverse a bad early decision?
  5. Are candidate values recomputed when the relevant state changes?
  6. Are competing moves evaluated on common uncertainty scenarios?
  7. Are shared resources priced consistently across local subproblems?
  8. Can the final solution be repaired without destroying its economics?

A search algorithm can only choose among the options you give it.

If the right decision is not reachable, more iterations will not help.

If the right decision requires crossing a temporary infeasible state, a feasibility-preserving one-variable neighborhood may never find it.

If the important choice is a bundle, ranking individual units is solving the wrong problem.

The practical lesson

Optimization practitioners spend a lot of time discussing objectives, constraints, algorithms, and solver settings.

Those all matter.

But in large operational systems, one of the most consequential design choices is often quieter:

What is the smallest change the optimizer is allowed to make?

For simple problems, the answer may be one variable increment.

For real supply chains, it may be a pack, a bundle, a vendor opening, a cross-region shift, a policy parameter change, or a coordinated move across several echelons.

Define that transition correctly and many algorithms become useful.

Define it poorly and even sophisticated optimization can spend enormous effort searching a world where the best business decision does not exist.

Before choosing the search algorithm, define the move.

The move defines the search space.

And the search space defines what the optimizer can ever learn to do.