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
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 uses prescriptive reasoning to find 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. The model runs multiple demand scenarios (80%, 100%, and 110% of base demand) to show how the optimal plan shifts as demand changes.
The scenario analysis loop demonstrates a powerful pattern for what-if planning. By solving the same model structure under different parameter values, decision-makers can understand the sensitivity of their production strategy to demand fluctuations.
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
- Anyone building production scheduling or capacity planning tools
What you’ll build
- A multi-machine, multi-product production planning model with integer variables
- Machine capacity constraints based on production rates
- Demand satisfaction constraints with configurable demand multipliers
- A profit maximization objective
- A scenario analysis loop comparing results across demand levels
What’s included
production_planning.py— Main script with scenario loop, model definition, and result summariesdata/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:
Running scenario: demand_multiplier = 0.8Status: OPTIMAL, Objective: 14650.0Production plan:name valueqty_Machine_1_Widget_A 80.0qty_Machine_1_Widget_C 48.0qty_Machine_2_Widget_B 64.0qty_Machine_3_Widget_A 2.0qty_Machine_3_Widget_B 2.0Running scenario: demand_multiplier = 1.0Status: OPTIMAL, Objective: 15950.0Production plan:name valueqty_Machine_1_Widget_A 80.0qty_Machine_1_Widget_C 60.0qty_Machine_2_Widget_B 70.0qty_Machine_3_Widget_A 20.0qty_Machine_3_Widget_B 10.0Running scenario: demand_multiplier = 1.1Status: OPTIMAL, Objective: 16800.0Production plan:name valueqty_Machine_1_Widget_A 80.0qty_Machine_1_Widget_B 10.0qty_Machine_1_Widget_C 66.0qty_Machine_2_Widget_B 70.0qty_Machine_3_Widget_A 30.0qty_Machine_3_Widget_B 10.0==================================================Scenario Analysis Summary==================================================0.8: OPTIMAL, obj=14650.01.0: OPTIMAL, obj=15950.01.1: OPTIMAL, obj=16800.0
Template structure
.├── README.md├── pyproject.toml├── production_planning.py└── data/ ├── products.csv ├── machines.csv └── production_rates.csvHow it works
1. Define the ontology and load data
The model defines products with demand and profit, machines with available hours, and production rates linking each machine-product pair.
Product = Concept("Product", identify_by={"id": Integer})Product.name = Property(f"{Product} has {String:name}")Product.demand = Property(f"{Product} has {Integer:demand}")Product.profit = Property(f"{Product} has {Float:profit}")
Machine = Concept("Machine", identify_by={"id": Integer})Machine.name = Property(f"{Machine} has {String:name}")Machine.hours_available = Property(f"{Machine} has {Float:hours_available}")
Rate = Concept("ProductionRate")Rate.machine = Property(f"{Rate} on {Machine}", short_name="machine")Rate.product = Property(f"{Rate} for {Product}", short_name="product")Rate.hours_per_unit = Property(f"{Rate} has {Float:hours_per_unit}")2. Define decision variables and scenarios
Scenarios are modeled as a Scenario concept with a demand_multiplier property — all scenarios are solved in a single call, not a loop.
Scenario = Concept("Scenario", identify_by={"name": String})Scenario.demand_multiplier = Property(f"{Scenario} has {Float:demand_multiplier}")
problem = Problem(model, Float)
# Variable indexed by Scenario — one quantity per production rate per scenarioProduction.x_quantity = Property(f"{Production} in {Scenario} has {Float:quantity}")problem.solve_for( Production.x_quantity(Scenario, x_qty), name=["qty", Scenario.name, Production.rate.machine.name, Production.rate.product.name], lower=0, type="int",)3. Add constraints
Machine capacity and demand satisfaction constraints are defined per scenario.
# Machine capacity: total production hours <= available hours (per machine, per scenario)problem.satisfy(model.where(...).require( sum(x_qty * Production.rate.hours_per_unit) .where(Production.rate.machine == Machine) .per(Machine, Scenario) <= Machine.hours_available))
# Meet scaled demand (per product, per scenario)problem.satisfy(model.where(...).require( sum(x_qty).where(Production.rate.product == Product).per(Product, Scenario) >= Product.demand * Scenario.demand_multiplier))4. Maximize profit
The objective maximizes total profit across all production assignments.
total_profit = sum(Production.x_quantity * Production.rate.product.profit)problem.maximize(total_profit)Customize this template
- Add more products or machines by extending the CSV files.
- Add raw material constraints by introducing material requirements per product and inventory limits.
- Model setup times between product changeovers on the same machine.
- Extend scenario analysis to vary other parameters like machine availability or profit margins.
- Add minimum lot sizes by setting lower bounds on production quantities when a product is produced.
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.