Skip to content

Example 14: Multi-stage distribution ​

1. Overview ​

This bounded context models a directed transshipment network with production nodes, sales nodes, and intermediate distribution nodes. Production capacity, sales demand, and the source's unitCost arc map are:

Node typeNodes and capacity/demand
Production (P)Guangzhou 600; Dalian 400
Sales (S)Nanjing 200; Jinan 150; Nanchang 350; Qingdao 300
Transshipment (T)Shanghai; Tianjin

The directed arcs and unit costs are exactly those present in the source map. An absent unitCost entry is not an arc.

ArcCostArcCost
Guangzhou → Shanghai2Guangzhou → Tianjin3
Dalian → Qingdao4Dalian → Shanghai3
Dalian → Tianjin1Shanghai → Nanjing2
Shanghai → Jinan6Shanghai → Nanchang3
Shanghai → Qingdao6Tianjin → Nanjing4
Tianjin → Jinan4Tianjin → Nanchang6
Tianjin → Qingdao5

1. Dependent Contexts ​

None. Node classes, arc data, and all capacities/costs are local to Demo14.


2. Concepts / Entities ​

1. Network node ​

A network node is one location in the distribution network. The Kotlin source represents the three roles with the sealed Node interface and Product, Sale, and Distribution classes.

Namei : display name of node i.

Storagei : production capacity of a production node i∈P; distribution and sales nodes have no production capacity field.

Demandi : demand of a sales node i∈S; production and distribution nodes have no demand field.

2. Directed arc ​

A directed arc connects a source node to a destination node and carries an integer flow.

cij : unit transportation cost on arc (i,j), as read from unitCost or ArcData.unit_cost.

xij : flow assigned to arc (i,j); its decision-variable definition is given in Section 3.


3. Variables ​

1. Decision Variables ​

xij : integer goods flow from node i to node j, a shipment quantity, domain Z≥0, for every (i,j)∈N×N. Non-arc entries are fixed to zero; only arc entries are registered in the Kotlin model. Kotlin additionally applies the production-node range bound xij≤Storagei to each outgoing arc, while the Rust model represents the same production limit through its node constraint.

2. Auxiliary Variables ​

There are no solver-owned auxiliary variables. Cost, Out, and In are intermediate expressions.


4. Predicates ​

1. Node-role predicates ​

ProductNode(i) : node i is a production node and has a storage/capacity value.

SaleNode(i) : node i is a sales node and has a demand value.

DistributionNode(i) : node i is an intermediate transshipment node.

2. Arc predicate ​

ArcExists(i,j) : the source unitCost map (or Rust arcs list) contains the directed arc (i,j).


5. Sets ​

1. Network nodes ​

N : the universal set of eight nodes.

P={i∈N∣ProductNode(i)} : production nodes Guangzhou and Dalian, which supply goods.

S={i∈N∣SaleNode(i)} : sales nodes Nanjing, Jinan, Nanchang, and Qingdao, which receive demand.

T={i∈N∣DistributionNode(i)} : transshipment nodes Shanghai and Tianjin, which conserve flow.

2. Directed arcs ​

E : the directed arc set,

E={(i,j)∈N×N∣ArcExists(i,j)}.

The model does not infer a reverse arc when (i,j)∈E; each direction must be listed explicitly.

3. Entity Pairs / Relations ​

E⊆N×N : the directed transport relation. Non-edges are fixed to zero before model registration and therefore cannot carry flow.


6. Intermediate Values ​

1. Transportation cost ​

Description: Cost is the total unit cost of all flow on the defined directed arcs.

Cost=∑(i,j)∈Ecijxij.

2. Outgoing flow ​

Description: Out is the total flow leaving a production or transshipment node. Because non-arc variables are fixed to zero, the source's all-destination sum is equivalent to the arc-restricted sum.

Outi=∑j:(i,j)∈Exij,∀i∈P∪T.

3. Incoming flow ​

Description: In is the total flow entering a sales or transshipment node. The source uses the incoming orientation xji; writing xij here would count outgoing flow instead.

Ini=∑j:(j,i)∈Exji,∀i∈S∪T.

7. Assertions ​

1. Node-role partition ​

Description: Every node belongs to exactly one of the three source node classes.

N=P∪˙S∪˙T,P∩S=S∩T=P∩T=∅.

2. Balanced capacity and demand data ​

Description: Total production capacity equals total sales demand in this instance.

∑i∈PStoragei=600+400=1000=200+150+350+300=∑i∈SDemandi.

3. Flow accounting identity ​

Description: Every arc flow is counted once at its source and once at its destination; transshipment balance therefore transfers, rather than creates or destroys, flow.

