The arcline is a Python framework for building, solving, and analyzing supply chain optimization problems as network flow
models. Real supply chains are graphs. Suppliers, plants, warehouses, distribution centers, and customers are nodes; the lanes
between them are arcs (or edges); and the optimization questions that matter - where to source, how much to produce, which
routes to use, when to open or close a facility - are all decisions about flow on those arcs. The project gives you a declarative
API for modeling these networks, a solver-agnostic backend (CBC, HiGHS, Gurobi, CPLEX), and first-class tooling for the parts
of the workflow that real practitioners spend most of their time on: data ingestion, scenario comparison, sensitivity analysis,
and visualization. It is designed to bridge the gap between the academic clarity of textbook formulations and the messy practical
needs of production supply chain teams. Whether you are running a one-off facility location study, building a digital twin of
a global distribution network, or embedding a recurring optimization into a daily planning pipeline, arcline aims to be the
layer that makes the network the first-class object - and the math, the I/O, and the solver plumbing fade into the background.
Status: Phase 1 (Graph + I/O + Dashboard) and Phase 1.5 (Historian + Analytics) are shipped. Phase 2 (Pyomo optimization) and Phase 3 (Scenarios & Sensitivity) are on the roadmap. See
CLAUDE.mdfor the full design plan.
- Why arcline?
- Installation
- 60-second quickstart
- Core concepts
- The CLI
- Authoring a network in Python
- Project file layout
- The Dashboard
- The Historian (MS-SQL Server)
- Examples shipped with the repo
- Troubleshooting
- Contributing
A typical supply-chain modelling stack forces you to glue together at least four ad-hoc layers: a graph data structure (NetworkX), a serialization format (CSVs / pickles), a solver harness (PuLP / Pyomo) and a visualization (matplotlib / Tableau). Each switchover is a place for entropy: silent column renames, lost types, fragile id schemes, scenarios that drift from the canonical model.
arcline treats the network itself as the first-class object. Once you have an AbstractGraph of typed nodes and edges, the
framework owns the rest: validating it, persisting it as a portable, git-friendly project folder, rendering it in an interactive
Dash dashboard, pulling per-attribute history from a MS-SQL Server data warehouse, and (in upcoming phases) compiling it down to
Pyomo for solving and scenario comparison.
| Capability | Status | Where it lives |
|---|---|---|
| Typed taxonomy (SupplierNode / PlantNode / WarehouseNode / CustomerNode / LaneEdge / ProductionEdge / StorageEdge) | ✅ shipped | arcline/graph/library/ |
| Pydantic validation of every node and edge | ✅ shipped | arcline/graph/base/ |
| Project I/O (JSON · YAML · Parquet · CSV) | ✅ shipped | arcline/io/ |
NetworkBuilder fluent assembly |
✅ shipped | arcline/graph/builder.py |
| Multi-page Dash dashboard with CRUD on nodes / edges | ✅ shipped | arcline/dashboard/ |
| Visualize page (force-directed · tiered · geo via Mapbox) | ✅ shipped | arcline/dashboard/pages/visualize.py |
MS-SQL historian with declarative HistorySpec, Parquet cache, analytics primitives |
✅ shipped | arcline/historian/ |
/dashboard/history (time-series · distribution · summary) |
✅ shipped | arcline/dashboard/pages/history.py |
| Pyomo optimization (min-cost flow, facility location, multi-period, share-of-business) | ⏳ Phase 2 | arcline/optim/ (planned) |
| Scenario workspace + sensitivity sweeps | ⏳ Phase 3 | arcline/scenarios/ (planned) |
arcline requires Python ≥ 3.12. Install in editable mode with the extras you need:
# core only (graph + I/O + CLI)
pip install -e .
# add the dashboard
pip install -e .[dashboard]
# add the MS-SQL historian
pip install -e .[historian]
# everything + dev tooling (pytest, ruff, etc.)
pip install -e .[dashboard,historian,dev]The MS-SQL historian additionally needs the Microsoft ODBC Driver 18 for SQL Server installed at the OS level. The Python side (sqlalchemy, pyodbc) comes with the [historian] extra.
# 1. generate a synthetic 4-tier supply chain (21 nodes, 29 edges, geo-located)
python examples/random_network.py --output ./demo_network
# 2. open it in the dashboard (port 8050)
arcline dashboard ./demo_networkThen visit http://127.0.0.1:8050. The Visualize page automatically switches to geo mode (Mapbox / OpenStreetMap tiles) because the generator jitters lat/lon around Bengaluru.
Customize the generator:
python examples/random_network.py \
--output ./big_network \
--suppliers 30 --plants 12 --warehouses 8 --customers 60 \
--seed 7arcline init ./my_network --name "My First Network"
arcline validate ./my_network
arcline dashboard ./my_networkarcline init creates the canonical project layout. The Nodes / Edges pages let you add records through pydantic-driven forms; click Save to persist back to disk.
| Concept | Where | Notes |
|---|---|---|
AbstractNode / AbstractEdge |
arcline/graph/base/ |
Pydantic v2 base classes; every concrete node/edge inherits validation, hashing, and serialization for free. Both expose hashKey (deterministic id), name, optional latitude / longitude. |
AbstractGraph |
arcline/graph/base/graph.py |
Storage-agnostic interface. The default backend wraps networkx.MultiDiGraph; an igraph backend is on the roadmap for >1M-edge networks. Provides addNode, addEdge, updateNode, updateEdge, cached neighbour indices, etc. |
| Built-in taxonomy | arcline/graph/library/ |
SupplierNode · PlantNode · WarehouseNode (alias DistributionCenterNode) · CustomerNode · LaneEdge · ProductionEdge · StorageEdge. Each has typed attributes (e.g. LaneEdge.distanceKm, PlantNode.maxCapacity), a kind discriminator used for polymorphic (de)serialization, and ClassVar capability flags (canShip, canManufacture, …). Categorical fields use Enums (TransportationMode.ROAD, FacilityStatus.OPEN, etc.) — see arcline/graph/enums.py. |
| Kind registry | arcline/graph/registry.py |
Maps "supplier" → SupplierNode, "lane" → LaneEdge, etc. Auto-populated when arcline.graph.library is imported. arcline.io triggers this side-effect on import so the CLI just works. |
NetworkBuilder |
arcline/graph/builder.py |
Fluent assembly: b.add(SupplierNode(...)), b.connect(src, dst, cls=LaneEdge, ...), b.build(). Catches duplicate hash keys and dangling endpoints early. |
Project |
arcline/io/project.py |
File-based project facade. Three constructors: Project.init (empty), Project.open (load + validate), Project.fromGraph (persist an in-memory graph). Use proj.toGraph() to materialise. |
HistorySpec |
arcline/historian/spec.py |
Declarative mapping from a node/edge attribute to a row-set in MS-SQL. The framework synthesises a parameterised SELECT … BETWEEN :start AND :end; classes never write SQL by hand. |
The built-in taxonomy is a small, opinionated tree. Concrete leaves (the classes you instantiate) inherit from intermediate abstract bases that hold shared fields, so adding a new node/edge kind is usually a 5-line subclass.
AbstractNode
├── SourceNode canShip=True canStore=False canManufacture=False
│ └── SupplierNode
├── FacilityNode canShip=True canStore=True canManufacture=False
│ ├── PlantNode canManufacture=True
│ └── WarehouseNode (alias: DistributionCenterNode)
└── DemandNode canShip=False canStore=False canDemand=True
└── CustomerNode
AbstractEdge
└── FlowEdge carriesProduct=True carriesInfo=False
├── TransportEdge mode, transitDays
│ └── LaneEdge
├── ProductionEdge
└── StorageEdge
Common fields are lifted up:
| Lifted to | Fields |
|---|---|
FacilityNode |
minCapacity, maxCapacity (Optional — None = unconstrained), operatingCostPerHr, status, ownership, shift (with minCapacity ≤ maxCapacity cross-field validator) |
FlowEdge |
costPerUnit, capacityPerPeriod (Optional — None = unbounded) |
TransportEdge |
mode, transitDays |
Every categorical attribute is an Enum from arcline.graph.enums:
| Enum | Members | Used by |
|---|---|---|
TransportationMode |
ROAD, RAIL, SEA, AIR |
LaneEdge.mode |
FacilityStatus |
PLANNED, OPEN, CLOSED, DECOMMISSIONED |
FacilityNode.status |
OwnershipType |
OWNED, LEASED, THIRD_PARTY |
FacilityNode.ownership |
OperationalShift |
DAY, NIGHT, TWENTY_FOUR_SEVEN |
FacilityNode.shift |
StorageType |
AMBIENT, COLD_CHAIN, FROZEN, HAZMAT |
WarehouseNode.storageType, StorageEdge.storageType |
CustomerSegment |
RETAIL, WHOLESALE, B2B, B2C |
CustomerNode.segment |
LaneServiceLevel |
STANDARD, EXPEDITED, OVERNIGHT |
LaneEdge.serviceLevel |
Currency |
USD, EUR, GBP, INR, JPY |
declared (Phase 2) |
UnitOfMeasure |
EACH, CASE, PALLET, KG, TON, LITRE |
declared (Phase 2) |
All enums subclass _CamelStrEnum(str, Enum) so they JSON-serialise as their UPPER .name and accept any case-variant on input — TransportationMode("road"), TransportationMode("ROAD") and TransportationMode.ROAD are interchangeable.
Capability flags let downstream tooling avoid isinstance() ladders:
node.supports("canManufacture") # PlantNode -> True, WarehouseNode -> False
edge.supports("carriesProduct") # any FlowEdge -> TrueThe full set of public re-exports lives at the top of the package:
from arcline import (
SupplierNode, PlantNode, WarehouseNode, DistributionCenterNode, CustomerNode,
LaneEdge, ProductionEdge, StorageEdge,
SourceNode, FacilityNode, DemandNode, FlowEdge, TransportEdge,
TransportationMode, FacilityStatus, OwnershipType, OperationalShift,
StorageType, CustomerSegment, LaneServiceLevel, Currency, UnitOfMeasure,
)After pip install -e ., an arcline console script is on $PATH:
arcline --help
arcline init <path> # create an empty project
arcline validate <path> # cross-file integrity checks
arcline dashboard <path> # serve the Dash UI on http://127.0.0.1:8050
arcline history sync <path> # pre-warm the Parquet cache from MS-SQL
arcline history clear <path> # delete cached parquet snapshots (offline-only)
arcline history validate # check catalog + DB reachabilityAll commands accept relative or absolute paths. On any I/O error the CLI prints the resolved absolute path plus an actionable hint (e.g. "directory exists but is missing manifest.yaml — initialise it with arcline init <path>") — no raw tracebacks.
from arcline.graph.builder import NetworkBuilder
from arcline.graph.library import (
SupplierNode, PlantNode, WarehouseNode, CustomerNode, LaneEdge,
)
from arcline.graph.enums import TransportationMode
from arcline.io import Project
b = NetworkBuilder()
s1 = b.add(SupplierNode(
name = "Acme Steel", hashKey = "N-S1",
latitude = 12.97, longitude = 77.59,
leadTimeDays = 3.0, reliabilityScore = 0.95,
))
p1 = b.add(PlantNode(
name = "Bengaluru Plant", hashKey = "N-P1",
productionRatePerHr = 120.0, maxCapacity = 10_000.0,
))
w1 = b.add(WarehouseNode(
name = "Whitefield DC", hashKey = "N-W1",
maxCapacity = 25_000.0,
))
c1 = b.add(CustomerNode(
name = "Customer A", hashKey = "N-C1",
demandMean = 350.0, demandStd = 45.0,
))
b.connect(s1, p1, cls = LaneEdge, name = "S1-P1", hashKey = "E-S1P1",
distanceKm = 220.0, costPerUnit = 2.5, transitDays = 1.5,
mode = TransportationMode.ROAD)
b.connect(p1, w1, cls = LaneEdge, name = "P1-W1", hashKey = "E-P1W1",
distanceKm = 15.0, costPerUnit = 0.4, transitDays = 0.2,
mode = TransportationMode.ROAD)
b.connect(w1, c1, cls = LaneEdge, name = "W1-C1", hashKey = "E-W1C1",
distanceKm = 40.0, costPerUnit = 1.1, transitDays = 0.5,
mode = TransportationMode.ROAD)
graph = b.build(backend = "networkx")
# Persist as a project on disk
proj = Project.fromGraph(graph, path = "./my_network", name = "My Demo")
# Re-open later
proj2 = Project.open("./my_network")
graph2 = proj2.toGraph()arcline init <path> (or Project.fromGraph(...)) produces a self-contained, git-versionable folder:
my_network/
├── manifest.yaml # name · description · arclineSchemaVersion · timestamps
├── nodes.json # canonical flat array of node records (diff-friendly)
├── edges.json # canonical flat array of edge records
├── nodes.parquet # OPTIONAL bulk format (written by toParquet / random generator)
├── edges.parquet
├── icons/ # custom node/edge icons (override library defaults)
├── scenarios/ # populated in Phase 3
├── .cache/history/ # historian Parquet cache (gitignored)
└── .gitignore
The JSON files are the canonical format — that is what Project.open reads and what arcline validate checks. The Parquet pair is convenient for bulk pandas/Spark workflows but is not required.
arcline dashboard <path> (or python -m arcline.dashboard --project <path>) launches a multi-page Dash + Plotly + Bootstrap app on http://127.0.0.1:8050:
| Page | URL | What it does |
|---|---|---|
| Home | / |
Project picker; recent projects; new-project wizard. |
| Nodes | /dashboard/nodes |
Paginated table of all nodes. Create / Edit / Delete / Duplicate via pydantic-driven forms. |
| Edges | /dashboard/edges |
Same CRUD surface for edges; src/dst dropdowns are filtered to existing nodes. |
| Visualize | /dashboard/visualize |
Whole-network rendering. Toggle: geo (Mapbox/OSM tiles, when nodes have lat/lon), force-directed (NetworkX spring_layout), tiered (multipartite_layout). Edge widths can encode flow once Phase 2 lands. |
| History | /dashboard/history |
Time-series · distribution · summary stats for any (entity, attribute) that has a HistorySpec. Pulls from MS-SQL or warm Parquet cache. |
| Solve | /dashboard/solve |
Phase 2 placeholder. |
| Scenarios | /dashboard/scenarios |
Phase 3 placeholder. |
The top navbar carries a DB-status pill (green = live, amber = cached-only, red = DSN unset). It is TTL-cached (30 s) so navigation is not gated on a synchronous SELECT 1.
Single-user MVP: state lives in a process-wide session slot. Multi-user / multi-tenant support is gated behind a future
[server]extra.
Every node / edge attribute can be wired to a row-set in your data warehouse via a class-level HistorySpec mapping. The shipped taxonomy already declares specs for the common attributes; see examples/historian_schema.sql for the matching DDL.
# 1. install the extras + Microsoft ODBC Driver 18 (OS-level)
pip install -e .[historian]
# 2. point arcline at your warehouse (the only place credentials live)
export ARCLINE_MSSQL_DSN="mssql+pyodbc://user:pass@host/db?driver=ODBC+Driver+18+for+SQL+Server"
# 3. smoke-check the catalog + connection
arcline history validate
# 4. pre-warm the parquet cache for a project (offline-friendly)
arcline history sync ./demo_network --since 2023-01-01
# 5. open the dashboard — /dashboard/history is now live
arcline dashboard ./demo_networkfrom arcline.historian import fetch, summary, rolling, distribution
from arcline.io import Project
proj = Project.open("./demo_network")
graph = proj.toGraph()
# pick any edge that has a HistorySpec
edge = next(e for e in graph.edges if hasattr(type(e), "history"))
spec = type(edge).history["transitDays"]
df = fetch(
projectPath = proj.path, kind = type(edge).kind,
hashKey = edge.hashKey, attribute = "transitDays",
spec = spec, start = "2023-01-01", end = "2025-01-01",
)
print(summary(df)) # count · mean · std · p5/p95 · last
print(rolling(df, window = 14).tail()) # 14-period rolling mean / std
print(distribution(df, bins = 30)) # histogram bins- Credentials never leave the env var. No DSN ever appears in
manifest.yaml, the Parquet cache, or logs (redactDsnmasks both URL and ODBC keyword forms). - Spec-hash invalidation. Each Parquet file is keyed by a hash of its
HistorySpec, so swapping avalueColumnautomatically busts the old cache. - Offline-friendly. The dashboard remains functional against the warm cache when the warehouse is unreachable; the DB-status pill goes amber.
- Identifier whitelist. Every
table/keyColumn/valueColumn/tsColumn/ filter key must match^[A-Za-z_][A-Za-z0-9_]*$before being interpolated; values are always parameter-bound.
| Path | What it shows |
|---|---|
examples/toy_3node/ |
Hand-curated 3-node project (SupplierNode → PlantNode → CustomerNode). Open with arcline dashboard examples/toy_3node. |
examples/full_taxonomy_demo.py |
One-file showcase that exercises every built-in node and edge class plus every Enum. Prints a capability matrix and writes a 4-node / 5-edge project. Run python examples/full_taxonomy_demo.py --output ./showcase. |
examples/random_network.py |
Parametric random 4-tier supply-chain generator. Writes a full project + bulk Parquet pair. Defaults: 6 / 4 / 3 / 8 entities, seed 42 → 21 nodes, 29 edges. |
examples/networkx_supply_chain.py |
Low-level walkthrough of the AbstractNode / AbstractEdge surface for users who want to extend the framework with their own classes (rather than use the shipped taxonomy). |
examples/historian_schema.sql |
DDL for the MS-SQL fact tables that match the built-in HistorySpec catalog (fact_lane_lead_time, fact_lane_cost, fact_plant_throughput, fact_warehouse_throughput, fact_customer_demand). |
arcline resolves your path against the current working directory. If you ran the random generator from one shell and then opened a fresh shell to launch the dashboard, the cwd may differ. The CLI now prints the resolved absolute path plus an actionable hint:
Project not found at ./demo_network (resolved: C:\...\demo_network).
Directory does not exist. Either pass a different path, or generate one first:
arcline init ./demo_network
# or, for a random demo network:
python examples/random_network.py --output ./demo_network
Either cd to the right directory or pass an absolute path.
Means the kind registry is empty when Project.open() runs. The arcline.io package triggers the registry side-effect on import — but if you import lower-level modules in an unusual order, force the side-effect explicitly:
import arcline.graph.library # noqa: F401
from arcline.io import Project
proj = Project.open("./my_network")- Open the navbar: pill should be green (DB live) or amber (cached-only). If red,
ARCLINE_MSSQL_DSNis unset. - Run
arcline history validateto confirm catalog + connection. - For an offline demo: run
arcline history sync ./demo_networkfrom a machine that does have access; the.cache/history/directory is portable.
Phase 2 (Pyomo + solver backends) has not yet shipped. Track it on the CLAUDE.md §7 roadmap.
- Naming: strict camelCase across all Python identifiers (functions, methods, attributes, parameters, locals). Enforced by
tools/check_camel_case.py(CI gate; 0 violations). Exemptions:def test_*, dunders, environment variables, external library kwargs. - Tests:
pytest -qshould be green before any PR (currently 115 passed, 2 skipped in ~9 s). - Design intent: every architectural decision lives in
CLAUDE.md— read it before proposing structural changes.
pytest -q # full suite
python tools/check_camel_case.py # naming guardrail