Production Planning
Schedule production across machines to meet demand while maximizing profit.
Schedule production across machines to meet demand while maximizing profit.
Schedule production across machines to meet demand and maximize profit with scenario analysis.
Browse files
Browse files
What this template is for
Manufacturing teams often need to decide how much of each product to produce on each machine, given limited machine time and product demand targets. This template models a small production planning problem where:
- Each product has a demand requirement and a per-unit profit.
- Each machine has a limited number of available production hours.
- Each machine–product route has a specific processing time (hours per unit).
The key challenge is that capacity is shared across products, so meeting demand for one product can crowd out more profitable production elsewhere. This template uses RelationalAI’s prescriptive reasoning (optimization) capabilities to compute a profit-maximizing production plan that meets demand while respecting machine capacity.
Who this is for
- You want an end-to-end example of prescriptive reasoning (optimization) with RelationalAI.
- You’re comfortable with basic Python and the idea of decision variables, constraints, and objectives.
What you’ll build
- A semantic model of machines, products, and machine–product production rates using concepts and properties.
- A MILP with one integer decision variable per feasible machine–product pair.
- Constraints that enforce machine-hour capacity and product demand satisfaction.
- A small scenario analysis loop that reruns the solve under different demand scaling assumptions.
What’s included
- Model + solve script:
production_planning.py - Sample data:
data/products.csv,data/machines.csv,data/production_rates.csv - Outputs: solver status/objective per scenario, a per-scenario production plan table, and a scenario summary
Prerequisites
Access
- A Snowflake account that has the RAI Native App installed.
- A Snowflake user with permissions to access the RAI Native App.
Tools
- Python >= 3.10
Quickstart
Follow these steps to run the template with the included sample data.
-
Download the ZIP file for this template and extract it:
Terminal window curl -O https://private.relational.ai/templates/zips/v0.13/production_planning.zipunzip production_planning.zipcd production_planning -
Create and activate a virtual environment
Terminal window python -m venv .venvsource .venv/bin/activatepython -m pip install -U pip -
Install dependencies
From this folder:
Terminal window python -m pip install . -
Configure Snowflake connection and RAI profile
Terminal window rai init -
Run the template
Terminal window python production_planning.py -
Expected output
The script solves three demand scenarios and prints a production plan table for each. You should see output shaped like:
Running scenario: demand_multiplier = 1.0Status: OPTIMAL, Objective: 14945.0Production plan:name valueqty_Machine_1_Widget_A 4.0qty_Machine_1_Widget_C 95.0...==================================================Scenario Analysis Summary==================================================0.8: OPTIMAL, obj=15020.01.0: OPTIMAL, obj=14945.01.1: OPTIMAL, obj=14770.0
Template structure
.├─ README.md├─ pyproject.toml├─ production_planning.py # main runner / entrypoint└─ data/ # sample input data ├─ products.csv ├─ machines.csv └─ production_rates.csvStart here: production_planning.py
Sample data
Data files are in data/.
products.csv
| Column | Meaning |
|---|---|
id | Unique product identifier |
name | Product name |
demand | Units that must be produced (before scenario scaling) |
profit | Profit per unit |
machines.csv
| Column | Meaning |
|---|---|
id | Unique machine identifier |
name | Machine name |
hours_available | Total hours available (capacity) |
production_rates.csv
| Column | Meaning |
|---|---|
machine_id | Foreign key to machines.csv.id |
product_id | Foreign key to products.csv.id |
hours_per_unit | Hours required to produce one unit on that machine |
Model overview
The optimization model is built around four concepts.
Product
A product with demand and profit parameters.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/products.csv |
name | string | No | Used for variable naming in the output |
demand | int | No | Minimum units required (scaled by demand_multiplier) |
profit | float | No | Profit per unit in the objective |
Machine
A production resource with limited available hours.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/machines.csv |
name | string | No | Used for variable naming in the output |
hours_available | float | No | Capacity constraint per machine |
ProductionRate
A feasible machine–product route that specifies how long it takes to produce one unit.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
machine | Machine | Part of compound key | Joined via data/production_rates.csv.machine_id |
product | Product | Part of compound key | Joined via data/production_rates.csv.product_id |
hours_per_unit | float | No | Coefficient in the machine-capacity constraints |
Production (decision concept)
One decision row per ProductionRate route; the solver chooses the production quantity.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
rate | ProductionRate | Yes | One decision variable per machine–product route |
quantity | float | No | Integer decision variable (type="int"), lower bounded by 0 |
How it works
This section walks through the highlights in production_planning.py.
Import libraries and configure inputs
First, the script imports the Semantics APIs and configures DATA_DIR and the pandas string inference behavior:
from pathlib import Path
import pandasfrom pandas import read_csv
from relationalai.semantics import Model, data, define, require, sum, wherefrom relationalai.semantics.reasoners.optimization import Solver, SolverModel
# --------------------------------------------------# Configure inputs# --------------------------------------------------
DATA_DIR = Path(__file__).parent / "data"
# Disable pandas inference of string types. This ensures that string columns# in the CSVs are loaded as object dtype. This is only required when using# relationalai versions prior to v1.0.pandas.options.future.infer_string = False
# --------------------------------------------------# Define semantic model & load data# --------------------------------------------------
# Create a Semantics model container.model = Model("production_planning", config=globals().get("config", None), use_lqp=False)Define concepts and load CSV data
Next, the script defines Product and Machine, loads products.csv and machines.csv via data(...).into(...), and then joins the foreign keys in production_rates.csv to create ProductionRate rows using where(...).define(...):
# Product concept: products with demand and profit per unit.Product = model.Concept("Product")Product.id = model.Property("{Product} has {id:int}")Product.name = model.Property("{Product} has {name:string}")Product.demand = model.Property("{Product} has {demand:int}")Product.profit = model.Property("{Product} has {profit:float}")
# Load product data from CSV.data(read_csv(DATA_DIR / "products.csv")).into(Product, keys=["id"])
# Machine concept: machines with a limited number of available production hours.Machine = model.Concept("Machine")Machine.id = model.Property("{Machine} has {id:int}")Machine.name = model.Property("{Machine} has {name:string}")Machine.hours_available = model.Property("{Machine} has {hours_available:float}")
# Load machine data from CSV.data(read_csv(DATA_DIR / "machines.csv")).into(Machine, keys=["id"])
# ProductionRate concept: hours required per unit for each machine-product pair.Rate = model.Concept("ProductionRate")Rate.machine = model.Property("{ProductionRate} on {machine:Machine}")Rate.product = model.Property("{ProductionRate} for {product:Product}")Rate.hours_per_unit = model.Property("{ProductionRate} has {hours_per_unit:float}")
# Load production rate data from CSV.rates_data = data(read_csv(DATA_DIR / "production_rates.csv"))
# Define ProductionRate entities by joining the rate CSV with Machine and Product.where( Machine.id == rates_data.machine_id, Product.id == rates_data.product_id).define( Rate.new(machine=Machine, product=Product, hours_per_unit=rates_data.hours_per_unit))Define decision variables, constraints, and objective
Then the script creates a Production decision concept (one row per ProductionRate) and defines a build_formulation helper. That helper registers the integer decision variable with solve_for(...), adds constraints with require(...), and sets a profit-maximizing objective:
# Production decision concept: production quantity for each machine-product pair.Production = model.Concept("Production")Production.rate = model.Property("{Production} uses {rate:ProductionRate}")Production.x_quantity = model.Property("{Production} has {quantity:float}")define(Production.new(rate=Rate))
Prod = Production.ref()
# Scenario parameter (overridden within the scenario loop).demand_multiplier = 1.0
def build_formulation(s): """Register variables, constraints, and objective on the solver model.""" # Variable: production quantity (integer) s.solve_for( Production.x_quantity, name=[ "qty", Production.rate.machine.name, Production.rate.product.name, ], lower=0, type="int", )
# Constraint: machine capacity machine_hours = ( sum(Prod.quantity * Prod.rate.hours_per_unit) .where(Prod.rate.machine == Machine) .per(Machine) ) capacity_limit = require(machine_hours <= Machine.hours_available) s.satisfy(capacity_limit)
# Constraint: meet demand (scaled by demand_multiplier) product_qty = sum(Prod.quantity).where(Prod.rate.product == Product).per(Product) meet_demand = require(product_qty >= Product.demand * demand_multiplier) s.satisfy(meet_demand)
# Objective: maximize total profit total_profit = sum(Production.x_quantity * Production.rate.product.profit) s.maximize(total_profit)Solve scenarios and print results
Finally, the script runs a small scenario analysis by setting demand_multiplier, solving a fresh SolverModel each time, and printing a filtered production plan table (only rows where the solver value is greater than 0.001):
SCENARIO_PARAM = "demand_multiplier"SCENARIO_VALUES = [0.8, 1.0, 1.1]
scenario_results = []
for scenario_value in SCENARIO_VALUES: print(f"\nRunning scenario: {SCENARIO_PARAM} = {scenario_value}")
# Set scenario parameter value demand_multiplier = scenario_value
# Create fresh SolverModel for each scenario. s = SolverModel(model, "cont") build_formulation(s)
solver = Solver("highs") s.solve(solver, time_limit_sec=60)
scenario_results.append( { "scenario": scenario_value, "status": str(s.termination_status), "objective": s.objective_value, } ) print(f" Status: {s.termination_status}, Objective: {s.objective_value}")
# Print production plan from solver results var_df = s.variable_values().to_df() qty_df = var_df[ var_df["name"].str.startswith("qty") & (var_df["float"] > 0.001) ].rename(columns={"float": "value"}) print(f"\n Production plan:") print(qty_df.to_string(index=False))
# Summaryprint("\n" + "=" * 50)print("Scenario Analysis Summary")print("=" * 50)for result in scenario_results: print(f" {result['scenario']}: {result['status']}, obj={result['objective']}")Customize this template
Change the scenario parameters
This template includes a simple demand sensitivity analysis controlled by demand_multiplier.
| Parameter | Type | Values | Description |
|---|---|---|---|
demand_multiplier | numeric | 0.8, 1.0, 1.1 | Multiplier applied to all product demands |
How to customize the scenarios:
- In
production_planning.py, editSCENARIO_VALUESto the multipliers you want to test.
How to interpret results:
- If increasing
demand_multiplierdecreases the objective, demand is forcing production into less-profitable routes. - If changing
demand_multiplierdoes not change the objective, the demand constraints are likely non-binding at those values.
Use your own data
- Replace the CSV files under
data/. - Keep IDs consistent across files (
machine_id/product_idmust exist inmachines.csv/products.csv).
Extend the model
- Add setup costs and binary on/off decisions per route.
- Add maximum production limits per product (demand as an upper bound rather than a lower bound).
- Add multi-period planning (introduce a
Periodconcept and inventory/transition constraints).
Troubleshooting
Why does authentication/configuration fail?
- Run
rai initto create/updateraiconfig.toml. - If you have multiple profiles, set
RAI_PROFILEor switch profiles in your config.
Why does the script fail to connect to the RAI Native App?
- Verify the Snowflake account/role/warehouse and
rai_app_nameare correct inraiconfig.toml. - Ensure the RAI Native App is installed and you have access.
ModuleNotFoundError when running the script
- Confirm your virtual environment is activated.
- Install the template dependencies from this folder:
python -m pip install .
CSV loading fails (missing file or column)
- Confirm the CSVs exist under
data/and the filenames match. - Ensure the headers match the expected schema:
products.csv:id,name,demand,profitmachines.csv:id,name,hours_availableproduction_rates.csv:machine_id,product_id,hours_per_unit
Why do I get Status: INFEASIBLE?
- Check that total machine hours are sufficient to meet demand: for each product, at least one route must exist in
production_rates.csv. - If you increased
demand_multiplier, try lowering it or increasinghours_available.
Why is the production plan empty?
- The output filters on
float > 0.001and only prints variables whose names start withqty. - If you suspect near-zero values, print
s.variable_values().to_df()without filtering.
Solver fails or returns an unexpected termination status
- Try re-running; transient connectivity issues can affect the solve step.
- If the solve is slow, reduce problem size (fewer machines/products/routes) or increase
time_limit_secinproduction_planning.py.
What this template is for
Manufacturing teams often need to decide how much of each product to produce on each machine, given limited machine time and product demand targets. This template models a small production planning problem where:
- Each product has a demand requirement and a per-unit profit.
- Each machine has a limited number of available production hours.
- Each machine–product route has a specific processing time (hours per unit).
The key challenge is that capacity is shared across products, so meeting demand for one product can crowd out more profitable production elsewhere. This template uses RelationalAI’s prescriptive reasoning (optimization) capabilities to compute a profit-maximizing production plan that meets demand while respecting machine capacity.
Who this is for
- You want an end-to-end example of prescriptive reasoning (optimization) with RelationalAI.
- You’re comfortable with basic Python and the idea of decision variables, constraints, and objectives.
What you’ll build
- A semantic model of machines, products, and machine–product production rates using concepts and properties.
- A MILP with one integer decision variable per feasible machine–product pair.
- Constraints that enforce machine-hour capacity and product demand satisfaction.
- A small scenario analysis loop that reruns the solve under different demand scaling assumptions.
What’s included
- Model + solve script:
production_planning.py - Sample data:
data/products.csv,data/machines.csv,data/production_rates.csv - Outputs: solver status/objective per scenario, a per-scenario production plan table, and a scenario summary
Prerequisites
Access
- A Snowflake account that has the RAI Native App installed.
- A Snowflake user with permissions to access the RAI Native App.
Tools
- Python >= 3.10
Quickstart
Follow these steps to run the template with the included sample data.
-
Download the ZIP file for this template and extract it:
Terminal window curl -O https://private.relational.ai/templates/zips/v0.14/production_planning.zipunzip production_planning.zipcd production_planning -
Create and activate a virtual environment
Terminal window python -m venv .venvsource .venv/bin/activatepython -m pip install -U pip -
Install dependencies
From this folder:
Terminal window python -m pip install . -
Configure Snowflake connection and RAI profile
Terminal window rai init -
Run the template
Terminal window python production_planning.py -
Expected output
The script solves three demand scenarios and prints a production plan table for each. You should see output shaped like:
Running scenario: demand_multiplier = 1.0Status: OPTIMAL, Objective: 14945.0Production plan:name valueqty_Machine_1_Widget_A 4.0qty_Machine_1_Widget_C 95.0...==================================================Scenario Analysis Summary==================================================0.8: OPTIMAL, obj=15020.01.0: OPTIMAL, obj=14945.01.1: OPTIMAL, obj=14770.0
Template structure
.├─ README.md├─ pyproject.toml├─ production_planning.py # main runner / entrypoint└─ data/ # sample input data ├─ products.csv ├─ machines.csv └─ production_rates.csvStart here: production_planning.py
Sample data
Data files are in data/.
products.csv
| Column | Meaning |
|---|---|
id | Unique product identifier |
name | Product name |
demand | Units that must be produced (before scenario scaling) |
profit | Profit per unit |
machines.csv
| Column | Meaning |
|---|---|
id | Unique machine identifier |
name | Machine name |
hours_available | Total hours available (capacity) |
production_rates.csv
| Column | Meaning |
|---|---|
machine_id | Foreign key to machines.csv.id |
product_id | Foreign key to products.csv.id |
hours_per_unit | Hours required to produce one unit on that machine |
Model overview
The optimization model is built around four concepts.
Product
A product with demand and profit parameters.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/products.csv |
name | string | No | Used for variable naming in the output |
demand | int | No | Minimum units required (scaled by demand_multiplier) |
profit | float | No | Profit per unit in the objective |
Machine
A production resource with limited available hours.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/machines.csv |
name | string | No | Used for variable naming in the output |
hours_available | float | No | Capacity constraint per machine |
ProductionRate
A feasible machine–product route that specifies how long it takes to produce one unit.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
machine | Machine | Part of compound key | Joined via data/production_rates.csv.machine_id |
product | Product | Part of compound key | Joined via data/production_rates.csv.product_id |
hours_per_unit | float | No | Coefficient in the machine-capacity constraints |
Production (decision concept)
One decision row per ProductionRate route; the solver chooses the production quantity.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
rate | ProductionRate | Yes | One decision variable per machine–product route |
quantity | float | No | Integer decision variable (type="int"), lower bounded by 0 |
How it works
This section walks through the highlights in production_planning.py.
Import libraries and configure inputs
First, the script imports the Semantics APIs and configures DATA_DIR and the pandas string inference behavior:
from pathlib import Path
import pandasfrom pandas import read_csv
from relationalai.semantics import Model, Relationship, data, define, require, sum, wherefrom relationalai.semantics.reasoners.optimization import Solver, SolverModel
# --------------------------------------------------# Configure inputs# --------------------------------------------------
DATA_DIR = Path(__file__).parent / "data"
# Disable pandas inference of string types. This ensures that string columns# in the CSVs are loaded as object dtype. This is only required when using# relationalai versions prior to v1.0.pandas.options.future.infer_string = False
# --------------------------------------------------# Define semantic model & load data# --------------------------------------------------
# Create a Semantics model container.model = Model("production_planning", config=globals().get("config", None))Define concepts and load CSV data
Next, the script defines Product and Machine, loads products.csv and machines.csv via data(...).into(...), and then joins the foreign keys in production_rates.csv to create ProductionRate rows using where(...).define(...):
# Product concept: products with demand and profit per unit.Product = model.Concept("Product")Product.id = model.Property("{Product} has {id:int}")Product.name = model.Property("{Product} has {name:string}")Product.demand = model.Property("{Product} has {demand:int}")Product.profit = model.Property("{Product} has {profit:float}")
# Load product data from CSV.data(read_csv(DATA_DIR / "products.csv")).into(Product, keys=["id"])
# Machine concept: machines with a limited number of available production hours.Machine = model.Concept("Machine")Machine.id = model.Property("{Machine} has {id:int}")Machine.name = model.Property("{Machine} has {name:string}")Machine.hours_available = model.Property("{Machine} has {hours_available:float}")
# Load machine data from CSV.data(read_csv(DATA_DIR / "machines.csv")).into(Machine, keys=["id"])
# ProductionRate concept: hours required per unit for each machine-product pair.Rate = model.Concept("ProductionRate")Rate.machine = model.Relationship("{ProductionRate} on {machine:Machine}")Rate.product = model.Relationship("{ProductionRate} for {product:Product}")Rate.hours_per_unit = model.Property("{ProductionRate} has {hours_per_unit:float}")
# Load production rate data from CSV.rates_data = data(read_csv(DATA_DIR / "production_rates.csv"))
# Define ProductionRate entities by joining the rate CSV with Machine and Product.where( Machine.id == rates_data.machine_id, Product.id == rates_data.product_id).define( Rate.new(machine=Machine, product=Product, hours_per_unit=rates_data.hours_per_unit))Define decision variables, constraints, and objective
Then the script creates a Production decision concept (one row per ProductionRate) and defines a build_formulation helper. That helper registers the integer decision variable with solve_for(...), adds constraints with require(...), and sets a profit-maximizing objective:
# Production decision concept: production quantity for each machine-product pair.Production = model.Concept("Production")Production.rate = model.Relationship("{Production} uses {rate:ProductionRate}")Production.x_quantity = model.Property("{Production} has {quantity:float}")define(Production.new(rate=Rate))
ProductionRef = Production.ref()
# Scenario parameter (overridden within the scenario loop).demand_multiplier = 1.0
def build_formulation(s): """Register variables, constraints, and objective on the solver model.""" # Variable: production quantity (integer) s.solve_for( Production.x_quantity, name=[ "qty", Production.rate.machine.name, Production.rate.product.name, ], lower=0, type="int", )
# Constraint: machine capacity machine_hours = ( sum(ProductionRef.x_quantity * ProductionRef.rate.hours_per_unit) .where(ProductionRef.rate.machine == Machine) .per(Machine) ) capacity_limit = require(machine_hours <= Machine.hours_available) s.satisfy(capacity_limit)
# Constraint: meet demand (scaled by demand_multiplier) product_qty = sum(ProductionRef.x_quantity).where(ProductionRef.rate.product == Product).per(Product) meet_demand = require(product_qty >= Product.demand * demand_multiplier) s.satisfy(meet_demand)
# Objective: maximize total profit total_profit = sum(Production.x_quantity * Production.rate.product.profit) s.maximize(total_profit)Solve scenarios and print results
Finally, the script runs a small scenario analysis by setting demand_multiplier, solving a fresh SolverModel each time, and printing a filtered production plan table (only rows where the solver value is greater than 0.001):
SCENARIO_PARAM = "demand_multiplier"SCENARIO_VALUES = [0.8, 1.0, 1.1]
scenario_results = []
for scenario_value in SCENARIO_VALUES: print(f"\nRunning scenario: {SCENARIO_PARAM} = {scenario_value}")
# Set scenario parameter value demand_multiplier = scenario_value
# Create fresh SolverModel for each scenario. s = SolverModel(model, "cont") build_formulation(s)
solver = Solver("highs") s.solve(solver, time_limit_sec=60)
scenario_results.append( { "scenario": scenario_value, "status": str(s.termination_status), "objective": s.objective_value, } ) print(f" Status: {s.termination_status}, Objective: {s.objective_value}")
# Print production plan from solver results var_df = s.variable_values().to_df() qty_df = var_df[ var_df["name"].str.startswith("qty") & (var_df["value"] > 0.001) ] print(f"\n Production plan:") print(qty_df.to_string(index=False))
# Summaryprint("\n" + "=" * 50)print("Scenario Analysis Summary")print("=" * 50)for result in scenario_results: print(f" {result['scenario']}: {result['status']}, obj={result['objective']}")Customize this template
Change the scenario parameters
This template includes a simple demand sensitivity analysis controlled by demand_multiplier.
| Parameter | Type | Values | Description |
|---|---|---|---|
demand_multiplier | numeric | 0.8, 1.0, 1.1 | Multiplier applied to all product demands |
How to customize the scenarios:
- In
production_planning.py, editSCENARIO_VALUESto the multipliers you want to test.
How to interpret results:
- If increasing
demand_multiplierdecreases the objective, demand is forcing production into less-profitable routes. - If changing
demand_multiplierdoes not change the objective, the demand constraints are likely non-binding at those values.
Use your own data
- Replace the CSV files under
data/. - Keep IDs consistent across files (
machine_id/product_idmust exist inmachines.csv/products.csv).
Extend the model
- Add setup costs and binary on/off decisions per route.
- Add maximum production limits per product (demand as an upper bound rather than a lower bound).
- Add multi-period planning (introduce a
Periodconcept and inventory/transition constraints).
Troubleshooting
Why does authentication/configuration fail?
- Run
rai initto create/updateraiconfig.toml. - If you have multiple profiles, set
RAI_PROFILEor switch profiles in your config.
Why does the script fail to connect to the RAI Native App?
- Verify the Snowflake account/role/warehouse and
rai_app_nameare correct inraiconfig.toml. - Ensure the RAI Native App is installed and you have access.
ModuleNotFoundError when running the script
- Confirm your virtual environment is activated.
- Install the template dependencies from this folder:
python -m pip install .
CSV loading fails (missing file or column)
- Confirm the CSVs exist under
data/and the filenames match. - Ensure the headers match the expected schema:
products.csv:id,name,demand,profitmachines.csv:id,name,hours_availableproduction_rates.csv:machine_id,product_id,hours_per_unit
Why do I get Status: INFEASIBLE?
- Check that total machine hours are sufficient to meet demand: for each product, at least one route must exist in
production_rates.csv. - If you increased
demand_multiplier, try lowering it or increasinghours_available.
Why is the production plan empty?
- The output filters on
value > 0.001and only prints variables whose names start withqty. - If you suspect near-zero values, print
s.variable_values().to_df()without filtering.
Solver fails or returns an unexpected termination status
- Try re-running; transient connectivity issues can affect the solve step.
- If the solve is slow, reduce problem size (fewer machines/products/routes) or increase
time_limit_secinproduction_planning.py.
What this template is for
Manufacturers must decide how many units of each product to produce on each machine to maximize profit while meeting customer demand and respecting machine capacity. When market conditions are uncertain, planners need to evaluate how production plans change under different demand scenarios.
This template finds the profit-maximizing production plan across a set of machines and products. Each machine has limited available hours, and each machine-product combination has a specific production rate. Three demand scenarios (80%, 100%, and 110% of base demand) are modeled together so decision-makers can see how the optimal plan shifts as demand changes and understand how sensitive their production strategy is to demand fluctuations.
The reasoning approach uses prescriptive optimization: a single mixed-integer program that solves all demand scenarios simultaneously, with scenario-scoped capacity and demand constraints.
Who this is for
- Production planners optimizing machine utilization and product mix
- Operations managers evaluating plans under demand uncertainty
- Developers learning integer programming and scenario analysis with RelationalAI
- Assumed knowledge: comfortable reading Python; the optimization and manufacturing terms are explained as they come up
What you’ll build
- A profit-maximizing production plan across machines and products, produced by prescriptive reasoning (mixed-integer program)
- Machine-capacity limits enforced as per-machine, per-scenario constraints
- Demand-satisfaction requirements scaled by a per-scenario demand multiplier
- A three-scenario comparison (80% / 100% / 110% of base demand) solved in a single call over a first-class
Scenarioconcept
What’s included
production_planning.py— Main script with the scenario model, constraints, objective, and result summary- Runbook:
runbook.md— a paste-testable walkthrough that reproduces the template step by step with the RAI skills; as important a reference as the script itself. data/products.csv— Products with base demand and per-unit profit marginsdata/machines.csv— Machines with available hours per planning perioddata/production_rates.csv— Hours required per unit for each machine-product combinationpyproject.toml— Python project configuration with dependencies
Prerequisites
Access
- A Snowflake account that has the RAI Native App installed.
- A Snowflake user with permissions to access the RAI Native App.
Tools
- Python >= 3.10
Quickstart
-
Download ZIP:
Terminal window curl -O https://docs.relational.ai/templates/zips/v1/production_planning.zipunzip production_planning.zipcd production_planning -
Create venv:
Terminal window python -m venv .venvsource .venv/bin/activatepython -m pip install --upgrade pip -
Install:
Terminal window python -m pip install . -
Configure:
Terminal window rai init -
Run:
Terminal window python production_planning.py -
Expected output (all scenarios solved together in a single call):
Solve result:• status: OPTIMAL• objective: 44735.0• solver: HiGHS• relative gap: 0.0Production plan per scenario:scenario machine product quantity0 baseline Machine_1 Widget_A 4.01 baseline Machine_1 Widget_C 95.02 baseline Machine_2 Widget_B 70.03 baseline Machine_3 Widget_A 96.04 baseline Machine_3 Widget_B 11.05 high_demand Machine_1 Widget_A 24.06 high_demand Machine_1 Widget_B 1.07 high_demand Machine_1 Widget_C 68.08 high_demand Machine_2 Widget_B 70.09 high_demand Machine_3 Widget_A 87.010 high_demand Machine_3 Widget_B 17.011 low_demand Machine_1 Widget_C 100.012 low_demand Machine_2 Widget_B 70.013 low_demand Machine_3 Widget_A 111.014 low_demand Machine_3 Widget_B 1.0The single objective (
44735.0) is the combined profit across all three scenarios. Per-scenario profit is14,945 (1.0x), and $14,770 (1.1x) — profit falls as the demand floor rises, because a looser floor leaves more capacity for the highest-margin mix.
Template structure
production_planning/├── README.md # this file├── pyproject.toml # dependencies├── production_planning.py # main script (single solve over all scenarios)├── runbook.md # analyst-facing walkthrough└── data/ ├── products.csv # products with base demand and per-unit profit ├── machines.csv # machines with available hours per period └── production_rates.csv # hours per unit for each machine-product pairStart here: run python production_planning.py for the full run end to end, or follow runbook.md to reproduce it step by step with the RAI skills.
Sample data
The bundled data is small and illustrative — three CSVs describing a plant with a handful of products and machines, sized so the model solves instantly while showing the full workflow.
products.csv— one row per product, withdemand(base units required) andprofit(per-unit margin).machines.csv— one row per machine, withhours_availableper planning period.production_rates.csv— one row per machine-product pair, withhours_per_unit(machine time to make one unit). A missing pair means that machine cannot make that product.
The three demand scenarios (low_demand 0.8, baseline 1.0, high_demand 1.1) are defined inline in the script, not loaded from a CSV.
Model overview
- Key entities:
Product— a product to manufacture, with its base demand and profit margin;Machine— a machine with a fixed number of available hours per planning period;ProductionRate— how long a given machine takes to make one unit of a given product;Scenario— a demand scenario applied uniformly to all products; and the decision conceptProduction— one per production rate, carrying the solved quantity per scenario. - Primary identifiers: integer
idonProductandMachine; stringnameonScenario.ProductionRateandProductionare identified structurally by the entities they link. - Important invariants:
demand,profit,hours_available, andhours_per_unitare non-negative; the decision variablex_quantityis a non-negative integer; total machine hours used cannot exceedhours_available; total units produced must meet demand scaled by the scenario multiplier.
For the full concept and property definitions, see production_planning.py; runbook.md builds them step by step with the RAI skills.
How it works
The pipeline loads the three CSVs into the ontology, layers a Scenario concept over them, and hands a single mixed-integer program to the solver that decides production quantities for every scenario at once.
CSV inputs → load Product / Machine / ProductionRate → add Scenario multipliers → build the integer production variable (per rate, per scenario) → capacity + demand constraints → maximize profit → solve → per-scenario plan- Load the data. Products carry base demand and per-unit profit, machines carry available hours, and each
ProductionRatelinks a machine-product pair to the hours it takes to make one unit. A missing pair means that machine cannot make that product. - Add scenarios and the decision variable. The three demand scenarios become a first-class
Scenarioconcept, each with ademand_multiplier. The solver decides one non-negative integer quantity per production rate per scenario, so all scenarios are solved together in a single call rather than in a loop. - Constrain the plan. For each machine and scenario, total hours used cannot exceed the machine’s available hours. For each product and scenario, total units produced must meet that product’s demand scaled by the scenario’s multiplier.
- Maximize profit. The objective sums per-unit profit across every production assignment, and the solver returns the profit-maximizing plan for all scenarios at once.
See production_planning.py for the implementation and runbook.md for the skill-driven reproduction.
Customize this template
Focus on the first changes most users will make.
Use your own data
- Replace the CSVs in
data/with your own, keeping the column names listed in Sample data above. - Add a
production_rates.csvrow for every machine-product pair a machine can actually make; omit pairs it cannot make. - For Snowflake-backed runs, swap the
read_csv(...)calls formodel.data(snowflake_table).
Tune parameters
- Demand scenarios — edit the
scenario_datalist at the top of the script to change the multipliers or add scenarios. - Solver time limit —
time_limit_sec(default60) on theproblem.solve(...)call. - Extend scenario analysis to vary other parameters, such as machine availability or profit margins.
Extend the model
- Add raw material constraints by introducing material requirements per product and inventory limits.
- Model setup times between product changeovers on the same machine.
- Add minimum lot sizes by setting lower bounds on production quantities when a product is produced.
Scale up / productionize
- Replace the CSV bundle with ingestion from your ERP or MES tables.
- Integer programs grow harder with more products, machines, and scenarios; give the solver more time via
time_limit_sec, or relax integrality during exploratory runs (see Troubleshooting).
Troubleshooting
Solver returns INFEASIBLE for high demand multipliers
Machine capacity limits how much can be produced. If the demand multiplier is too high, the machines may not have enough hours to meet all demand. Try increasing hours_available in machines.csv or reducing the demand multiplier.
Integer solutions take longer to solve
Integer programming is harder than continuous optimization. For large instances, consider relaxing integer constraints during exploratory analysis by changing type="int" to type="cont", then switch back for final planning.
Connection or authentication errors
Run rai init to configure your Snowflake connection. Verify that the RAI Native App is installed and your user has the required permissions.
ModuleNotFoundError for relationalai
Ensure you activated the virtual environment and ran python -m pip install . to install all dependencies listed in pyproject.toml.
Learn more
Core concepts
- PyRel v1 query language —
model.where(...),.per(...), aggregations, andmodel.select(...).
Reasoner reference
- Prescriptive reasoner — the
ProblemAPI, decision variables, constraints, and objectives.
Deeper dives
- Scenario modeling with a
Scenarioconcept — how a single solve covers multiple parameter settings via a scenario-indexed decision variable.
Support
- File issues at the RelationalAI templates repository.