Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ repos:
name: Verify version README indexes
entry: python scripts/generate_version_indexes.py --check
language: python
pass_filenames: false
additional_dependencies:
- PyYAML==6.0.2
files: ^(v\d.*/.*/README\.md|scripts/generate_version_indexes\.py)$
1 change: 0 additions & 1 deletion scripts/generate_version_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import yaml


VERSION_DIR_RE = re.compile(r"^v\d")
FRONT_MATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", re.DOTALL)

Expand Down
3 changes: 3 additions & 0 deletions v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This directory contains the templates for v1. Each template folder includes its
| Template | Description |
| --- | --- |
| [ad_spend_allocation](./ad_spend_allocation/) | Allocate marketing budget across channels and campaigns to maximize conversions. |
| [bom-reachability](./bom-reachability/) | Trace transitive dependencies through a bill of materials to identify which raw materials each finished product depends on and which components are structural bottlenecks. |
| [demand_planning_temporal](./demand_planning_temporal/) | Plan weekly production and inventory across sites over a date-filtered planning horizon to minimize total cost while meeting demand. |
| [diet](./diet/) | Select foods to satisfy nutritional requirements at minimum cost. |
| [disease-outbreak-prevention](./disease-outbreak-prevention/) | Use weighted degree centrality to identify the highest-risk healthcare facilities in a public health network, considering both connection volume and intensity, to prioritize resource deployment during disease outbreaks. |
Expand All @@ -24,7 +25,9 @@ This directory contains the templates for v1. Each template folder includes its
| [retail_markdown](./retail_markdown/) | Set discount levels across weeks to maximize revenue while clearing inventory. |
| [shift_assignment](./shift_assignment/) | Assign workers to shifts based on availability to meet coverage requirements. |
| [simple-start](./simple-start/) | A minimal notebook to connect to Snowflake, model a small graph, and compute betweenness centrality with RelationalAI. |
| [site-centrality-network](./site-centrality-network/) | Identify the most critical sites in a supply chain network using weakly connected components, bridge detection, and eigenvector centrality to assess resilience and detect single points of failure. |
| [sprint_scheduling](./sprint_scheduling/) | Assign backlog issues to developers across sprints, minimizing weighted completion time while respecting capacity and skill constraints. |
| [supplier-impact-analysis](./supplier-impact-analysis/) | Trace multi-hop supply chain dependencies to identify which suppliers high-value customers depend on and assess the blast radius of a supplier disruption, including affected customers and products at risk. |
| [supplier_reliability](./supplier_reliability/) | Select suppliers to meet product demand while balancing cost and reliability. |
| [supply_chain_transport](./supply_chain_transport/) | Minimize inventory holding and transport costs with TL/LTL mode selection. |
| [test_data_generation](./test_data_generation/) | Determine optimal row counts for test database tables satisfying schema and referential integrity constraints. |
Expand Down
173 changes: 173 additions & 0 deletions v1/bom-reachability/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
title: "BOM Reachability"
description: "Trace transitive dependencies through a bill of materials to identify which raw materials each finished product depends on and which components are structural bottlenecks."
experience_level: intermediate
industry: Manufacturing
featured: false
reasoning_types:
- Graph
tags:
- graph-analytics
- reachability
- betweenness-centrality
- bill-of-materials
- manufacturing
sidebar:
order: 5
---

## What this template is for

A bill of materials (BOM) defines how finished products are built from components and raw materials through multiple assembly stages. Understanding the full transitive dependency tree -- not just direct inputs -- is critical for supply chain risk management. This template demonstrates two graph analysis techniques on a BOM structure:

1. **Reachability** (`reachable(full=True)`) -- Trace all transitive dependencies to answer "What does Product X ultimately depend on?" across multiple assembly tiers.
2. **Betweenness Centrality** -- Identify structural bottleneck components that sit on the most dependency paths between finished goods and raw materials.

## Who this is for

- **Intermediate users** who want to learn reachability analysis on directed graphs
- **Supply chain analysts** assessing multi-tier dependency exposure
- **Manufacturing engineers** identifying single-source risks in their BOM

## What you'll build

