Factory Production
Choose production quantities per machine-product pair to maximize profit while meeting minimum production requirements.
Choose production quantities per machine-product pair to maximize profit while meeting minimum production requirements.
Maximize production profit under per-factory resource limits, then read the sensitivity marginals (capacity shadow prices and product reduced costs) from one solve.
Browse files
Browse files
Browse files
What this template is for
Manufacturing facilities must decide what to produce given limited resources. This template models a factory with multiple machines producing different products, where:
- Each machine has limited hours available and a machine-specific hourly operating cost.
- Each product has a selling price and a minimum production requirement.
- Different machines take different amounts of time to produce each product.
The goal is to find the optimal product mix—how much of each product to make on each machine—to maximize profit (revenue minus machine operating costs) while respecting machine capacity and meeting minimum production targets.
Who this is for
- You want a small, end-to-end example of prescriptive reasoning (optimization) using RelationalAI Semantics.
- You’re comfortable with basic Python and the idea of constraints + objectives.
What you’ll build
- A semantic model of machines, products, and machine-product production times.
- A linear program (LP) with one continuous decision variable per feasible machine-product pair.
- Capacity and minimum-production constraints.
- A profit-maximizing objective.
What’s included
factory_production.py— defines the semantic model, optimization problem, and prints a solutiondata/— sample CSV inputs (machines.csv,products.csv,production_times.csv)
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 using 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/factory_production.zipunzip factory_production.zipcd factory_production -
Create and activate a virtual environment:
Terminal window python -m venv .venvsource .venv/bin/activatepython -m pip install -U pip -
Install dependencies:
Terminal window python -m pip install . -
Initialize your RAI profile:
Terminal window rai init -
Run the template script:
Terminal window python factory_production.py -
Expected output:
Status: OPTIMAL Total profit: $20977.78
Production plan: machine product quantity Machine_A Widget 80.000000 Machine_B Device 38.888889 Machine_C Gadget 15.000000 Machine_C Widget 90.000000Template structure
.├─ README.md├─ pyproject.toml├─ factory_production.py # main runner / entrypoint└─ data/ # sample input data ├─ machines.csv ├─ products.csv └─ production_times.csvStart here: factory_production.py
Sample data
Data files are located in the data/ subdirectory.
machines.csv
| Column | Description |
|---|---|
id | Unique machine identifier |
name | Machine name |
hours_available | Hours available per period |
hourly_cost | Operating cost per hour ($) |
products.csv
| Column | Description |
|---|---|
id | Unique product identifier |
name | Product name |
price | Selling price per unit ($) |
min_production | Minimum units that must be produced |
production_times.csv
| Column | Description |
|---|---|
machine_id | Reference to machine |
product_id | Reference to product |
hours_per_unit | Hours required to produce one unit |
Model overview
The optimization model is built around four concepts.
Machine
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from machines.csv |
name | string | No | Used for output labels |
hours_available | float | No | Capacity for each machine |
hourly_cost | float | No | Cost term in the objective |
Product
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from products.csv |
name | string | No | Used for output labels |
price | float | No | Revenue per unit |
min_production | int | No | Minimum units required |
ProductionTime
| Property | Type | Identifying? | Notes |
|---|---|---|---|
machine | Machine | Part of compound key | Machine for the route |
product | Product | Part of compound key | Product for the route |
hours_per_unit | float | No | Time to make one unit |
Production (decision concept)
| Property | Type | Identifying? | Notes |
|---|---|---|---|
prod_time | ProductionTime | Yes | One variable per machine-product route |
quantity | float | No | Continuous decision variable, lower bounded by 0 |
How it works
This section walks through the highlights in factory_production.py.
Import libraries and configure inputs
This template uses Concept objects from relationalai.semantics to model machines, products, and production times, and uses Solver and SolverModel from relationalai.semantics.reasoners.optimization to define and solve the linear program:
from pathlib import Path
import pandasfrom pandas import read_csv
from relationalai.semantics import Model, data, define, require, select, sum, wherefrom relationalai.semantics.reasoners.optimization import Solver, SolverModel
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("factory", use_lqp=False)Define concepts and load CSV data
Next, it declares the Machine, Product, and ProductionTime concepts and loads the corresponding CSV tables. data(...).into(...) creates entities from machines.csv and products.csv, and where(...).define(...) joins production_times.csv onto those concepts:
# Machine concept: represents a production machine with available hours and hourly costMachine = 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}")Machine.hourly_cost = model.Property("{Machine} has {hourly_cost:float}")
# Load machine data from CSV and create Machine entities.data(read_csv(DATA_DIR / "machines.csv")).into(Machine, keys=["id"])
# Product concept: represents a product with price and minimum production requirementsProduct = model.Concept("Product")Product.id = model.Property("{Product} has {id:int}")Product.name = model.Property("{Product} has {name:string}")Product.price = model.Property("{Product} has {price:float}")Product.min_production = model.Property("{Product} has {min_production:int}")
# Load product data from CSV and create Product entities.data(read_csv(DATA_DIR / "products.csv")).into(Product, keys=["id"])
# ProdTime concept: represents the time required to produce one unit of a product on a machineProdTime = model.Concept("ProductionTime")ProdTime.machine = model.Property("{ProductionTime} on {machine:Machine}")ProdTime.product = model.Property("{ProductionTime} of {product:Product}")ProdTime.hours_per_unit = model.Property("{ProductionTime} takes {hours_per_unit:float}")
# Load production time data from CSV.times_data = data(read_csv(DATA_DIR / "production_times.csv"))
# Define ProductionTime entities by joining machine/product IDs from the CSV with# the Machine and Product concepts.where( Machine.id == times_data.machine_id, Product.id == times_data.product_id,).define( ProdTime.new(machine=Machine, product=Product, hours_per_unit=times_data.hours_per_unit))Define decision variables, constraints, and objective
Then it creates one continuous, non-negative decision variable per machine–product route, adds machine-hour and minimum-production constraints with require(...), and maximizes profit:
# Decision concept: production quantities for each machine/productProduction = model.Concept("Production")Production.prod_time = model.Property("{Production} uses {prod_time:ProductionTime}")Production.x_quantity = model.Property("{Production} has {quantity:float}")
# Define one Production entity per machine-product ProductionTime record.define(Production.new(prod_time=ProdTime))
Prod = Production.ref()
s = SolverModel(model, "cont")
# Variable: production quantitys.solve_for( Production.x_quantity, name=["qty", Production.prod_time.machine.name, Production.prod_time.product.name], lower=0,)
# Constraint: total production hours per machine <= hours_availabletotal_hours = sum( Prod.quantity * Prod.prod_time.hours_per_unit).where( Prod.prod_time.machine == Machine).per(Machine)machine_limit = require(total_hours <= Machine.hours_available)s.satisfy(machine_limit)
# Constraint: total production per product >= min_productiontotal_produced = sum(Prod.quantity).where(Prod.prod_time.product == Product).per(Product)meet_minimum = require(total_produced >= Product.min_production)s.satisfy(meet_minimum)
# Objective: maximize profit (revenue - machine costs)revenue = sum(Production.x_quantity * Production.prod_time.product.price)machine_cost = sum( Production.x_quantity * Production.prod_time.hours_per_unit * Production.prod_time.machine.hourly_cost)profit = revenue - machine_costs.maximize(profit)Solve and print results
Finally, it solves using the HiGHS backend and prints only rows where Production.x_quantity > 0:
solver = Solver("highs")s.solve(solver, time_limit_sec=60)
print(f"Status: {s.termination_status}")print(f"Total profit: ${s.objective_value:.2f}")
plan = select( Production.prod_time.machine.name.alias("machine"), Production.prod_time.product.name.alias("product"), Production.x_quantity).where(Production.x_quantity > 0).to_df()
print("\nProduction plan:")print(plan.to_string(index=False))Customize this template
Use your own data
- Replace the CSV files under
data/. - Keep IDs consistent across files (
machine_id/product_idmust exist in the respective tables).
Extend the model
- Add maximum production limits, inventory constraints, or demand caps.
- Add setup/changeover costs and binary on/off decisions (MILP).
- Switch to integer quantities (or start from ../production_planning/README.md).
Troubleshooting
Connection/auth issues
- Re-run
rai initand confirm the selected profile is correct. - Ensure you have access to the RAI Native App in Snowflake.
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:
machines.csv:id,name,hours_available,hourly_costproducts.csv:id,name,price,min_productionproduction_times.csv:machine_id,product_id,hours_per_unit
Status is INFEASIBLE
- Check that product minimums can be met with available machine hours.
- Ensure each required product has at least one route in
production_times.csv.
No rows printed in the production plan
- The output filters on
Production.x_quantity > 0. - If quantities are extremely small, print the full variable set or relax the filter.
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 the time limit in
factory_production.py.
What this template is for
Manufacturing facilities must decide what to produce given limited resources. This template models a factory with multiple machines producing different products, where:
- Each machine has limited hours available and a machine-specific hourly operating cost.
- Each product has a selling price and a minimum production requirement.
- Different machines take different amounts of time to produce each product.
The goal is to find the optimal product mix—how much of each product to make on each machine—to maximize profit (revenue minus machine operating costs) while respecting machine capacity and meeting minimum production targets.
This template uses RelationalAI’s Prescriptive reasoning capabilities (optimization) to compute a profit-maximizing production plan that meets minimum requirements while respecting machine capacity.
Who this is for
- You want a small, end-to-end example of prescriptive reasoning (optimization) using RelationalAI Semantics.
- You’re comfortable with basic Python and the idea of constraints + objectives.
What you’ll build
- A semantic model of machines, products, and machine-product production times.
- A linear program (LP) with one continuous decision variable per feasible machine-product pair.
- Capacity and minimum-production constraints.
- A profit-maximizing objective.
What’s included
factory_production.py— defines the semantic model, optimization problem, and prints a solutiondata/— sample CSV inputs (machines.csv,products.csv,production_times.csv)
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 using 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/factory_production.zipunzip factory_production.zipcd factory_production -
Create and activate a virtual environment:
Terminal window python -m venv .venvsource .venv/bin/activatepython -m pip install -U pip -
Install dependencies:
Terminal window python -m pip install . -
Initialize your RAI profile:
Terminal window rai init -
Run the template script:
Terminal window python factory_production.py -
Expected output:
Status: OPTIMAL Total profit: $20977.78
Production plan: machine product quantity Machine_A Widget 80.000000 Machine_B Device 38.888889 Machine_C Gadget 15.000000 Machine_C Widget 90.000000Template structure
.├─ README.md├─ pyproject.toml├─ factory_production.py # main runner / entrypoint└─ data/ # sample input data ├─ machines.csv ├─ products.csv └─ production_times.csvStart here: factory_production.py
Sample data
Data files are located in the data/ subdirectory.
machines.csv
| Column | Description |
|---|---|
id | Unique machine identifier |
name | Machine name |
hours_available | Hours available per period |
hourly_cost | Operating cost per hour ($) |
products.csv
| Column | Description |
|---|---|
id | Unique product identifier |
name | Product name |
price | Selling price per unit ($) |
min_production | Minimum units that must be produced |
production_times.csv
| Column | Description |
|---|---|
machine_id | Reference to machine |
product_id | Reference to product |
hours_per_unit | Hours required to produce one unit |
Model overview
The optimization model is built around four concepts.
Machine
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from machines.csv |
name | string | No | Used for output labels |
hours_available | float | No | Capacity for each machine |
hourly_cost | float | No | Cost term in the objective |
Product
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from products.csv |
name | string | No | Used for output labels |
price | float | No | Revenue per unit |
min_production | int | No | Minimum units required |
ProductionTime
| Property | Type | Identifying? | Notes |
|---|---|---|---|
machine | Machine | Part of compound key | Machine for the route |
product | Product | Part of compound key | Product for the route |
hours_per_unit | float | No | Time to make one unit |
Production (decision concept)
| Property | Type | Identifying? | Notes |
|---|---|---|---|
prod_time | ProductionTime | Yes | One variable per machine-product route |
quantity | float | No | Continuous decision variable, lower bounded by 0 |
How it works
This section walks through the highlights in factory_production.py.
Import libraries and configure inputs
This template uses Concept objects from relationalai.semantics to model machines, products, and production times, and uses Solver and SolverModel from relationalai.semantics.reasoners.optimization to define and solve the linear program:
from pathlib import Path
import pandasfrom pandas import read_csv
from relationalai.semantics import Model, Relationship, data, define, require, select, sum, wherefrom relationalai.semantics.reasoners.optimization import Solver, SolverModel
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("factory")Define concepts and load CSV data
Next, it declares the Machine, Product, and ProductionTime concepts and loads the corresponding CSV tables. data(...).into(...) creates entities from machines.csv and products.csv, and where(...).define(...) joins production_times.csv onto those concepts:
# Machine concept: represents a production machine with available hours and hourly costMachine = 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}")Machine.hourly_cost = model.Property("{Machine} has {hourly_cost:float}")
# Load machine data from CSV and create Machine entities.data(read_csv(DATA_DIR / "machines.csv")).into(Machine, keys=["id"])
# Product concept: represents a product with price and minimum production requirementsProduct = model.Concept("Product")Product.id = model.Property("{Product} has {id:int}")Product.name = model.Property("{Product} has {name:string}")Product.price = model.Property("{Product} has {price:float}")Product.min_production = model.Property("{Product} has {min_production:int}")
# Load product data from CSV and create Product entities.data(read_csv(DATA_DIR / "products.csv")).into(Product, keys=["id"])
# ProdTime concept: represents the time required to produce one unit of a product on a machineProdTime = model.Concept("ProductionTime")ProdTime.machine = model.Relationship("{ProductionTime} on {machine:Machine}")ProdTime.product = model.Relationship("{ProductionTime} of {product:Product}")ProdTime.hours_per_unit = model.Property("{ProductionTime} takes {hours_per_unit:float}")
# Load production time data from CSV.times_data = data(read_csv(DATA_DIR / "production_times.csv"))
# Define ProductionTime entities by joining machine/product IDs from the CSV with# the Machine and Product concepts.where( Machine.id == times_data.machine_id, Product.id == times_data.product_id,).define( ProdTime.new(machine=Machine, product=Product, hours_per_unit=times_data.hours_per_unit))Define decision variables, constraints, and objective
Then it creates one continuous, non-negative decision variable per machine–product route, adds machine-hour and minimum-production constraints with require(...), and maximizes profit:
# Decision concept: production quantities for each machine/productProduction = model.Concept("Production")Production.prod_time = model.Relationship("{Production} uses {prod_time:ProductionTime}")Production.x_quantity = model.Property("{Production} has {quantity:float}")
# Define one Production entity per machine-product ProductionTime record.define(Production.new(prod_time=ProdTime))
ProductionRef = Production.ref()
s = SolverModel(model, "cont")
# Variable: production quantitys.solve_for( Production.x_quantity, name=["qty", Production.prod_time.machine.name, Production.prod_time.product.name], lower=0,)
# Constraint: total production hours per machine <= hours_availabletotal_hours = sum( ProductionRef.x_quantity * ProductionRef.prod_time.hours_per_unit).where( ProductionRef.prod_time.machine == Machine).per(Machine)machine_limit = require(total_hours <= Machine.hours_available)s.satisfy(machine_limit)
# Constraint: total production per product >= min_productiontotal_produced = sum(ProductionRef.x_quantity).where(ProductionRef.prod_time.product == Product).per(Product)meet_minimum = require(total_produced >= Product.min_production)s.satisfy(meet_minimum)
# Objective: maximize profit (revenue - machine costs)revenue = sum(Production.x_quantity * Production.prod_time.product.price)machine_cost = sum( Production.x_quantity * Production.prod_time.hours_per_unit * Production.prod_time.machine.hourly_cost)profit = revenue - machine_costs.maximize(profit)Solve and print results
Finally, it solves using the HiGHS backend and prints only rows where Production.x_quantity > 0:
solver = Solver("highs")s.solve(solver, time_limit_sec=60)
print(f"Status: {s.termination_status}")print(f"Total profit: ${s.objective_value:.2f}")
plan = select( Production.prod_time.machine.name.alias("machine"), Production.prod_time.product.name.alias("product"), Production.x_quantity).where(Production.x_quantity > 0).to_df()
print("\nProduction plan:")print(plan.to_string(index=False))Customize this template
Use your own data
- Replace the CSV files under
data/. - Keep IDs consistent across files (
machine_id/product_idmust exist in the respective tables).
Extend the model
- Add maximum production limits, inventory constraints, or demand caps.
- Add setup/changeover costs and binary on/off decisions (MILP).
- Switch to integer quantities (or start from ../production_planning/README.md).
Troubleshooting
Connection/auth issues
- Re-run
rai initand confirm the selected profile is correct. - Ensure you have access to the RAI Native App in Snowflake.
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:
machines.csv:id,name,hours_available,hourly_costproducts.csv:id,name,price,min_productionproduction_times.csv:machine_id,product_id,hours_per_unit
Status is INFEASIBLE
- Check that product minimums can be met with available machine hours.
- Ensure each required product has at least one route in
production_times.csv.
No rows printed in the production plan
- The output filters on
Production.x_quantity > 0. - If quantities are extremely small, print the full variable set or relax the filter.
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 the time limit in
factory_production.py.
What this template is for
This template uses Prescriptive reasoning to maximize factory production profit under resource constraints, and then to read back the sensitivity marginals a planner asks next — all from a single solve. It is a compact, textbook product-mix linear program: a small, fully hand-checkable setting for learning what shadow prices and reduced costs mean.
Manufacturing operations must decide how much of each product to produce at each factory to maximize profit, given limited resources and bounded demand. Each product has a production rate (units per hour of resource), a profit per unit, and a maximum demand. Each factory has a fixed number of available resource-hours.
This template formulates the problem as a linear program. Decision variables represent the quantity of each product to produce, bounded above by demand. A per-factory constraint keeps total resource usage within availability, and the objective maximizes total profit.
A plain solve answers “what is the most profitable production plan?”. Adding sensitivity=True to the solve ALSO answers the marginal questions — in the same solve, with the answers read straight off the variable and constraint objects:
- Capacity shadow price (
cap.shadow_price): how much total profit moves per extra hour at a factory. A capacity with idle hours prices at zero (it is not the bottleneck); a positive price flags a binding capacity worth expanding — so this ranks which factory to expand first. (The rule is one-way: slack ⇒ zero price, and a positive price ⇒ binding.) - Product reduced cost and basis status (
quantity_var.reduced_cost/quantity_var.basis_status): a product held at its demand cap shows a positive reduced cost here (the extra profit per unit of demand allowed); the swing product that sets the binding factory’s marginal price isBASICat ~0.
Because the objective is a maximization, the capacity shadow price is the mirror image of a minimize-cost model — a binding <= capacity prices >= 0 here, versus <= 0 in the cost-minimizing supplier_reliability template. The nonbasic bound marginals shown in both tables are non-negative; what flips for the product marginal is the active bound — a demand-capped product sits at its upper bound (NONBASIC_AT_UPPER) here, while a priced-out supply lane sits at its lower bound of zero (NONBASIC_AT_LOWER) there. Laying the two reduced-cost tables side by side is the fastest way to internalize the conventions.
Production-planning learning ladder
- Factory Production (this template) — single-period product-mix LP with sensitivity analysis.
production_planning— multi-machine assignment with integer decisions and demand multipliers.demand_planning_temporal— multi-period production + inventory across sites with date-filtered planning horizon.
Who this is for
- Manufacturing planners optimizing production schedules and ranking capacity investments
- Operations researchers learning resource-constrained profit maximization and LP duality
- Data scientists who want shadow prices and reduced costs without leaving the model
- Anyone learning what “sensitivity analysis” means on a small, fully hand-checkable LP
What you’ll build
- A linear programming model that determines optimal production quantities per product
- Per-factory resource capacity constraints, captured as handles for marginal read-back
- Demand upper bounds on each product (whose marginals surface as reduced costs)
- A one-solve sensitivity report: capacity shadow prices, product reduced costs, and basis status, each joined back to its factory/product by entity key
What’s included
factory_production.py— Main script defining the optimization model, constraints, objective, and sensitivity read-backdata/factories.csv— Factory names and available resource-hoursdata/products.csv— Products with factory assignment, production rate, profit, and demand cappyproject.toml— Python package 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
- RelationalAI Python SDK (
relationalai) == 1.11.0
Quickstart
-
Download ZIP:
Terminal window curl -O https://docs.relational.ai/templates/zips/v1/factory_production.zipunzip factory_production.zipcd factory_production -
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 factory_production.py -
Expected output (model and solver display trimmed):
Baseline status: OPTIMAL, total profit: 200000.00Production plan:factory product quantityamazing_brewery ales 2000.0amazing_brewery stouts 1000.0steel_factory bands 6000.0steel_factory coils 1400.0Factory capacity shadow prices (d profit / d hour):factory avail shadow_priceamazing_brewery 30.0 -0.0steel_factory 40.0 4200.0Product reduced costs and basis status:factory product reduced_cost basis_statusamazing_brewery ales 2.0 NONBASIC_AT_UPPERamazing_brewery stouts 4.0 NONBASIC_AT_UPPERsteel_factory bands 4.0 NONBASIC_AT_UPPERsteel_factory coils -0.0 BASICMost profit-sensitive capacity: steel_factory (d profit / d hour = +4200.00)==================================================Factory Capacity Summary==================================================factory avail hours_used idleamazing_brewery 30.0 25.0 5.0steel_factory 40.0 40.0 0.0Reading the result:
steel_factoryfills all 40 hours (it is the binding factory, so its capacity prices at +4200/hour — the per-hour profit of the swing productcoils, i.e. its 30/unit profit × 140 units/hour rate).amazing_brewerymeets all demand in 25 of its 30 hours, so its capacity is slack and prices at 0 — its bottleneck is demand, not capacity. Each of these demand-capped products (bands,stouts,ales) carries a positive reduced cost here: the profit you would gain per extra unit of demand allowed.
Template structure
.├── README.md├── pyproject.toml├── factory_production.py└── data/ ├── factories.csv └── products.csvHow it works
1. Define concepts and load data
The model defines Factory (with available resource-hours) and Product (with factory assignment, production rate, profit, and demand). A relationship links products to their factory:
Factory = Concept("Factory", identify_by={"name": String})Factory.avail = Property(f"{Factory} has {Float:avail}")
Product = Concept("Product", identify_by={"name": String, "factory_name": String})Product.factory = Property(f"{Product} is produced by {Factory}")Product.rate = Property(f"{Product} has {Float:rate}")Product.profit = Property(f"{Product} has {Float:profit}")Product.demand = Property(f"{Product} has {Integer:demand}")2. Decision variables
Each product gets a continuous variable bounded between 0 and its demand cap. The demand cap is the variable’s upper bound (not a separate constraint), so its marginal surfaces later as the variable’s reduced cost:
quantity_var = problem.solve_for( Product.x_quantity, name=["qty", Product.factory.name, Product.name], lower=0, upper=Product.demand, populate=False,)3. Capacity constraint and objective
Each factory’s total resource usage must not exceed its availability. The constraint is captured as a handle (cap), named per factory (a readable label), and declared with keyed_by={"factory": Factory}, so each instance’s shadow price reads back through that entity key (cap.factory) rather than by parsing a name string. The objective maximizes total profit across all factories:
cap = problem.satisfy( model.require( sum(Product.x_quantity / Product.rate) .where(Product.factory == Factory) .per(Factory) <= Factory.avail ), name=["cap", Factory.name], keyed_by={"factory": Factory},)
problem.maximize(sum(Product.profit * Product.x_quantity))problem.solve("highs", time_limit_sec=60, sensitivity=True)4. Read the sensitivity marginals
After a sensitivity=True solve, the marginals are attributes on the captured handles. A constraint carries the entity back-pointer declared with keyed_by (cap.factory) and a variable carries an automatic one to its product (quantity_var.product), so each marginal joins to that entity’s own data by key — no pandas, no name parsing:
# Which factory's capacity to expand first?model.select(cap.factory.name, cap.factory.avail, cap.shadow_price).inspect()
# Which products are pinned at their demand cap, and which is the swing product?model.select( quantity_var.product.factory.name, quantity_var.product.name, quantity_var.reduced_cost, quantity_var.basis_status,).inspect()(.inspect() prints the rows for a quick look; the script materializes the same selects as DataFrames with .to_df() for its printed report and assertions.)
Sensitivity marginals are exact for a linear program. They describe the rate of change at the current optimum — the range over which that rate holds is not reported (there is no RHS/coefficient ranging) — and a large, discrete change (adding a factory, removing a product) is a structural change best answered by re-solving.
Customize this template
- Find the demand bottleneck: Raise
amazing_brewery’s demand caps inproducts.csv. Once its 30 hours bind, its capacity shadow price jumps from 0 to positive — capacity becomes the bottleneck. - Shift the swing product: Lower
steel_factory’savailinfactories.csv.coils(the basic, swing product) shrinks but stays the swing down to just above 30 hours, so the shadow price holds at 4200. At exactly 30 hourscoilshits zero — a degenerate breakpoint where the marginal is one-sided — and below 30 hoursbandsbecomes the swing and the price rises to 5000. - Add more factories and products: Extend the CSV files. The model and the per-factory marginals pick up new rows automatically.
- Integer production: Change the variable type from continuous to integer if products must be produced in whole units. Note that sensitivity marginals are an LP concept — they are reported only for continuous (LP/QP) problems, and are empty for integer models.
Troubleshooting
Shadow prices or reduced costs are all empty
Sensitivity marginals are an LP/QP concept. They are populated only when the problem is continuous and solved with sensitivity=True. An integer (MIP) model returns no marginals — keep the production variables continuous to see them.
A factory's capacity shadow price is zero
Usually that factory has idle resource-hours — slack capacity, so an extra hour buys nothing, and its bottleneck is elsewhere (typically product demand). Confirm with the idle column in the capacity summary: a positive idle is genuine slack. (Less commonly, a binding capacity can also price at zero under degeneracy — zero idle hours yet a zero price — so read the idle column, not the price alone.)
rai init fails or connection errors
Ensure your Snowflake credentials are configured correctly and that the RAI Native App is installed on your account. Run rai init again and verify the connection settings.
ModuleNotFoundError for relationalai
Make sure you activated the virtual environment and ran python -m pip install . from the template directory. The pyproject.toml declares the required dependencies.
Products missing from the plan
A product with zero quantity is not profitable enough to justify its resource usage given tighter, more profitable competitors. Its reduced cost tells you how far its economics are from being worth producing. Increase factory avail, raise the product’s profit, or lower its production rate to bring it into the plan.