Overview
ProDex’s planning module is an Advanced Planning and Scheduling (APS) optimizer. Given your demand, inventory, bill of materials, and resource constraints, it calculates an optimal production plan — what to make, when, and in what quantities.
Planning operates over a configurable time horizon broken into intervals (days, weeks, etc.) and balances two objectives:
- Demand fulfillment — meeting demand on time (the lateness loss)
- Inventory targets — keeping inventory inside its goal band (the inventory loss)
Resource capacity is a hard constraint, not an objective. The optimizer never trades demand or inventory against capacity — if there isn’t enough capacity in some interval, the plan stays infeasible until you add capacity, slacken deadlines, or relax constraints.
Setting Up Your Planning Model
A Planning Model is the configuration template for your planning runs. It defines the structure of your planning problem — the horizon, resources, BOMs, variants, and objective weights. You create it once and reuse it across multiple planning runs.
Planning Horizon
Set the number of intervals (n_intervals) and the interval unit (e.g., day, week, month). A 30-day horizon with daily intervals means the optimizer plans 30 days out, day by day.
Resources
Define the resource types available for production — lines, machines, labor pools, etc. Each resource has a per-interval capacity ceiling, defined per (resource, date) on each planning run, not on the model.
Resources connect to BOMs via resource requirements (resource_requirements.json) — a flat array of {bom_id, resource_id, quantity} entries specifying how much of each resource a given BOM consumes per unit produced.
Bill of Materials
Specify which BOMs the planning model includes. The model’s metadata.json carries an explicit boms field — BOMs not listed are invisible to the optimizer, even if they exist in the factory. ProDex classifies materials automatically:
- RAW — leaf materials with no components (inputs only, supplied externally)
- INTERMEDIATE — materials that have components AND are consumed by at least one other BOM — produced internally and consumed by downstream BOMs
- SKU — saleable finished goods that have components and are not consumed by any other BOM
A componentless BOM is RAW regardless of whether anything consumes it.
Variants
Most factories produce the same entity in multiple variants (size, color, grade, configuration). The planning system handles variants through typed attribute predicates — every BOM, demand order, inventory goal, and on-hand record can carry a variant specification.
The optimizer matches a demand line to a BOM by checking that the BOM’s variant condition is a subset of the demand’s variant. A BOM with no variant condition (the default recipe) matches anything for that entity. See BOMs for the variant condition syntax (boolean / discrete_text / discrete_number / range conditions, ANDed together).
Objective Balance (Alpha)
The single parameter that controls the trade-off between the two objectives:
Total Loss = (1 − α) · Demand Lateness + α · Inventory Loss
- α = 0 — pure demand fulfillment: the optimizer only cares about hitting deadlines; inventory targets are ignored.
- α = 1 — pure inventory targets: the optimizer only cares about inventory bands; demand can slip arbitrarily.
- α ≈ 0.5 — balanced; tune per factory.
There is no separate weight for resource utilization — resources are always hard constraints.
Time Decay (Beta, Gamma)
Two damping parameters that down-weight penalties further out in the horizon:
- Beta (β) — time-decay on demand penalties. A demand order missed
d intervals out is penalized by β^d. Default 0.9 — at 30 days out a missed delivery costs ~4% of what the same miss costs today.
- Gamma (γ) — time-decay on inventory goal penalties. Same shape, applied to inventory band violations. Default
0.9.
Lower the decay (closer to 0) when you only care about the near term; raise it (closer to 1) when long-horizon planning matters as much as today.
Tags classify demand orders and inventory goals into categories with their own weights and penalty shapes. This lets you express that some orders are more critical than others, or that some inventory targets carry more risk.
Each tag carries:
weight — multiplier on that tag’s penalty term
hard — boolean. A hard tag promotes the deadline (or inventory target) into a hard constraint; missing it makes the run infeasible
penalty_curvature — controls how aggressively the penalty grows with the size of the miss (linear, sub-linear, super-linear)
buffer — slack window before the penalty kicks in
- Rolling vs. non-rolling (inventory tags only) — non-rolling inventory tags force inventory to zero outside the goal window, useful for time-bounded targets like spoilage. Default is rolling with weight 0.
Custom Constraints
Optionally define additional constraints in custom_constraints/ — for example, minimum production quantities, maximum daily output for a specific SKU, or production sequence rules.
Custom Objectives
Separate from constraints, custom_objectives/ lets you add weighted terms to the objective function — useful when “minimize total cost” or “smooth production across the horizon” is part of your real objective.
Running a Plan
A Planning Run is one execution of the optimizer against your planning model. Each run takes a point-in-time snapshot of your demand and supply inputs.
Demand Orders
What you need to produce. Each demand order specifies:
name — human-readable order name (the filename serves as the ID)
tag_id — which order tag classifies this order
date — due date
skus — list of SKU + quantity entries, each optionally carrying a variant
Demand orders only accept SKU-classified entities. RAW or INTERMEDIATE entities cannot be ordered as demand.
Supply Orders
Raw materials you expect to receive. Each supply order specifies:
name
date — arrival date
materials — list of {entity_id, quantity, variant?} entries
Supply orders only accept RAW-classified entities.
On-Hand Inventory
Current stock at the time of planning, set on the run’s initial_conditions.json alongside the run’s start_date. Each entry is {entity_id, quantity, variant?}.
Inventory Goals
Target inventory levels you want to maintain, with a tag, target quantity, and an acceptable margin band. Penalties grow piecewise-linear outside the band — slopes ±1 inside the buffer, ±7 in the next zone, ±25 in the deep miss zone — so small misses are forgiven and large misses dominate quickly.
Resource Capacity
Per-(resource, date) capacity ceilings for the run.
Missing intervals default to zero, not unlimited. If you forget to specify capacity for a date, the optimizer treats that resource as unavailable on that date — and “why isn’t anything being produced?” is usually a missing capacity row, not a solver bug.
There is no implicit baseline; you define capacity from scratch per (resource, date). Plan for downtime by omitting or zeroing the relevant intervals; plan for surge capacity by raising the ceiling.
Before running the optimizer, validate. The validate_inputs step catches the most common breakages:
- Missing or unreferenced BOMs in the model’s
boms list
- Variant condition violations (mutually exclusive variants, references to undefined attributes)
- Material classifications inconsistent with how entities are used (e.g., a non-SKU appearing in a demand order)
- BOM graph cycles
Fix validation errors before optimizing — they almost always indicate a data setup issue, not a solver issue.
Optimization
Once your inputs are ready, run the optimizer. ProDex returns synchronously for fast plans (under ~30 seconds). Longer runs return a job_id; poll get_optimization_results to retrieve the plan when it completes.
Interpreting the Output
Production Plan
A day-by-day (or interval-by-interval) schedule of what to produce: entity, variant, date, and quantity. This is the primary output.
Order Fulfillments
Each demand order’s fulfilled field is boolean — every order is either fully covered by the plan or it isn’t. There is no “partially satisfied” middle state at the order level.
Feasibility
If the optimizer cannot find a valid plan, it reports infeasible with a reason. The most common cause is a hard deadline (a demand order on a hard tag) that cannot be met given the resource capacity and BOM lead-up. Other causes include missing supply for raw materials and over-constrained custom constraints.
To recover: relax the hard tag, add capacity in the constrained intervals, extend the horizon, or remove the over-restrictive constraint.
KPIs
- Solver status — optimal, feasible, or infeasible
- Objective value — the weighted total loss the optimizer minimized
- Optimality gap — how close the solution is to the theoretical best (lower is better)
- Computation time — solver wall-clock time, in seconds
Adjusting and Re-Running
Planning is iterative. After reviewing results:
- Adjust α if the lateness/inventory balance doesn’t match your priorities
- Re-tune the per-tag weights if specific orders or goals deserve more or less emphasis
- Add or modify custom constraints / custom objectives
- Update demand or supply inputs as new information arrives
- Run again and compare against the previous plan
Results from multiple planning runs are stored and can be compared side by side.