∑i∈NOuti−∑i∈NIni=0

where undefined Out/In terms at node roles are understood as zero and non-arc flows are zero.


8. Constraints ​

1. Production Capacity [生产能力上限] ​

Description: The flow dispatched by each production node cannot exceed its storage/capacity value.

s.t.Outi≤Storagei,∀i∈P.

2. Sales Demand Coverage [销售需求满足] ​

Description: Each sales node must receive at least its stated demand.

s.t.Ini≥Demandi,∀i∈S.

3. Transshipment Flow Balance [转运流量平衡] ​

Description: A transshipment node cannot accumulate or create goods; its incoming and outgoing flows must be equal. Kotlin registers the equality as two inequalities, while Rust registers one equality relation.

s.t.Ini=Outi,∀i∈T.

Corollary: With the balanced totals in Section 7, the aggregate inequalities force all production capacity and all sales demand to be tight.

∑i∈POuti=∑i∈PStoragei=1000,∑i∈SIni=∑i∈SDemandi=1000.

9. Objective Function ​

Description: Minimize total transportation cost over the listed directed arcs.

minCost=min∑(i,j)∈Ecijxij.

For the current data, a cost-4600 flow is:

ArcFlow
Guangzhou → Shanghai550
Guangzhou → Tianjin50
Dalian → Qingdao300
Dalian → Tianjin100
Shanghai → Nanjing200
Shanghai → Nanchang350
Tianjin → Jinan150

All other defined arcs carry zero flow. This is a documented feasible optimum for the listed arc costs, not a numeric assertion made by the Kotlin core build-structure test.


10. Algorithm References ​

No standalone algorithm document is referenced; the page describes the source-local transshipment model.

Algorithm NameFile PathReferenced InBrief Description
None——Direct arc-flow expressions and node-balance constraints

11. Ubiquitous Language ​

TermSymbolDefinition
Production nodePNode that supplies goods and is bounded by Storage.
Sales nodeSNode that requires incoming goods according to Demand.
Transshipment nodeTNode where incoming and outgoing flows balance.
Directed arcEA listed source-to-destination transport connection.
Arc flowxijInteger goods sent along arc (i,j).
Unit costcijCost of one unit on arc (i,j).
Total costCostSum of unit cost times arc flow.

12. Design Decisions ​

DecisionAlternativesRationaleDate
Use the explicit directed arc set ETreat every node pair as an arcMatches unitCost/ArcData and prevents nonexistent direct shipments.2026-09-08
Keep In as ∑jxjiUse ∑jxij for both directionsThe incoming orientation is required for transshipment balance.2026-09-08
Preserve inequality/equality implementation differencesNormalize both languages to one API formKotlin uses two inequalities for balance; Rust uses Equal, and both semantics are recorded.2026-09-08

13. Change Log ​

VersionChangeReason
1.0Reorganized the example into the domain-model sections and documented Kotlin/Rust arc-flow implementations.Align the page with the template while retaining the current data and the cost-4600 flow.

Minimal current model-building example ​

The snippets are model-building fragments. nodes, unitCost, Node subclasses, arcs, NodeType, and helper functions are supplied by the linked source files.

kotlin
import fuookami.ospf.kotlin.multiarray.*
import fuookami.ospf.kotlin.math.*
import fuookami.ospf.kotlin.math.algebra.number.*
import fuookami.ospf.kotlin.math.symbol.operation.*
import fuookami.ospf.kotlin.core.model.intermediate.*
import fuookami.ospf.kotlin.core.model.mechanism.*
import fuookami.ospf.kotlin.core.solver.scip.*
import fuookami.ospf.kotlin.core.symbol.*
import fuookami.ospf.kotlin.core.variable.*
import fuookami.ospf.kotlin.example.solveLinearMetaModel

// nodes, unitCost, and flt64Converter come from Demo14.kt.
val model = LinearMetaModel<Flt64>("demo14", converter = flt64Converter)
val x = UIntVariable2("x", Shape2(nodes.size, nodes.size))
for (from in nodes) for (to in nodes) {
    if (unitCost[from]?.get(to) != null) {
        if (from is Product) x[from, to].range.leq(from.storage)
        model.add(x[from, to])
    } else {
        x[from, to].range.eq(UInt64.zero)
    }
}
val cost = LinearExpressionSymbol(
    sum(nodes.flatMap { from -> nodes.mapNotNull { to ->
        unitCost[from]?.get(to)?.let { it * x[from, to] }
    } }), name = "cost"
)
val out = LinearIntermediateSymbols1<Flt64>("out", Shape1(nodes.size)) { i, _ ->
    LinearExpressionSymbol(sum(x[nodes[i], _a]), name = "out_${nodes[i].name}")
}
val input = LinearIntermediateSymbols1<Flt64>("in", Shape1(nodes.size)) { i, _ ->
    LinearExpressionSymbol(sum(x[_a, nodes[i]]), name = "in_${nodes[i].name}")
}
model.add(cost); model.add(out); model.add(input); model.minimize(cost)
for (node in nodes.filterIsInstance<Product>()) model.addConstraint(out[node] leq node.storage)
for (node in nodes.filterIsInstance<Sale>()) model.addConstraint(input[node] geq node.demand)
for (node in nodes.filterIsInstance<Distribution>()) {
    model.addConstraint(out[node] geq input[node])
    model.addConstraint(out[node] leq input[node])
}

