Plan and allocate production items to plants subject to capacity and compatibility rules, while balancing practical preferences like keeping the same model on as few plants as possible, keeping the same customer order together, and avoiding tiny lots.
This project builds a single optimization and lets you tune its behavior with clear weights and thresholds. You can feed inputs from JSON or generate them from an Excel file and get back a detailed JSON result plus Markdown tables and an optional visualization.
Given:
- Plants, each with a maximum producible quantity and the set of model names it can produce.
- Items to produce, each with a model family, model name, submodel, required quantity, due date, and order id.
Find a feasible allocation of items to plants that:
- Never exceeds a plant’s capacity.
- Only sends an item to a plant that can produce its model.
- Uses weights to prioritize which items to place (e.g., urgent due dates, larger quantities).
- Penalizes splitting the same model across many plants and splitting the same order across many plants.
- Optionally minimizes the total number of plants used.
- Encourages meeting a per-plant minimum batch size for each model (soft threshold), or enforces it as a hard rule (hard threshold).
The outcome is a single, best-scoring plan under a time limit.
main.py— CLI entrypoint: loads inputs, reads settings, builds objective specs, calls the optimizer, prints Markdown tables, and writes a JSON result.optimizer.py— Core optimization logic: builds and solves the allocation model, then decodes the solution into friendly structures and diagnostics.datatypes.py— Data class for per-item objective terms (ObjectiveSpec).input_loader.py— Loads and validates JSON inputs (plants and items), maps due dates into urgency scores, and reads settings.inputs_from_excel_loader.py— Reads items and plants from an Excel file and produces JSON in the shapes the loaders expect.utilities.py— Helpers for normalization, reporting, and decoding the solver result into tables and tallies.tests/— Unit tests covering constraints, soft/hard minimum behaviors, and weight sensitivity.visualization/visualize.html— A small viewer to load an output JSON and see per‑plant histograms of allocated quantities by model.scripts/run_allocation-from-json.shandscripts/run_allocation-from-excel.sh— Example launchers (work in bash shells, including Git Bash on Windows).
- Load inputs
- From JSON:
input_loader.load_plants_arrays()reads plant names, capacities, and allowed models.input_loader.load_items_arrays()reads items (flat schema) and computes a per‑item due‑date “boost” score in [0,100] from the due date. - From Excel:
inputs_from_excel_loadercreates JSONs first; then the JSON loaders above are used. - Settings:
input_loader.load_Settings()validates all required weights, thresholds, andtime_limit_s. Optionalrandom_seedfixes pseudo‑randomness for reproducibility.
- Build objectives (in
main.py)
- Three per‑item additive terms are created via
ObjectiveSpec:- fill: [1, 1, …] (encourages placing more items)
- due_date_boost: computed from due dates (encourages urgent items)
- quantity: item quantities (encourages pushing more total quantity)
- Each term has a weight from
settings-1.jsonand is normalized so weights are comparable.
- Optimize (in
optimizer.py)
- Unsupported items (whose model isn’t producible by any plant) are filtered upfront. If everything is unsupported, a clear error is raised.
- Constraints and the score are assembled (details below) and solved within
time_limit_s. Ifrandom_seedis provided, the search becomes reproducible.
- Decode and report (in
utilities.py)
- The chosen assignments are turned into:
- An
itemsarray with status per item: allocated, unallocated, or unsupported model. - Per‑plant and per‑model summaries, tallies, and markdown tables.
- Detailed
objective_breakdownwith achieved sums and scaling coefficients for transparency.
- An
- Persist/visualize
- Results are written to
outputs/*.jsonand printed as Markdown for quick inspection. - Optionally open
visualization/visualize.htmlin a browser and load the JSON to see charts by plant and model.
-
Plants JSON (per plant):
plant_name: stringplant_quantity_capacity: intallowedModels: list of model names the plant can produce
-
Items JSON (flat list, per item):
order: string (order id)dueDate: stringYYYY-MM-DD(used to compute an urgency boost)modelFamily: stringmodel: string (the model name used by the optimizer)submodel: stringquantity: int
-
Settings JSON (see
inputs/settings-1.jsonfor an example):fill_weight,due_date_boost_weight,quantity_weightw_model_name_group(discourage splitting a model across many plants)w_model_order_id_group(discourage splitting an order across many plants)w_minimize_plants(prefer using fewer plants overall)soft_min_qty_of_items_same_model_name_in_a_plant(soft threshold per model per plant)w_soft_min_qty_of_items_same_model_name_in_a_plant(weight of the soft shortfall penalty)min_allowed_qty_of_items_same_model_name_in_a_plant(hard threshold per model per plant)time_limit_s(required; seconds)random_seed(optional; integer)
Important: soft and hard thresholds are mutually exclusive by validation. If the soft threshold or its weight is set, the hard threshold must be zero (see inputvalidations.py).
- One plant per item at most: an item can be sent to at most one plant.
- Plant capacity: the sum of item quantities sent to a plant cannot exceed that plant’s capacity.
- Compatibility: an item may only go to plants that list its model in
allowedModels. Items with models producible nowhere are flagged “unsupported” and never allocated. - Plant usage linking: a plant counts as “used” if any item is sent there, and if a plant is marked used it must host at least one item (keeps the count honest).
- Model presence linking: if any item of a model is sent to a plant, that model is marked “present” at that plant. If a model is marked “used” globally, it must be present on at least one plant. Equivalent presence links exist for orders.
Optional thresholds per (model, plant):
- Hard minimum quantity: if set (>0), a plant cannot run a model in tiny lots. If the plant can’t reach the minimum with available/compatible items and capacity, the model simply won’t run there.
- Soft minimum quantity: if set (>0) with a positive weight, falling below the threshold incurs a penalty proportional to the shortfall; once the threshold is met, there is no extra reward for exceeding it.
The optimizer maximizes a single score that combines:
- Per‑item additive terms (weighted and normalized)
- fill: rewards placing more items
- due_date_boost: rewards urgent items (loader maps due dates to [0,100])
- quantity: rewards total produced quantity
- Normalization: each term’s weight is scaled based on an upper bound of what that term could achieve under capacity, so different terms share a comparable scale. This uses a fast “fractional knapsack” bound (zero‑quantity items contribute fully, others are bounded by capacity).
- Structural penalties (discourage “spread”)
- Model grouping: penalizes the number of extra plants a model touches beyond its first. Intuition: prefer to keep the same model on as few plants as practical.
- Order grouping: same idea but for orders; prefer not to split the same order across multiple plants when capacity allows.
- Plants used: penalizes using more plants in total, nudging solutions to pack where feasible.
- Soft minimum shortfall (if enabled)
- For every allowed (model, plant) where the model is present but the quantity is below
soft_min_qty, a shortfall is counted as max(0, threshold − allocated). The score subtracts the weighted sum of these shortfalls, so the optimizer prefers meeting thresholds but may accept small shortfalls if it helps other priorities.
Tuning: You control relative importance with the weights in settings. Because of normalization, a weight of, say, 5 on grouping is intended to be comparable to a weight of 5 on an additive term. The time limit governs how long the search spends looking for improvements.
Note on terminology: Internally, the model uses yes/no decisions like “send item i to plant p” and “model f is present at plant p”. You don’t need to know constraint programming details; just think of them as switches that enforce the rules above and allow the score to be computed. For advanced readers, this is built with Google OR‑Tools’ CP‑SAT interface (see references below).
The main result is a JSON with keys including:
items: array of items with{ name, quantity, status, plant, model_name }, where status isallocated,unallocated, orunsupported model_name.plants_to_items: mapping plant → list of allocated item names.plants_to_model_names: mapping plant → list of models present at that plant.model_name_to_plants: mapping model → plants where it runs.- Tallies:
items_placed,total_quantity_placed,plants_used,extra_plants(the total extra plants models use beyond their first),model_name_plants_used,placed_per_model_name. - Unsupported and unallocated lists:
unsupported_items,unsupported_items_by_model_name,unallocated_items. - Markdown tables: per‑plant tables, unallocated, and unsupported sections; also a concatenated
markdown_all_tablesfor quick printing. objective_breakdown: achieved additive sums, structural normalizers, soft‑min meta (threshold, weight, total shortfall), coefficients, and solverstatus.
You can visualize the JSON with visualization/visualize.html by selecting the file in the browser; it draws per‑plant histograms of quantity by model.
Prerequisites
- Python 3.11+ and a virtual environment. Install dependencies from
requirements.txt.
Setup and run (bash shell on Windows is fine, e.g., Git Bash):
# from repo root
python -m venv .venv
./.venv/Scripts/activate
pip install -r requirements.txt
# Run from JSON inputs
./.venv/Scripts/python.exe main.py \
--plants-file inputs/plants-info-2.json \
--items-file inputs/items_to_be_allocated-2.json \
--settings-file inputs/settings-1.json
# Or run from Excel
./.venv/Scripts/python.exe main.py \
--load-from-excel true \
--excel-input excels/example_1_portafoglio_da_inviare_AI.xlsm \
--settings-file inputs/settings-1.jsonTests
- In VS Code, run the task “Run allocate unit tests”, or from a shell:
./.venv/Scripts/python.exe -m unittest -v tests/*.pyOutputs
- Results are written under
outputs/by default. The console prints Markdown tables and the final solver status.
- Increase
fill_weightif you want to place more items regardless of quantity; increasequantity_weightif total quantity is the priority. - Raise
w_model_name_groupto keep the same model concentrated on fewer plants; raisew_model_order_id_groupto keep orders together. - Use the soft min threshold and its weight to discourage tiny lots without forbidding them. Switch to the hard min only when you want to strictly block tiny lots.
- Set
random_seedfor reproducibility during experimentation. - Use reasonable
time_limit_s: more time can improve the plan, but diminishing returns apply.
- “No eligible items remain after filtering”: every item’s model is unsupported by all plants. Fix
allowedModelsor the item models. - “Mutual exclusivity violated” for soft/hard minima: if the soft threshold or its weight is positive, the hard threshold must be zero.
- “time_limit_s must be > 0 seconds”: ensure settings include a positive numeric time limit.
- Quantities and capacities must be integers; the loaders will raise if types are wrong.
If you want to look under the hood, the model is implemented with Google OR‑Tools’ CP‑SAT Python API. You don’t need these to use the tool, but they explain the building blocks behind constraints and the solver:
- OR‑Tools Python reference for CP‑SAT model and solver: https://developers.google.com/optimization/reference/python/sat/python/cp_model
Maintained in this repository: see tests/ for examples that exercise grouping penalties, soft/hard minima, and plants‑used trade‑offs.