Supplier Reliability
Select suppliers to meet product demand while balancing cost and reliability.
Select suppliers to meet product demand while balancing cost and reliability.
Select suppliers to meet product demand at minimum cost, with sensitivity marginals and supplier-disruption scenario analysis.
Browse files
Browse files
Browse files
What this template is for
Procurement teams routinely need to decide which suppliers to buy from to meet product demand. The hard part is that sourcing decisions are rarely “cost only”: cheaper suppliers can be less reliable (late deliveries, quality issues, disruptions), while highly reliable suppliers can be more expensive or have limited capacity.
This template uses RelationalAI’s prescriptive reasoning (optimization) capabilities to choose a feasible order plan that meets demand and supplier capacity constraints, while minimizing total cost. You can optionally add a reliability penalty to quantify the trade-off between low price and delivery risk, and run disruption scenarios that exclude a supplier entirely.
Who this is for
- You want a small, 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
Supplier,Product, and supplier–productSupplyOptiondata. - A continuous decision variable (
Order.x_quantity) for how much to buy through each supply option. - Constraints that enforce supplier capacity and product demand satisfaction.
- An objective that minimizes procurement cost, with an optional reliability penalty.
- A small scenario analysis loop that re-solves the model after excluding a supplier.
What’s included
- Model + solve script:
supplier_reliability.py - Sample data:
data/suppliers.csv,data/products.csv,data/supply_options.csv - Outputs: solver status/objective per scenario, an orders table per scenario, 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/supplier_reliability.zipunzip supplier_reliability.zipcd supplier_reliability -
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 supplier_reliability.py -
Expected output
Your exact plan may vary if multiple optima exist, but you should see an OPTIMAL status and an orders table for each scenario:
Running scenario: excluded_supplier = NoneStatus: OPTIMAL, Objective: 4850.0Orders:name valueqty_SupplierB_Gadget 150.0qty_SupplierC_Component 200.0qty_SupplierC_Gadget 100.0qty_SupplierC_Widget 300.0==================================================Scenario Analysis Summary==================================================None: OPTIMAL, obj=4850.0SupplierC: OPTIMAL, obj=6750.0SupplierB: OPTIMAL, obj=5150.0
Template structure
.├─ README.md├─ pyproject.toml├─ supplier_reliability.py # main runner / entrypoint└─ data/ # sample input data ├─ suppliers.csv ├─ products.csv └─ supply_options.csvStart here: supplier_reliability.py
Sample data
Data files are in data/.
suppliers.csv
Defines the supplier master data (reliability and capacity).
| Column | Meaning |
|---|---|
id | Unique supplier identifier |
name | Supplier name (used for labeling variables/output) |
reliability | Reliability score (0 to 1, higher is better) |
capacity | Maximum total units the supplier can provide |
products.csv
Defines product demand requirements.
| Column | Meaning |
|---|---|
id | Unique product identifier |
name | Product name (used for labeling variables/output) |
demand | Units required |
supply_options.csv
Defines which suppliers can supply which products, and the per-unit price for each option.
| Column | Meaning |
|---|---|
id | Unique supply option identifier |
supplier_id | Foreign key to suppliers.csv.id |
product_id | Foreign key to products.csv.id |
cost_per_unit | Cost per unit for this supplier–product option |
Model overview
The semantic model for this template is built around four concepts.
Supplier
Suppliers are the sourcing entities with reliability and total capacity.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Loaded as the key from data/suppliers.csv |
name | string | No | Used for output labeling and scenario selection |
reliability | float | No | Used in the optional reliability penalty term |
capacity | int | No | Upper bound in the capacity constraint |
Product
Products have demand requirements that must be met.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Loaded as the key from data/products.csv |
name | string | No | Used for output labeling |
demand | int | No | Lower bound in the demand constraint |
SupplyOption
A feasible supplier–product option with a per-unit cost.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Loaded from data/supply_options.csv.id |
supplier | Supplier | No | Joined via data/supply_options.csv.supplier_id |
product | Product | No | Joined via data/supply_options.csv.product_id |
cost_per_unit | float | No | Used in the direct cost term |
Order (decision concept)
One decision row per SupplyOption, with a non-negative quantity chosen by the solver.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
option | SupplyOption | Yes | One order per supply option |
quantity | float | No | Continuous decision variable, lower-bounded by 0 |
supplier | Supplier | No | Derived from Order.option |
product | Product | No | Derived from Order.option |
cost_per_unit | float | No | Derived from Order.option |
How it works
This section walks through the highlights in supplier_reliability.py.
Import libraries and configure inputs
First, the script imports the Semantics APIs and defines the data folder (DATA_DIR), along with the scenario configuration that controls which supplier to exclude:
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
# Parameters.RELIABILITY_WEIGHT = 0.0 # Penalty weight for unreliable suppliers (0 = cost only).EXCLUDED_SUPPLIER = None
# Scenarios (what-if analysis).SCENARIO_PARAM = "excluded_supplier"SCENARIO_VALUES = [None, "SupplierC", "SupplierB"]SCENARIO_CONCEPT = "Supplier" # Entity type for exclusion scenarios.Define concepts and load CSV data
Next, it creates a Model, defines Supplier, Product, and SupplyOption, and loads the CSVs with data(...).into(...) and a where(...).define(...) join:
# --------------------------------------------------# Define semantic model & load data# --------------------------------------------------
# Create a Semantics model container.model = Model("supplier_reliability", config=globals().get("config", None), use_lqp=False)
# Supplier concept: suppliers with reliability scores and capacity.Supplier = model.Concept("Supplier")Supplier.id = model.Property("{Supplier} has {id:int}")Supplier.name = model.Property("{Supplier} has {name:string}")Supplier.reliability = model.Property("{Supplier} has {reliability:float}")Supplier.capacity = model.Property("{Supplier} has {capacity:int}")
# Load supplier data from CSV.data(read_csv(DATA_DIR / "suppliers.csv")).into(Supplier, keys=["id"])
# Product concept: products with demand requirements.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}")
# Load product data from CSV.data(read_csv(DATA_DIR / "products.csv")).into(Product, keys=["id"])
# SupplyOption concept: supplier–product supply options with a per-unit cost.SupplyOption = model.Concept("SupplyOption")SupplyOption.id = model.Property("{SupplyOption} has {id:int}")SupplyOption.supplier = model.Property("{SupplyOption} from {supplier:Supplier}")SupplyOption.product = model.Property("{SupplyOption} for {product:Product}")SupplyOption.cost_per_unit = model.Property("{SupplyOption} has {cost_per_unit:float}")
# Load supply option data from CSV.options_data = data(read_csv(DATA_DIR / "supply_options.csv"))
# Create one SupplyOption entity per row by joining supplier_id and product_id.where( Supplier.id == options_data.supplier_id, Product.id == options_data.product_id).define( SupplyOption.new( id=options_data.id, supplier=Supplier, product=Product, cost_per_unit=options_data.cost_per_unit, ))Define decision variables, constraints, and objective
Then it declares an Order decision concept and builds a continuous optimization model. The solver chooses Order.x_quantity, enforces capacity and demand with require(...), and minimizes cost (optionally adding a reliability penalty):
# --------------------------------------------------# Model the decision problem# --------------------------------------------------
# Order decision concept: quantity ordered via each supply option.Order = model.Concept("Order")Order.option = model.Property("{Order} uses {option:SupplyOption}")Order.x_quantity = model.Property("{Order} has {quantity:float}")define(Order.new(option=SupplyOption))
# Derived properties for direct access in constraints and objective.Order.supplier = model.Property("{Order} has {supplier:Supplier}")define(Order.supplier(Supplier)).where( Order.option == SupplyOption, SupplyOption.supplier == Supplier,)
Order.product = model.Property("{Order} has {product:Product}")define(Order.product(Product)).where( Order.option == SupplyOption, SupplyOption.product == Product,)
Order.cost_per_unit = model.Property("{Order} has {cost_per_unit:float}")define(Order.cost_per_unit(SupplyOption.cost_per_unit)).where(Order.option == SupplyOption)
def build_formulation(s): """Register variables, constraints, and objective on the solver model.""" # Variable: order quantity s.solve_for(Order.x_quantity, name=["qty", Order.supplier.name, Order.product.name], lower=0)
# Constraint: total orders from supplier cannot exceed supplier capacity capacity_limit = require( sum(Order.x_quantity).where(Order.supplier == Supplier).per(Supplier) <= Supplier.capacity ) s.satisfy(capacity_limit)
# Constraint: demand satisfaction for each product meet_demand = require( sum(Order.x_quantity).where(Order.product == Product).per(Product) >= Product.demand ) s.satisfy(meet_demand)
# Constraint: exclude supplier if specified if EXCLUDED_SUPPLIER is not None: exclude = require(Order.x_quantity == 0).where(Order.supplier.name == EXCLUDED_SUPPLIER) s.satisfy(exclude)
# Objective: minimize cost with optional reliability penalty direct_cost = sum(Order.x_quantity * Order.cost_per_unit) if RELIABILITY_WEIGHT > 0: reliability_penalty = RELIABILITY_WEIGHT * sum( Order.x_quantity * (1.0 - Order.supplier.reliability) ) total_cost = direct_cost + reliability_penalty else: total_cost = direct_cost s.minimize(total_cost)Solve and print results
Finally, the template loops over scenarios, solves with HiGHS, and prints non-trivial order quantities (filtered with float > 0.001):
# --------------------------------------------------# Solve with Scenario Analysis (Supplier Exclusion)# --------------------------------------------------
scenario_results = []
for scenario_value in SCENARIO_VALUES: print(f"\nRunning scenario: {SCENARIO_PARAM} = {scenario_value}")
# Set scenario parameter (entity to exclude). EXCLUDED_SUPPLIER = scenario_value
# Create a 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 order 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("\n Orders:") print(qty_df.to_string(index=False))
# Summary.print("\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
Use your own data
- Replace the CSVs in
data/with your own data. - Keep the required headers:
suppliers.csv:id,name,reliability,capacityproducts.csv:id,name,demandsupply_options.csv:id,supplier_id,product_id,cost_per_unit
- Ensure foreign keys match (
supplier_idexists insuppliers.csv.id, andproduct_idexists inproducts.csv.id).
Tune parameters
Edit parameters in supplier_reliability.py:
RELIABILITY_WEIGHT: set to0.0for pure cost minimization, or increase it to penalize unreliable sourcing.SCENARIO_VALUES: change which supplier names are excluded in the what-if scenarios.
Extend the model
Common extensions include:
- Add per-product maximum sourcing shares (supplier diversification).
- Add fixed costs for activating a supplier.
- Replace the reliability penalty with a hard constraint (e.g., minimum average reliability).
Troubleshooting
ModuleNotFoundError when running the script
- Confirm your virtual environment is active.
- Reinstall dependencies from the template folder:
python -m pip install . - Confirm you’re using Python 3.10+.
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.
CSV loading fails (missing file or column)
- Confirm the CSVs exist under
data/and the filenames match. - Ensure the headers match the expected schema:
suppliers.csv:id,name,reliability,capacityproducts.csv:id,name,demandsupply_options.csv:id,supplier_id,product_id,cost_per_unit
Why do I get Status: INFEASIBLE?
- Check that total supplier capacity is sufficient to meet total demand.
- Confirm every product has at least one feasible option in
supply_options.csv. - If you are excluding a supplier, make sure remaining options can still satisfy demand.
Why are my orders empty?
- The script filters variables with
float > 0.001. If you suspect near-zero values, prints.variable_values().to_df()without filtering. - Confirm demands are non-zero and supply options exist for each product.
What this template is for
Procurement teams routinely need to decide which suppliers to buy from to meet product demand. The hard part is that sourcing decisions are rarely “cost only”: cheaper suppliers can be less reliable (late deliveries, quality issues, disruptions), while highly reliable suppliers can be more expensive or have limited capacity.
This template uses RelationalAI’s prescriptive reasoning (optimization) capabilities to choose a feasible order plan that meets demand and supplier capacity constraints, while minimizing total cost. You can optionally add a reliability penalty to quantify the trade-off between low price and delivery risk, and run disruption scenarios that exclude a supplier entirely.
Who this is for
- You want a small, 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
Supplier,Product, and supplier–productSupplyOptiondata. - A continuous decision variable (
Order.x_quantity) for how much to buy through each supply option. - Constraints that enforce supplier capacity and product demand satisfaction.
- An objective that minimizes procurement cost, with an optional reliability penalty.
- A small scenario analysis loop that re-solves the model after excluding a supplier.
What’s included
- Model + solve script:
supplier_reliability.py - Sample data:
data/suppliers.csv,data/products.csv,data/supply_options.csv - Outputs: solver status/objective per scenario, an orders table per scenario, 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/supplier_reliability.zipunzip supplier_reliability.zipcd supplier_reliability -
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 supplier_reliability.py -
Expected output
Your exact plan may vary if multiple optima exist, but you should see an OPTIMAL status and an orders table for each scenario:
Running scenario: excluded_supplier = NoneStatus: OPTIMAL, Objective: 4850.0Orders:name valueqty_SupplierB_Gadget 150.0qty_SupplierC_Component 200.0qty_SupplierC_Gadget 100.0qty_SupplierC_Widget 300.0==================================================Scenario Analysis Summary==================================================None: OPTIMAL, obj=4850.0SupplierC: OPTIMAL, obj=6750.0SupplierB: OPTIMAL, obj=5150.0
Template structure
.├─ README.md├─ pyproject.toml├─ supplier_reliability.py # main runner / entrypoint└─ data/ # sample input data ├─ suppliers.csv ├─ products.csv └─ supply_options.csvStart here: supplier_reliability.py
Sample data
Data files are in data/.
suppliers.csv
Defines the supplier master data (reliability and capacity).
| Column | Meaning |
|---|---|
id | Unique supplier identifier |
name | Supplier name (used for labeling variables/output) |
reliability | Reliability score (0 to 1, higher is better) |
capacity | Maximum total units the supplier can provide |
products.csv
Defines product demand requirements.
| Column | Meaning |
|---|---|
id | Unique product identifier |
name | Product name (used for labeling variables/output) |
demand | Units required |
supply_options.csv
Defines which suppliers can supply which products, and the per-unit price for each option.
| Column | Meaning |
|---|---|
id | Unique supply option identifier |
supplier_id | Foreign key to suppliers.csv.id |
product_id | Foreign key to products.csv.id |
cost_per_unit | Cost per unit for this supplier–product option |
Model overview
The semantic model for this template is built around four concepts.
Supplier
Suppliers are the sourcing entities with reliability and total capacity.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Loaded as the key from data/suppliers.csv |
name | string | No | Used for output labeling and scenario selection |
reliability | float | No | Used in the optional reliability penalty term |
capacity | int | No | Upper bound in the capacity constraint |
Product
Products have demand requirements that must be met.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Loaded as the key from data/products.csv |
name | string | No | Used for output labeling |
demand | int | No | Lower bound in the demand constraint |
SupplyOption
A feasible supplier–product option with a per-unit cost.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Loaded from data/supply_options.csv.id |
supplier | Supplier | No | Joined via data/supply_options.csv.supplier_id |
product | Product | No | Joined via data/supply_options.csv.product_id |
cost_per_unit | float | No | Used in the direct cost term |
Order (decision concept)
One decision row per SupplyOption, with a non-negative quantity chosen by the solver.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
option | SupplyOption | Yes | One order per supply option |
quantity | float | No | Continuous decision variable, lower-bounded by 0 |
supplier | Supplier | No | Derived from Order.option |
product | Product | No | Derived from Order.option |
cost_per_unit | float | No | Derived from Order.option |
How it works
This section walks through the highlights in supplier_reliability.py.
Import libraries and configure inputs
First, the script imports the Semantics APIs and defines the data folder (DATA_DIR), along with the scenario configuration that controls which supplier to exclude:
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
# Parameters.RELIABILITY_WEIGHT = 0.0 # Penalty weight for unreliable suppliers (0 = cost only).EXCLUDED_SUPPLIER = None
# Scenarios (what-if analysis).SCENARIO_PARAM = "excluded_supplier"SCENARIO_VALUES = [None, "SupplierC", "SupplierB"]SCENARIO_CONCEPT = "Supplier" # Entity type for exclusion scenarios.Define concepts and load CSV data
Next, it creates a Model, defines Supplier, Product, and SupplyOption, and loads the CSVs with data(...).into(...) and a where(...).define(...) join:
# --------------------------------------------------# Define semantic model & load data# --------------------------------------------------
# Create a Semantics model container.model = Model("supplier_reliability", config=globals().get("config", None))
# Supplier concept: suppliers with reliability scores and capacity.Supplier = model.Concept("Supplier")Supplier.id = model.Property("{Supplier} has {id:int}")Supplier.name = model.Property("{Supplier} has {name:string}")Supplier.reliability = model.Property("{Supplier} has {reliability:float}")Supplier.capacity = model.Property("{Supplier} has {capacity:int}")
# Load supplier data from CSV.data(read_csv(DATA_DIR / "suppliers.csv")).into(Supplier, keys=["id"])
# Product concept: products with demand requirements.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}")
# Load product data from CSV.data(read_csv(DATA_DIR / "products.csv")).into(Product, keys=["id"])
# SupplyOption concept: supplier–product supply options with a per-unit cost.SupplyOption = model.Concept("SupplyOption")SupplyOption.id = model.Property("{SupplyOption} has {id:int}")SupplyOption.supplier = model.Relationship("{SupplyOption} from {supplier:Supplier}")SupplyOption.product = model.Relationship("{SupplyOption} for {product:Product}")SupplyOption.cost_per_unit = model.Property("{SupplyOption} has {cost_per_unit:float}")
# Load supply option data from CSV.options_data = data(read_csv(DATA_DIR / "supply_options.csv"))
# Create one SupplyOption entity per row by joining supplier_id and product_id.where( Supplier.id == options_data.supplier_id, Product.id == options_data.product_id).define( SupplyOption.new( id=options_data.id, supplier=Supplier, product=Product, cost_per_unit=options_data.cost_per_unit, ))Define decision variables, constraints, and objective
Then it declares an Order decision concept and builds a continuous optimization model. The solver chooses Order.x_quantity, enforces capacity and demand with require(...), and minimizes cost (optionally adding a reliability penalty):
# --------------------------------------------------# Model the decision problem# --------------------------------------------------
# Order decision concept: quantity ordered via each supply option.Order = model.Concept("Order")Order.option = model.Relationship("{Order} uses {option:SupplyOption}")Order.x_quantity = model.Property("{Order} has {quantity:float}")define(Order.new(option=SupplyOption))
# Derived properties for direct access in constraints and objective.Order.supplier = model.Relationship("{Order} has {supplier:Supplier}")define(Order.supplier(Supplier)).where( Order.option == SupplyOption, SupplyOption.supplier == Supplier,)
Order.product = model.Relationship("{Order} has {product:Product}")define(Order.product(Product)).where( Order.option == SupplyOption, SupplyOption.product == Product,)
Order.cost_per_unit = model.Property("{Order} has {cost_per_unit:float}")define(Order.cost_per_unit(SupplyOption.cost_per_unit)).where(Order.option == SupplyOption)
def build_formulation(s): """Register variables, constraints, and objective on the solver model.""" # Variable: order quantity s.solve_for(Order.x_quantity, name=["qty", Order.supplier.name, Order.product.name], lower=0)
# Constraint: total orders from supplier cannot exceed supplier capacity capacity_limit = require( sum(Order.x_quantity).where(Order.supplier == Supplier).per(Supplier) <= Supplier.capacity ) s.satisfy(capacity_limit)
# Constraint: demand satisfaction for each product meet_demand = require( sum(Order.x_quantity).where(Order.product == Product).per(Product) >= Product.demand ) s.satisfy(meet_demand)
# Constraint: exclude supplier if specified if EXCLUDED_SUPPLIER is not None: exclude = require(Order.x_quantity == 0).where(Order.supplier.name == EXCLUDED_SUPPLIER) s.satisfy(exclude)
# Objective: minimize cost with optional reliability penalty direct_cost = sum(Order.x_quantity * Order.cost_per_unit) if RELIABILITY_WEIGHT > 0: reliability_penalty = RELIABILITY_WEIGHT * sum( Order.x_quantity * (1.0 - Order.supplier.reliability) ) total_cost = direct_cost + reliability_penalty else: total_cost = direct_cost s.minimize(total_cost)Solve and print results
Finally, the template loops over scenarios, solves with HiGHS, and prints non-trivial order quantities (filtered with value > 0.001):
# --------------------------------------------------# Solve with Scenario Analysis (Supplier Exclusion)# --------------------------------------------------
scenario_results = []
for scenario_value in SCENARIO_VALUES: print(f"\nRunning scenario: {SCENARIO_PARAM} = {scenario_value}")
# Set scenario parameter (entity to exclude). EXCLUDED_SUPPLIER = scenario_value
# Create a 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 order 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("\n Orders:") print(qty_df.to_string(index=False))
# Summary.print("\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
Use your own data
- Replace the CSVs in
data/with your own data. - Keep the required headers:
suppliers.csv:id,name,reliability,capacityproducts.csv:id,name,demandsupply_options.csv:id,supplier_id,product_id,cost_per_unit
- Ensure foreign keys match (
supplier_idexists insuppliers.csv.id, andproduct_idexists inproducts.csv.id).
Tune parameters
Edit parameters in supplier_reliability.py:
RELIABILITY_WEIGHT: set to0.0for pure cost minimization, or increase it to penalize unreliable sourcing.SCENARIO_VALUES: change which supplier names are excluded in the what-if scenarios.
Extend the model
Common extensions include:
- Add per-product maximum sourcing shares (supplier diversification).
- Add fixed costs for activating a supplier.
- Replace the reliability penalty with a hard constraint (e.g., minimum average reliability).
Troubleshooting
ModuleNotFoundError when running the script
- Confirm your virtual environment is active.
- Reinstall dependencies from the template folder:
python -m pip install . - Confirm you’re using Python 3.10+.
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.
CSV loading fails (missing file or column)
- Confirm the CSVs exist under
data/and the filenames match. - Ensure the headers match the expected schema:
suppliers.csv:id,name,reliability,capacityproducts.csv:id,name,demandsupply_options.csv:id,supplier_id,product_id,cost_per_unit
Why do I get Status: INFEASIBLE?
- Check that total supplier capacity is sufficient to meet total demand.
- Confirm every product has at least one feasible option in
supply_options.csv. - If you are excluding a supplier, make sure remaining options can still satisfy demand.
Why are my orders empty?
- The script filters variables with
float > 0.001. If you suspect near-zero values, prints.variable_values().to_df()without filtering. - Confirm demands are non-zero and supply options exist for each product.
What this template is for
Procurement teams must choose which suppliers to source from when multiple options exist for each product. Each supplier has different pricing and capacity limits (plus a reliability score, carried as extension data — not priced into the objective, and not what drives the disruption scenarios below). The challenge is to meet all product demand at minimum cost without exceeding any supplier’s capacity.
This template uses Prescriptive reasoning to formulate the supplier selection problem as a linear program. It determines the optimal order quantities across supply options, ensuring that every product’s demand is met and no supplier is overloaded. The solver finds the cost-minimizing allocation automatically.
A plain solve answers “what is the cheapest sourcing plan?”. This template requests sensitivity analysis (solve(sensitivity=True)) on the baseline, which ALSO answers the marginal questions a planner asks next — in the same solve:
- Which supplier capacity is the bottleneck? The shadow price of each capacity constraint (
cap.shadow_price) is how much total cost moves per unit of that supplier’s capacity. A capacity with room to spare prices at zero; a nonzero price marks a binding bottleneck. - What does one more unit of demand cost? The shadow price of each demand constraint (
meet.shadow_price) is the marginal cost to serve one more unit of that product. - Which supply lanes are priced out? A lane’s reduced cost (
qty_var.reduced_cost) and basis status (qty_var.basis_status) show which options are unused and how far their cost must fall before they enter the plan.
Finally, the template demonstrates scenario analysis by re-solving the problem with specific suppliers fully excluded. This is a finite, structural change — what happens to cost and feasibility if a key supplier becomes unavailable? — that the local marginals contextualize but do not by themselves predict.
Who this is for
- Supply chain and procurement analysts evaluating supplier portfolios
- Operations researchers modeling multi-supplier sourcing decisions
- Developers learning how to build scenario analysis into optimization models with RelationalAI
What you’ll build
- A linear programming model that allocates order quantities across suppliers and products
- Capacity and demand satisfaction constraints
- A baseline solve with sensitivity analysis: capacity and demand shadow prices, plus lane reduced costs and basis status, read back by entity key
- A scenario loop that excludes suppliers one at a time to assess supply chain risk
- A summary comparing cost and feasibility across scenarios
What’s included
supplier_reliability.py— Main script defining the model, constraints, and scenario analysisdata/suppliers.csv— Supplier capacity and reliability scoresdata/products.csv— Product demand requirementsdata/supply_options.csv— Cost per unit for each supplier-product pairpyproject.toml— Python package configuration
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/supplier_reliability.zipunzip supplier_reliability.zipcd supplier_reliability -
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 supplier_reliability.py -
Expected output (model and solver display trimmed; marginal tables are read back by entity key):
Baseline status: OPTIMAL, objective: 4850.00Baseline orders:supplier product quantitySupplierB Component 150.0SupplierC Component 50.0SupplierC Gadget 250.0SupplierC Widget 300.0Lane reduced costs and basis status:supplier product reduced_cost basis_statusSupplierA Gadget 3.0 NONBASIC_AT_LOWERSupplierA Widget 2.0 NONBASIC_AT_LOWERSupplierB Component 0.0 BASICSupplierB Gadget 0.0 NONBASIC_AT_LOWERSupplierB Widget 0.0 NONBASIC_AT_LOWERSupplierC Component 0.0 BASICSupplierC Gadget 0.0 BASICSupplierC Widget 0.0 BASICSupplierD Component 2.0 NONBASIC_AT_LOWERSupplierD Gadget 2.0 NONBASIC_AT_LOWERSupplier capacity shadow prices (d cost / d capacity):supplier capacity shadow_priceSupplierA 500 0.0SupplierB 400 0.0SupplierC 600 -2.0SupplierD 350 0.0Product demand shadow prices (d cost / d demand):product demand shadow_priceComponent 200 7.0Gadget 250 9.0Widget 300 8.0Most cost-sensitive capacity: SupplierC (d cost / d capacity = -2.00)Running scenario: without_SupplierCStatus: OPTIMAL, Objective: 6750.0Orders:supplier product quantitySupplierA Widget 300.0SupplierB Component 200.0SupplierB Gadget 200.0SupplierD Gadget 50.0Running scenario: without_SupplierBStatus: OPTIMAL, Objective: 5150.0Orders:supplier product quantitySupplierC Component 200.0SupplierC Gadget 100.0SupplierC Widget 300.0SupplierD Gadget 150.0==================================================Scenario Analysis Summary==================================================baseline: OPTIMAL, obj=4850.00without_SupplierC: OPTIMAL, obj=6750.00without_SupplierB: OPTIMAL, obj=5150.00Reading the marginals. SupplierC is the cheapest source for every product, so it fills its 600-unit capacity and is the only binding capacity — its shadow price of
-2.0means each extra unit of SupplierC capacity would lower total cost by2 above SupplierC — an alternate-optimum tie, which is why the script asserts only that used lanes have ~0 reduced cost, never that every unused lane is strictly positive. The exact order quantities (and the matching basis statuses) above are one of several cost-equal optima — a different HiGHS build may land on another vertex with the same $4,850 objective and the same shadow prices. Scenario analysis. Removing SupplierC entirely increases cost by 39% (
6,750) as demand shifts to the more expensive SupplierA, SupplierB, and SupplierD — consistent with SupplierC’s high marginal value, though the duals (local marginals) do not by themselves predict the full impact of removing all 600 units. Removing SupplierB has less impact (+6%) since SupplierC absorbs most of the displaced volume.
Template structure
.├── README.md├── pyproject.toml├── supplier_reliability.py└── data/ ├── products.csv ├── suppliers.csv └── supply_options.csvHow it works
1. Define the ontology and load data
The model defines three concepts — Supplier, Product, and SupplyOption — and loads them from CSV files:
Supplier = Concept("Supplier", identify_by={"id": Integer})Supplier.name = Property(f"{Supplier} has {String:name}")Supplier.reliability = Property(f"{Supplier} has {Float:reliability}")Supplier.capacity = Property(f"{Supplier} has {Integer:capacity}")supplier_csv = read_csv(DATA_DIR / "suppliers.csv")model.define(Supplier.new(model.data(supplier_csv).to_schema()))SupplyOption links suppliers to products with a cost per unit, establishing the many-to-many relationship:
SupplyOption = Concept("SupplyOption", identify_by={"id": Integer})SupplyOption.supplier = Property(f"{SupplyOption} from {Supplier}", short_name="supplier")SupplyOption.product = Property(f"{SupplyOption} for {Product}", short_name="product")SupplyOption.cost_per_unit = Property(f"{SupplyOption} has {Float:cost_per_unit}")2. Create decision variables
A SupplyOrder concept holds the decision variable — the quantity to order through each supply option:
SupplyOrder = Concept("SupplyOrder")SupplyOrder.option = Property(f"{SupplyOrder} uses {SupplyOption}", short_name="option")SupplyOrder.x_quantity = Property(f"{SupplyOrder} has {Float:quantity}")model.define(SupplyOrder.new(option=SupplyOption))3. Add constraints and objective
Capacity and demand constraints ensure feasibility, while the objective minimizes total procurement cost. Each constraint is captured as a handle, named per entity (a readable label), and declared with keyed_by — the entity key its marginal reads back through after the solve:
cap = baseline.satisfy( model.require( sum(SupplyOrder.x_quantity).where(SupplyOrder.supplier == Supplier).per(Supplier) <= Supplier.capacity ), name=["cap", Supplier.name], keyed_by={"supplier": Supplier},)meet = baseline.satisfy( model.require( sum(SupplyOrder.x_quantity).where(SupplyOrder.product == Product).per(Product) >= Product.demand ), name=["demand", Product.name], keyed_by={"product": Product},)baseline.minimize(sum(SupplyOrder.x_quantity * SupplyOrder.cost_per_unit))4. Request sensitivity and read the marginals
Solve the baseline with sensitivity=True, then read each marginal straight off the variable or constraint object — the same attribute style as .name. A constraint declared with keyed_by carries an entity back-pointer (cap.supplier, meet.product), mirroring the variable’s automatic back-pointer (qty_var.supplyorder), so a marginal joins to that entity’s own data by KEY — no name parsing, no pandas:
baseline.solve("highs", time_limit_sec=60, sensitivity=True)
# Capacity shadow prices, joined to each supplier's capacity by key:model.select(cap.supplier.name, cap.supplier.capacity, cap.shadow_price).inspect()# Demand shadow prices, joined to each product's demand by key:model.select(meet.product.name, meet.product.demand, meet.shadow_price).inspect()# Lane reduced costs and basis status, joined to supplier / product by key:model.select( qty_var.supplyorder.supplier.name, qty_var.supplyorder.product.name, qty_var.reduced_cost, qty_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.)
The economics are also stated as integrity constraints joined by the same keys — but only the always-true directions of complementary slackness (a lane in use prices at ~0; SupplierA’s lanes are priced out). The converse “every unused lane has a positive reduced cost” is not asserted, because SupplierB’s lanes tie SupplierC at the margin (alternate optima).
5. Scenario analysis
Each disruption scenario is a separate Problem that excludes one supplier with a where= filter on the decision variable — a finite, structural change the marginals contextualize but do not by themselves predict:
for excluded in ["SupplierC", "SupplierB"]: problem = Problem(model, Float) qty_scn = problem.solve_for( SupplyOrder.x_quantity, name=["qty", SupplyOrder.supplier.name, SupplyOrder.product.name], lower=0, where=[SupplyOrder.supplier.name != excluded], populate=False, ) # ... re-add capacity / demand constraints and the objective ... problem.solve("highs", time_limit_sec=60)Customize this template
- Add a reliability penalty to the objective function, weighting cost against supplier reliability scores. One weighting yields a single trade-off point; sweep the weight to trace the cost-vs-reliability frontier.
- Expand the scenario analysis to exclude combinations of suppliers or simulate capacity reductions.
- Add minimum order quantities by setting lower bounds on the decision variables for active supply options.
- Introduce transportation costs by adding a distance or shipping cost dimension to supply options.
Troubleshooting
Solver returns INFEASIBLE for a scenario
This means total remaining supplier capacity cannot meet product demand after excluding a supplier. Check that the remaining suppliers have enough combined capacity by reviewing suppliers.csv and products.csv. You may need to relax demand constraints or add alternative suppliers.
ModuleNotFoundError: No module named 'relationalai'
Make sure you activated the virtual environment and ran python -m pip install . to install all dependencies listed in pyproject.toml.
Connection or authentication errors
Run rai init to configure your Snowflake connection. Verify that your account has the RAI Native App installed and that your user has the required permissions.
Unexpected zero quantities in the solution
The solver minimizes cost, so it will avoid expensive supply options when cheaper alternatives exist. Check supply_options.csv to see if the cost differences explain the allocation. If you want to enforce minimum diversification, add constraints requiring orders from multiple suppliers.