suspend fun solve() = solveLinearMetaModel(ScipLinearSolver(), model)
rust
use ospf_rust_core::model::{ConstraintRelation, MetaModel, ObjectiveCategory};
use ospf_rust_core::symbol::flat_map1;
use ospf_rust_core::variable::{UInteger, VariableCombination2D, VariableRange};
use ospf_rust_multiarray::Shape;

// build_nodes/build_arcs, extract_coeffs, and solve_typed are from demo14.rs.
let nodes = build_nodes();
let arcs = build_arcs();
let mut model = MetaModel::<f64>::new("demo14");
let x_vars = VariableCombination2D::with_name_and_range_generator(
    Shape::new([nodes.len(), nodes.len()]), "x", |_i, v| format!("{}_{}", v[0], v[1]),
    |_i, v| if arcs.iter().any(|a| a.from == v[0] && a.to == v[1]) {
        VariableRange::with_lower(0.0)
    } else { VariableRange::fixed(0.0) }
);
let x_idx = model.register_combination(&x_vars)?;
let cost = flat_map1("cost", &arcs, |arc| {
    ospf_rust_core::symbol::flatten::Linear::new(vec![
        ospf_rust_core::symbol::flatten::LinearMonomial::new(
            arc.unit_cost, x_idx[&[arc.from, arc.to]])
    ], 0.0)
}, |_, arc| format!("{}_{}", arc.from, arc.to));
let ids: Vec<usize> = (0..nodes.len()).collect();
let out = flat_map1("trans_out", &ids, |&i| {
    let terms = (0..nodes.len()).map(|j|
        ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, x_idx[&[i, j]])).collect();
    ospf_rust_core::symbol::flatten::Linear::new(terms, 0.0)
}, |_, i| i.to_string());
let input = flat_map1("trans_in", &ids, |&j| {
    let terms = (0..nodes.len()).map(|i|
        ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, x_idx[&[i, j]])).collect();
    ospf_rust_core::symbol::flatten::Linear::new(terms, 0.0)
}, |_, j| j.to_string());
model.add_symbol_combination(&cost)?;
model.add_symbol_combination(&out)?;
model.add_symbol_combination(&input)?;
let coeffs = (0..arcs.len()).flat_map(|i| cost.symbol_polynomial(i).monomials().iter()
    .map(|m| (m.var_index(), *m.coefficient())).collect::<Vec<_>>()).collect::<Vec<_>>();
model.add_linear_objective(&coeffs, "cost");
model.set_objective_category(ObjectiveCategory::Minimum);
for (i, node) in nodes.iter().enumerate() {
    let out_coeffs = extract_coeffs(&out[i]);
    let in_coeffs = extract_coeffs(&input[i]);
    match node.kind {
        NodeType::Product(cap) => model.add_linear_constraint(
            &out_coeffs, ConstraintRelation::LessEqual, cap, &format!("product_{}", i))?,
        NodeType::Sale(demand) => model.add_linear_constraint(
            &in_coeffs, ConstraintRelation::GreaterEqual, demand, &format!("sale_{}", i))?,
        NodeType::Distribution => {
            let mut balance = out_coeffs;
            balance.extend(in_coeffs.into_iter().map(|(idx, c)| (idx, -c)));
            model.add_linear_constraint(&balance, ConstraintRelation::Equal, 0.0,
                &format!("balance_{}", i))?;
        }
    }
}
let _output = solve_typed(model)?;

Source and verification ​

Kotlin/Rust correspondence ​

Both implementations use the same eight nodes, thirteen directed arcs, costs, capacities, demands, and minimum-cost flow semantics. Their model, variable-combination, and symbol-combination APIs are independent. Kotlin registers each defined arc and applies a redundant per-production-arc range bound; Rust fixes non-arcs through VariableRange and applies production capacity at the node constraint.

The Kotlin core build test checks structure only and does not assert the numeric flow or the cost-4600 result.