- Load a 9-SKU, 14-BOM-entry product structure from CSV (consumer electronics: smartphones, tablets, components, raw materials)
- Construct a directed dependency graph where edges point from output SKU to input SKU ("depends on")
- Compute all-pairs reachability to map full transitive dependency trees
- List dependencies per finished good, broken down by type (COMPONENT vs RAW_MATERIAL)
- Rank components by how many other SKUs depend on them
- Compute betweenness centrality to identify structural bottlenecks

## What's included

- **Self-contained script**: `bom_reachability.py` -- Runs the full analysis end-to-end
- **Data**: `data/skus.csv` (9 SKUs across 3 tiers) and `data/bill_of_materials.csv` (14 BOM entries with site-specific assembly)

## Prerequisites

- Python >= 3.10
- A Snowflake account that has the RAI Native App installed.
- A Snowflake user with permissions to access the RAI Native App.

## Quickstart

1. Download and extract this template:

```bash
curl -O https://docs.relational.ai/templates/zips/v1/bom-reachability.zip
unzip bom-reachability.zip
cd bom-reachability
```

> [!TIP]
> You can also download the template ZIP using the "Download ZIP" button at the top of this page.

2. **Create and activate a virtual environment**

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
```

3. **Install dependencies**

```bash
python -m pip install .
```

4. **Configure Snowflake connection and RAI profile**

```bash
rai init
```

5. **Run the template**

```bash
python bom_reachability.py
```

## How it works

```text
CSV files --> Define SKU + BOM concepts --> Build directed graph --> Reachability analysis --> Betweenness centrality --> Display results
```

### 1. Load Ontology

SKU and BillOfMaterials concepts are loaded from CSV. Each BOM entry links an output SKU (what is produced) to an input SKU (what is required):

```python
SKU = model.Concept("SKU", identify_by={"id": String})
BillOfMaterials = model.Concept("BillOfMaterials", identify_by={"id": String})
BillOfMaterials.output_sku = model.Relationship(f"{BillOfMaterials} produces {SKU}")
BillOfMaterials.input_sku = model.Relationship(f"{BillOfMaterials} requires {SKU}")
```

### 2. Build Directed Graph

The graph uses BillOfMaterials as the edge concept, with edges pointing from output to input ("depends on"):

```python
graph = Graph(
model, directed=True, weighted=False,
node_concept=SKU,
edge_concept=BillOfMaterials,
edge_src_relationship=BillOfMaterials.output_sku,
edge_dst_relationship=BillOfMaterials.input_sku,
)
```

### 3. Trace Dependencies

`reachable(full=True)` computes all-pairs reachability -- every (source, destination) pair where a directed path exists:

```python
reachable = graph.reachable(full=True)

