Inventory Rebalancing
Transfer inventory between warehouse sites to meet demand at minimum cost.
Transfer inventory between warehouse sites to meet demand at minimum cost.
Transfer inventory through a warehouse-hub-store network to meet demand at minimum shipping cost, with flow conservation at transit nodes.
Browse files
Browse files
Browse files
What this template is for
Inventory is often in the wrong place: one warehouse has excess stock while another location is at risk of stockouts. This template models a small inventory transfer problem where you move units from source sites to destination sites through a set of transfer lanes.
The goal is to meet demand at each destination site while respecting lane capacities and available source inventory, and doing so at minimum total transfer cost. It’s an end-to-end example of prescriptive reasoning (optimization) with RelationalAI Semantics.
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 decision variables, constraints, and objectives.
What you’ll build
- A semantic model for
Site,Lane, andDemandloaded from CSV. - A linear program (LP) with one non-negative transfer decision variable per lane.
- Constraints for lane capacity, source inventory limits, and demand satisfaction.
- A cost-minimizing objective and a solve step using the HiGHS backend.
What’s included
inventory_rebalancing.py— defines the semantic model, optimization problem, and prints a solutiondata/— sample CSV inputs (sites.csv,lanes.csv,demand.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 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/inventory_rebalancing.zipunzip inventory_rebalancing.zipcd inventory_rebalancing -
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 inventory_rebalancing.py -
Expected output
Status: OPTIMALTotal transfer cost: $1500.00Transfers:from to quantityWarehouse_B Store_1 150.0Warehouse_A Store_1 50.0Warehouse_C Store_2 100.0Warehouse_B Store_2 70.0
Template structure
.├─ README.md├─ pyproject.toml├─ inventory_rebalancing.py # main runner / entrypoint└─ data/ # sample input data ├─ demand.csv ├─ lanes.csv └─ sites.csvStart here: inventory_rebalancing.py
Sample data
Data files are in data/.
sites.csv
Each row is a site (warehouse or store) with its current on-hand inventory.
| Column | Meaning |
|---|---|
id | Unique site identifier |
name | Site name (used for readable output) |
inventory | Current inventory available at the site |
lanes.csv
Each row is a directed transfer lane from a source site to a destination site.
| Column | Meaning |
|---|---|
id | Unique lane identifier |
source_id | Source site ID (foreign key to sites.csv.id) |
dest_id | Destination site ID (foreign key to sites.csv.id) |
cost_per_unit | Cost to transfer one unit on this lane |
capacity | Maximum transferable units on this lane |
demand.csv
Each row is a demand requirement at a site.
| Column | Meaning |
|---|---|
id | Unique demand record identifier |
site_id | Site ID where demand must be met (foreign key to sites.csv.id) |
quantity | Required units at the site |
Model overview
The optimization model is built around four concepts.
Site
A warehouse or store location that has current inventory.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/sites.csv |
name | string | No | Used for output labeling |
inventory | int | No | Used to limit total outbound transfers |
Lane
A directed transfer option between two sites.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/lanes.csv |
source | Site | No | Resolved from data/lanes.csv.source_id |
dest | Site | No | Resolved from data/lanes.csv.dest_id |
cost_per_unit | float | No | Cost coefficient in the objective |
capacity | int | No | Upper bound for each lane’s transfer |
Demand
A demand requirement at a site.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/demand.csv |
site | Site | No | Site where demand must be met |
quantity | int | No | Required units |
Transfer (decision concept)
One transfer decision is created for each Lane. The solver chooses a non-negative quantity.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
lane | Lane | Yes | One decision per lane |
quantity | float | No | Continuous decision variable ( |
How it works
This section walks through the highlights in inventory_rebalancing.py.
Import libraries and configure inputs
This template uses Concept objects from the relationalai.semantics module to model sites, lanes, and demands, and uses the Solver and SolverModel classes from relationalai.semantics.reasoners.optimization to define and solve the optimization problem:
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
# --------------------------------------------------# 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 = FalseDefine concepts and load CSV data
First, declare a Site concept and load one entity per row in sites.csv using data(...).into(...).
model = Model("inventory_rebalancing", config=globals().get("config", None), use_lqp=False)
# Concept: sites with current inventorySite = model.Concept("Site")Site.id = model.Property("{Site} has {id:int}")Site.name = model.Property("{Site} has {name:string}")Site.inventory = model.Property("{Site} has {inventory:int}")
# Load site data from CSV and populate the Site concept.data(read_csv(DATA_DIR / "sites.csv")).into(Site, keys=["id"])Next, declare a Lane concept for directed transfer options and use a where(...) join to resolve source_id and dest_id into Site entity references.
# Relationship: lanes between sites with cost and capacityLane = model.Concept("Lane")Lane.id = model.Property("{Lane} has {id:int}")Lane.source = model.Property("{Lane} from {source:Site}")Lane.dest = model.Property("{Lane} to {dest:Site}")Lane.cost_per_unit = model.Property("{Lane} has {cost_per_unit:float}")Lane.capacity = model.Property("{Lane} has {capacity:int}")
# Load lane data from CSV and create Lane entities.lanes_data = data(read_csv(DATA_DIR / "lanes.csv"))Dest = Site.ref()where( Site.id == lanes_data.source_id, Dest.id == lanes_data.dest_id).define( Lane.new( id=lanes_data.id, source=Site, dest=Dest, cost_per_unit=lanes_data.cost_per_unit, capacity=lanes_data.capacity ))Finally, declare a Demand concept and join demand.csv.site_id to the matching Site entity.
# Concept: demand at each siteDemand = model.Concept("Demand")Demand.id = model.Property("{Demand} has {id:int}")Demand.site = model.Property("{Demand} at {site:Site}")Demand.quantity = model.Property("{Demand} has {quantity:int}")
# Load demand data from CSV and create Demand entities.demand_data = data(read_csv(DATA_DIR / "demand.csv"))where(Site.id == demand_data.site_id).define( Demand.new(id=demand_data.id, site=Site, quantity=demand_data.quantity))Define decision variables, constraints, and objective
Create a Transfer decision concept and register one continuous, non-negative decision variable per lane.
# Decision concept: transfers on each laneTransfer = model.Concept("Transfer")Transfer.lane = model.Property("{Transfer} uses {lane:Lane}")Transfer.x_quantity = model.Property("{Transfer} has {quantity:float}")define(Transfer.new(lane=Lane))
Tr = Transfer.ref()Dm = Demand.ref()
s = SolverModel(model, "cont")
# Variable: transfer quantitys.solve_for(Transfer.x_quantity, name=["qty", Transfer.lane.source.name, Transfer.lane.dest.name], lower=0)Then add constraints to enforce lane capacities, limit total outbound shipments by source inventory, and meet destination demand (allowing local inventory to contribute):
# Constraint: transfer cannot exceed lane capacitycapacity_limit = require(Transfer.x_quantity <= Transfer.lane.capacity)s.satisfy(capacity_limit)
# Constraint: total outbound from source cannot exceed source inventoryoutbound = sum(Tr.quantity).where(Tr.lane.source == Site).per(Site)inventory_limit = require(outbound <= Site.inventory)s.satisfy(inventory_limit)
# Constraint: demand satisfaction at each destination siteinbound = sum(Tr.quantity).where(Tr.lane.dest == Dm.site).per(Dm)local_inv = sum(Site.inventory).where(Site == Dm.site).per(Dm)demand_met = require(inbound + local_inv >= Dm.quantity)s.satisfy(demand_met)With the feasible region defined, minimize the total transfer cost (quantity times per-unit lane cost):
# Objective: minimize total transfer costtotal_cost = sum(Transfer.x_quantity * Transfer.lane.cost_per_unit)s.minimize(total_cost)Solve and print results
After defining variables, constraints, and the objective, run the HiGHS solver and print only transfers with a non-trivial quantity:
solver = Solver("highs")s.solve(solver, time_limit_sec=60)
print(f"Status: {s.termination_status}")print(f"Total transfer cost: ${s.objective_value:.2f}")
transfers = select( Transfer.lane.source.name.alias("from"), Transfer.lane.dest.name.alias("to"), Transfer.x_quantity).where(Transfer.x_quantity > 0.001).to_df()
print("\nTransfers:")print(transfers.to_string(index=False))Customize this template
Use your own data
- Replace the CSVs in
data/with your own data, keeping the same column names (or update the load logic ininventory_rebalancing.py). - Make sure
lanes.csv.source_idandlanes.csv.dest_idonly reference valid site IDs insites.csv.id. - Make sure
demand.csv.site_idonly references valid site IDs.
Tune parameters
-
Change the solver time limit in:
s.solve(solver, time_limit_sec=60) -
Swap the solver backend if your environment supports a different one.
Extend the model
- Add per-lane fixed costs (requires binary “use lane” variables).
- Add service-level constraints (e.g., require minimum shipments into a subset of sites).
- Enforce integer transfer quantities by switching to a mixed-integer model and using integer decision variables.
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 do I get Status: INFEASIBLE?
- Check that each demand site has enough supply available across inbound lanes (considering lane capacities).
- If you’re relying on transfers only, ensure total supply across all sources is sufficient.
- Confirm that site IDs referenced in
lanes.csvanddemand.csvexist insites.csv.
Why is the Transfers table empty?
- The script filters transfers with
Transfer.x_quantity > 0.001. If the optimal solution uses only local inventory (no transfers needed), the table will be empty. - Confirm
demand.csv.quantityexceeds local inventory at some site if you expect transfers.
Why does the script fail when reading CSVs?
- Confirm the CSV headers match the expected column names.
- Check for non-numeric values in numeric columns like
inventory,capacity, andquantity. - Ensure the files are saved as UTF-8 and are comma-delimited.
What this template is for
Inventory is often in the wrong place: one warehouse has excess stock while another location is at risk of stockouts. This template models a small inventory transfer problem where you move units from source sites to destination sites through a set of transfer lanes.
The goal is to meet demand at each destination site while respecting lane capacities and available source inventory, and doing so at minimum total transfer cost. It’s an end-to-end example of prescriptive reasoning (optimization) with RelationalAI Semantics.
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 decision variables, constraints, and objectives.
What you’ll build
- A semantic model for
Site,Lane, andDemandloaded from CSV. - A linear program (LP) with one non-negative transfer decision variable per lane.
- Constraints for lane capacity, source inventory limits, and demand satisfaction.
- A cost-minimizing objective and a solve step using the HiGHS backend.
What’s included
inventory_rebalancing.py— defines the semantic model, optimization problem, and prints a solutiondata/— sample CSV inputs (sites.csv,lanes.csv,demand.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 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/inventory_rebalancing.zipunzip inventory_rebalancing.zipcd inventory_rebalancing -
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 inventory_rebalancing.py -
Expected output
Status: OPTIMALTotal transfer cost: $1500.00Transfers:from to quantityWarehouse_B Store_1 150.0Warehouse_A Store_1 50.0Warehouse_C Store_2 100.0Warehouse_B Store_2 70.0
Template structure
.├─ README.md├─ pyproject.toml├─ inventory_rebalancing.py # main runner / entrypoint└─ data/ # sample input data ├─ demand.csv ├─ lanes.csv └─ sites.csvStart here: inventory_rebalancing.py
Sample data
Data files are in data/.
sites.csv
Each row is a site (warehouse or store) with its current on-hand inventory.
| Column | Meaning |
|---|---|
id | Unique site identifier |
name | Site name (used for readable output) |
inventory | Current inventory available at the site |
lanes.csv
Each row is a directed transfer lane from a source site to a destination site.
| Column | Meaning |
|---|---|
id | Unique lane identifier |
source_id | Source site ID (foreign key to sites.csv.id) |
dest_id | Destination site ID (foreign key to sites.csv.id) |
cost_per_unit | Cost to transfer one unit on this lane |
capacity | Maximum transferable units on this lane |
demand.csv
Each row is a demand requirement at a site.
| Column | Meaning |
|---|---|
id | Unique demand record identifier |
site_id | Site ID where demand must be met (foreign key to sites.csv.id) |
quantity | Required units at the site |
Model overview
The optimization model is built around four concepts.
Site
A warehouse or store location that has current inventory.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/sites.csv |
name | string | No | Used for output labeling |
inventory | int | No | Used to limit total outbound transfers |
Lane
A directed transfer option between two sites.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/lanes.csv |
source | Site | No | Resolved from data/lanes.csv.source_id |
dest | Site | No | Resolved from data/lanes.csv.dest_id |
cost_per_unit | float | No | Cost coefficient in the objective |
capacity | int | No | Upper bound for each lane’s transfer |
Demand
A demand requirement at a site.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
id | int | Yes | Primary key loaded from data/demand.csv |
site | Site | No | Site where demand must be met |
quantity | int | No | Required units |
Transfer (decision concept)
One transfer decision is created for each Lane. The solver chooses a non-negative quantity.
| Property | Type | Identifying? | Notes |
|---|---|---|---|
lane | Lane | Yes | One decision per lane |
quantity | float | No | Continuous decision variable ( |
How it works
This section walks through the highlights in inventory_rebalancing.py.
Import libraries and configure inputs
This template uses Concept objects from the relationalai.semantics module to model sites, lanes, and demands, and uses the Solver and SolverModel classes from relationalai.semantics.reasoners.optimization to define and solve the optimization problem:
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
# --------------------------------------------------# 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 = FalseDefine concepts and load CSV data
First, declare a Site concept and load one entity per row in sites.csv using data(...).into(...).
model = Model("inventory_rebalancing", config=globals().get("config", None))
# Concept: sites with current inventorySite = model.Concept("Site")Site.id = model.Property("{Site} has {id:int}")Site.name = model.Property("{Site} has {name:string}")Site.inventory = model.Property("{Site} has {inventory:int}")
# Load site data from CSV and populate the Site concept.data(read_csv(DATA_DIR / "sites.csv")).into(Site, keys=["id"])Next, declare a Lane concept for directed transfer options and use a where(...) join to resolve source_id and dest_id into Site entity references.
# Relationship: lanes between sites with cost and capacityLane = model.Concept("Lane")Lane.id = model.Property("{Lane} has {id:int}")Lane.source = model.Relationship("{Lane} from {source:Site}")Lane.dest = model.Relationship("{Lane} to {dest:Site}")Lane.cost_per_unit = model.Property("{Lane} has {cost_per_unit:float}")Lane.capacity = model.Property("{Lane} has {capacity:int}")
# Load lane data from CSV and create Lane entities.lanes_data = data(read_csv(DATA_DIR / "lanes.csv"))DestSite = Site.ref()where( Site.id == lanes_data.source_id, DestSite.id == lanes_data.dest_id).define( Lane.new( id=lanes_data.id, source=Site, dest=DestSite, cost_per_unit=lanes_data.cost_per_unit, capacity=lanes_data.capacity ))Finally, declare a Demand concept and join demand.csv.site_id to the matching Site entity.
# Concept: demand at each siteDemand = model.Concept("Demand")Demand.id = model.Property("{Demand} has {id:int}")Demand.site = model.Relationship("{Demand} at {site:Site}")Demand.quantity = model.Property("{Demand} has {quantity:int}")
# Load demand data from CSV and create Demand entities.demand_data = data(read_csv(DATA_DIR / "demand.csv"))where(Site.id == demand_data.site_id).define( Demand.new(id=demand_data.id, site=Site, quantity=demand_data.quantity))Define decision variables, constraints, and objective
Create a Transfer decision concept and register one continuous, non-negative decision variable per lane.
# Decision concept: transfers on each laneTransfer = model.Concept("Transfer")Transfer.lane = model.Relationship("{Transfer} uses {lane:Lane}")Transfer.x_quantity = model.Property("{Transfer} has {quantity:float}")define(Transfer.new(lane=Lane))
TransferRef = Transfer.ref()DemandRef = Demand.ref()
s = SolverModel(model, "cont")
# Variable: transfer quantitys.solve_for(Transfer.x_quantity, name=["qty", Transfer.lane.source.name, Transfer.lane.dest.name], lower=0)Then add constraints to enforce lane capacities, limit total outbound shipments by source inventory, and meet destination demand (allowing local inventory to contribute):
# Constraint: transfer cannot exceed lane capacitycapacity_limit = require(Transfer.x_quantity <= Transfer.lane.capacity)s.satisfy(capacity_limit)
# Constraint: total outbound from source cannot exceed source inventoryoutbound = sum(TransferRef.x_quantity).where(TransferRef.lane.source == Site).per(Site)inventory_limit = require(outbound <= Site.inventory)s.satisfy(inventory_limit)
# Constraint: demand satisfaction at each destination siteinbound = sum(TransferRef.x_quantity).where(TransferRef.lane.dest == DemandRef.site).per(DemandRef)local_inv = sum(Site.inventory).where(Site == DemandRef.site).per(DemandRef)demand_met = require(inbound + local_inv >= DemandRef.quantity)s.satisfy(demand_met)With the feasible region defined, minimize the total transfer cost (quantity times per-unit lane cost):
# Objective: minimize total transfer costtotal_cost = sum(Transfer.x_quantity * Transfer.lane.cost_per_unit)s.minimize(total_cost)Solve and print results
After defining variables, constraints, and the objective, run the HiGHS solver and print only transfers with a non-trivial quantity:
solver = Solver("highs")s.solve(solver, time_limit_sec=60)
print(f"Status: {s.termination_status}")print(f"Total transfer cost: ${s.objective_value:.2f}")
transfers = select( Transfer.lane.source.name.alias("from"), Transfer.lane.dest.name.alias("to"), Transfer.x_quantity).where(Transfer.x_quantity > 0.001).to_df()
print("\nTransfers:")print(transfers.to_string(index=False))Customize this template
Use your own data
- Replace the CSVs in
data/with your own data, keeping the same column names (or update the load logic ininventory_rebalancing.py). - Make sure
lanes.csv.source_idandlanes.csv.dest_idonly reference valid site IDs insites.csv.id. - Make sure
demand.csv.site_idonly references valid site IDs.
Tune parameters
-
Change the solver time limit in:
s.solve(solver, time_limit_sec=60) -
Swap the solver backend if your environment supports a different one.
Extend the model
- Add per-lane fixed costs (requires binary “use lane” variables).
- Add service-level constraints (e.g., require minimum shipments into a subset of sites).
- Enforce integer transfer quantities by switching to a mixed-integer model and using integer decision variables.
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 do I get Status: INFEASIBLE?
- Check that each demand site has enough supply available across inbound lanes (considering lane capacities).
- If you’re relying on transfers only, ensure total supply across all sources is sufficient.
- Confirm that site IDs referenced in
lanes.csvanddemand.csvexist insites.csv.
Why is the Transfers table empty?
- The script filters transfers with
Transfer.x_quantity > 0.001. If the optimal solution uses only local inventory (no transfers needed), the table will be empty. - Confirm
demand.csv.quantityexceeds local inventory at some site if you expect transfers.
Why does the script fail when reading CSVs?
- Confirm the CSV headers match the expected column names.
- Check for non-numeric values in numeric columns like
inventory,capacity, andquantity. - Ensure the files are saved as UTF-8 and are comma-delimited.
What this template is for
Retail and distribution networks often have inventory spread unevenly across warehouses and stores. Some locations hold excess stock while others face shortages. Manually deciding which transfers to make, from where, and in what quantities quickly becomes impractical as the network grows.
This template uses prescriptive reasoning (optimization) to determine the optimal set of inventory transfers across a network of warehouses, transit hubs, and stores. It minimizes total shipping cost while ensuring every demand point receives enough stock, respecting lane capacities, available inventory at each source, and flow conservation at transit sites.
The model is a network flow formulation with three site types: warehouses (supply), transit hubs (flow-through only), and stores (demand). Flow conservation at transit sites ensures that everything flowing in must flow out — a fundamental network optimization pattern.
Who this is for
- Supply chain analysts optimizing inventory distribution across locations
- Operations teams managing warehouse-to-store replenishment
- Developers learning network flow optimization with RelationalAI
- Anyone building inventory transfer recommendation systems
What you’ll build
- A network flow model with continuous transfer quantity variables
- Lane capacity and source inventory constraints
- Flow conservation at transit hub sites (inflow == outflow)
- Demand satisfaction constraints at store sites
- A minimum-cost objective over all shipping lanes
What’s included
inventory_rebalancing.py— Main script that defines the model, solves it, and prints resultsdata/sites.csv— Warehouse, transit hub, and store locations with type and inventory levelsdata/lanes.csv— Shipping lanes between sites with per-unit costs and capacitiesdata/demand.csv— Demand quantities at destination sitespyproject.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/inventory_rebalancing.zipunzip inventory_rebalancing.zipcd inventory_rebalancing -
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 inventory_rebalancing.py -
Expected output:
Status: OPTIMALTotal transfer cost: $2730.00Transfers:Hub_East Store_1 200.0Hub_East Store_2 90.0Hub_East Store_3 10.0Hub_West Store_2 80.0Hub_West Store_3 120.0Warehouse_A Hub_East 300.0Warehouse_B Hub_West 200.0Warehouse A ships 300 units through Hub East, which distributes to all three stores. Warehouse B ships 200 units through Hub West, which covers the remaining demand at Store 2 and Store 3. Warehouse C is not used — the solver avoids it due to its higher transfer costs.
Template structure
.├── README.md├── pyproject.toml├── inventory_rebalancing.py└── data/ ├── sites.csv ├── lanes.csv └── demand.csvHow it works
1. Define the ontology and load data
The model defines three concepts: sites with inventory levels, lanes connecting sites with costs and capacities, and demand at destination sites.
Site = Concept("Site", identify_by={"id": Integer})Site.name = Property(f"{Site} has {String:name}")Site.type = Property(f"{Site} has {String:type}") # WAREHOUSE, TRANSIT, or STORESite.inventory = Property(f"{Site} has {Integer:inventory}")
Lane = Concept("Lane", identify_by={"id": Integer})Lane.source = Property(f"{Lane} from {Site}", short_name="source")Lane.dest = Property(f"{Lane} to {Site}", short_name="dest")Lane.cost_per_unit = Property(f"{Lane} has {Float:cost_per_unit}")Lane.capacity = Property(f"{Lane} has {Integer:capacity}")2. Set up decision variables
Each lane gets a continuous transfer quantity variable bounded at zero.
Transfer = Concept("Transfer", identify_by={"lane": Lane})Transfer.x_quantity = Property(f"{Transfer} has {Float:quantity}")model.define(Transfer.new(lane=Lane))
problem.solve_for(Transfer.x_quantity, name=["qty", Transfer.lane.source.name, Transfer.lane.dest.name], lower=0)3. Add constraints
Four constraint families ensure feasibility: lane capacity limits, source inventory limits, flow conservation at transit sites, and demand satisfaction at stores.
# Lane capacityproblem.satisfy(model.require(Transfer.x_quantity <= Transfer.lane.capacity))
# Source inventoryoutbound = sum(TransferRef.x_quantity).where(TransferRef.lane.source == Site).per(Site)problem.satisfy(model.require(outbound <= Site.inventory))
# Flow conservation at transit sites (inflow == outflow)inflow = sum(InRef.x_quantity).where(InRef.lane.dest == TransitSite).per(TransitSite)outflow = sum(OutRef.x_quantity).where(OutRef.lane.source == TransitSite).per(TransitSite)problem.satisfy(model.require(inflow == outflow).where(TransitSite.type("TRANSIT")))
# Demand satisfaction (inbound transfers + local inventory >= demand)inbound = sum(TransferRef.x_quantity).where(TransferRef.lane.dest == DemandRef.site).per(DemandRef)local_inv = sum(Site.inventory).where(Site == DemandRef.site).per(DemandRef)problem.satisfy(model.require(inbound + local_inv >= DemandRef.quantity))4. Minimize total cost
The objective sums shipping costs across all active transfers.
total_cost = sum(Transfer.x_quantity * Transfer.lane.cost_per_unit)problem.minimize(total_cost)Customize this template
- Add more sites and lanes by extending the CSV files to model a larger distribution network.
- Introduce multi-product inventory by adding a product dimension to sites, lanes, and demand.
- Add transfer lead times and model time-phased demand with delivery windows.
- Include holding costs at each site to balance between transfer costs and inventory carrying costs.
- Add minimum transfer quantities to model batch shipping requirements.
Troubleshooting
Status: INFEASIBLE
Check that total available inventory across all warehouses is sufficient to meet total demand at stores. Transit hubs carry no inventory — they only route flow through. Also verify that lane capacities allow enough flow between warehouses, hubs, and stores.
Unexpected transfer routes
The solver minimizes total cost, so it may route through cheaper lanes even if they seem indirect. Check the cost_per_unit values in lanes.csv to verify the cost structure matches your expectations.
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
Ensure you activated the virtual environment and ran python -m pip install . to install all dependencies listed in pyproject.toml.