src, dst = graph.Node.ref("src"), graph.Node.ref("dst")
all_deps_df = where(reachable(src, dst)).select(
src.id.alias("product_id"),
dst.id.alias("dep_id"),
...
).to_df()
```

### 4. Identify Bottlenecks

Betweenness centrality ranks components by how many shortest dependency paths pass through them:

```python
betweenness = graph.betweenness_centrality()
```

Components with high betweenness are structural bottlenecks -- disrupting them affects the most product lines.

## Customize this template

**Use your own data:**
- Replace CSVs in `data/` with your own SKU and BOM data, keeping the same column names.
- The BOM data can include site-specific assembly (SITE_ID column) for multi-site manufacturing.

**Extend the analysis:**
- Use `reachable(from_=target)` to trace downstream impact of a specific component disruption
- Add cost or lead time properties to quantify dependency exposure
- Combine with supplier data to map component-to-supplier risk chains

## Troubleshooting

<details>
<summary>Why do I see a "multi-edges" warning?</summary>

- The BOM data includes site-specific entries (e.g., SKU001 is assembled at both S001 and S012). This creates duplicate edges between the same SKU pair. The warning is informational -- the graph deduplicates automatically. To suppress it, add `aggregator="sum"` to the Graph constructor.

</details>

<details>
<summary>Why does authentication/configuration fail?</summary>

- Run `rai init` to create/update `raiconfig.toml`.
- If you have multiple profiles, set `RAI_PROFILE` or switch profiles in your config.

</details>
160 changes: 160 additions & 0 deletions v1/bom-reachability/bom_reachability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""BOM Reachability (graph analysis) template.

Answers:
"What components does Product X transitively depend on?"
"Which components are bottleneck dependencies across products?"

Data: supply chain (SKUs + BillOfMaterials).
Graph: directed, unweighted. SKU nodes, BOM rows as edges (output -> input).
Edge direction: output_sku -> input_sku ("depends on").
Algorithms: reachable(full=True) for transitive dependency tracing,
betweenness_centrality() for identifying structural bottlenecks.

Run:
`python bom_reachability.py`
"""

from pathlib import Path

from pandas import read_csv
from relationalai.semantics import Float, Model, String, where
from relationalai.semantics.reasoners.graph import Graph

model = Model("bom_reachability")

# --------------------------------------------------
# Load data & define semantic model
# --------------------------------------------------

data_dir = Path(__file__).parent / "data"

# SKU concept
SKU = model.Concept("SKU", identify_by={"id": String})
SKU.name = model.Property(f"{SKU} has {String:name}")
SKU.type = model.Property(f"{SKU} has type {String:type}")
SKU.category = model.Property(f"{SKU} in {String:category}")

sku_data = model.data(read_csv(data_dir / "skus.csv"))
model.define(SKU.new(id=sku_data["ID"]))
where(SKU.id == sku_data["ID"]).define(
SKU.name(sku_data["NAME"]),
SKU.type(sku_data["TYPE"]),
SKU.category(sku_data["CATEGORY"]),
)

# BillOfMaterials concept
BillOfMaterials = model.Concept("BillOfMaterials", identify_by={"id": String})
BillOfMaterials.output_sku = model.Relationship(f"{BillOfMaterials} produces {SKU}")
BillOfMaterials.input_sku = model.Relationship(f"{BillOfMaterials} requires {SKU}")

bom_data = model.data(read_csv(data_dir / "bill_of_materials.csv"))
model.define(BillOfMaterials.new(id=bom_data["ID"]))
where(BillOfMaterials.id == bom_data["ID"]).define(
BillOfMaterials.output_sku(SKU.filter_by(id=bom_data["OUTPUT_SKU_ID"])),
BillOfMaterials.input_sku(SKU.filter_by(id=bom_data["INPUT_SKU_ID"])),
)

# --------------------------------------------------
# Build graph: SKU nodes, BOM edges (directed)
# Edge: output_sku -> input_sku ("depends on")
# --------------------------------------------------

graph = Graph(
model,
directed=True,
weighted=False,
node_concept=SKU,
edge_concept=BillOfMaterials,
edge_src_relationship=BillOfMaterials.output_sku,
edge_dst_relationship=BillOfMaterials.input_sku,
)

print("=== BOM Dependency Graph ===")
graph.num_nodes().inspect()
graph.num_edges().inspect()

# --------------------------------------------------
# Reachability: all transitive dependency pairs
# --------------------------------------------------

reachable = graph.reachable(full=True)

src, dst = graph.Node.ref("src"), graph.Node.ref("dst")
all_deps_df = (
where(reachable(src, dst))
.select(
src.id.alias("product_id"),
src.name.alias("product_name"),
src.type.alias("product_type"),
dst.id.alias("dep_id"),
dst.name.alias("dep_name"),
dst.type.alias("dep_type"),
)
.to_df()
)

print(f"\nTotal reachable pairs: {len(all_deps_df)}")

# --------------------------------------------------
# Dependencies of each finished good
# --------------------------------------------------

finished = all_deps_df[all_deps_df["product_type"] == "FINISHED_GOOD"]
for product_id in sorted(finished["product_id"].unique()):
deps = finished[(finished["product_id"] == product_id) & (finished["dep_id"] != product_id)]
if len(deps) > 0:
product_name = deps["product_name"].iloc[0]
print(f"\n--- Dependencies of '{product_name}' ({product_id}) ---")
print(f" {len(deps)} transitive dependencies:")
for dep_type in ["COMPONENT", "RAW_MATERIAL"]:
type_deps = deps[deps["dep_type"] == dep_type]
if len(type_deps) > 0:
print(f"\n {dep_type}s ({len(type_deps)}):")
for _, row in type_deps.sort_values("dep_name").iterrows():
print(f" - {row['dep_name']} ({row['dep_id']})")

# --------------------------------------------------
# Most depended-on components
# --------------------------------------------------

non_self = all_deps_df[all_deps_df["product_id"] != all_deps_df["dep_id"]]
if len(non_self) > 0:
dep_counts = (
non_self.groupby(["dep_id", "dep_name", "dep_type"])
.size()
.reset_index(name="depended_on_by")
.sort_values("depended_on_by", ascending=False)
)
print("\n=== Most Depended-On SKUs ===")
print(dep_counts.to_string(index=False))

# --------------------------------------------------
# Betweenness centrality: structural bottlenecks
# --------------------------------------------------

betweenness = graph.betweenness_centrality()

node = graph.Node.ref("n")
score = Float.ref("s")
btw_df = (
where(betweenness(node, score))
.select(
node.id.alias("sku_id"),
node.name.alias("sku_name"),
node.type.alias("type"),
node.category.alias("category"),
score.alias("betweenness"),
)
.to_df()
.sort_values("betweenness", ascending=False)
.reset_index(drop=True)
)

print("\n=== Betweenness Centrality (bottleneck components) ===")
print(btw_df.to_string(index=False))

bottlenecks = btw_df[btw_df["betweenness"] > 0]
if len(bottlenecks) > 0:
top = bottlenecks.iloc[0]
print(f"\nTop bottleneck: {top['sku_name']} (betweenness={top['betweenness']:.4f})")
print(" Sits on the most dependency paths -- disruption here affects the most product lines.")
15 changes: 15 additions & 0 deletions v1/bom-reachability/data/bill_of_materials.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ID,SITE_ID,OUTPUT_SKU_ID,INPUT_SKU_ID,INPUT_QUANTITY
BOM007,S001,SKU001,SKU006,3
BOM008,S001,SKU001,SKU009,1
BOM009,S012,SKU001,SKU006,3
BOM010,S012,SKU001,SKU009,1
BOM011,S002,SKU002,SKU007,1
BOM012,S013,SKU002,SKU007,1
BOM013,S003,SKU003,SKU008,6
BOM014,S014,SKU003,SKU008,6
BOM001,S004,SKU004,SKU001,1
BOM002,S004,SKU004,SKU002,1
BOM003,S004,SKU004,SKU003,1
BOM004,S005,SKU005,SKU001,1
BOM005,S005,SKU005,SKU002,1
BOM006,S005,SKU005,SKU003,2
10 changes: 10 additions & 0 deletions v1/bom-reachability/data/skus.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ID,NAME,TYPE,CATEGORY,UNIT_OF_MEASURE,LEAD_TIME_DAYS,UNIT_COST,UNIT_PRICE
SKU006,Silicon Wafer 300mm,RAW_MATERIAL,SEMICONDUCTOR,UNIT,21,12.0,18.0
SKU007,Display Glass Panel 6.5in,RAW_MATERIAL,GLASS,UNIT,14,22.0,32.0
SKU008,Lithium Ion Cells 18650,RAW_MATERIAL,CHEMICAL,UNIT,10,2.5,4.0
SKU009,NAND Flash Memory Die,RAW_MATERIAL,SEMICONDUCTOR,UNIT,18,8.0,12.0
SKU001,Mobile Processor A15,COMPONENT,PROCESSOR,UNIT,14,45.0,75.0
SKU002,OLED Display Panel 6.1,COMPONENT,DISPLAY,UNIT,10,65.0,95.0
SKU003,Lithium Battery Pack 4500mAh,COMPONENT,BATTERY,UNIT,7,18.0,28.0
SKU004,ProPhone X1,FINISHED_GOOD,SMARTPHONE,UNIT,5,180.0,799.0
SKU005,ProTab T1,FINISHED_GOOD,TABLET,UNIT,5,220.0,599.0
Loading
Loading