Example 9: Integer Manhattan facility location
1. Overview
This bounded context chooses one integer facility coordinate minimizing the sum of Manhattan distances to six settlements. The Kotlin and Rust implementations use different absolute-value helper APIs and therefore have different explicit coordinate bounds.
The supplied settlement coordinates are:
| Settlement | ||
|---|---|---|
| 1 | 9 | 2 |
| 2 | 2 | 1 |
| 3 | 3 | 8 |
| 4 | 3 | -2 |
| 5 | 5 | 9 |
| 6 | 4 | -2 |
1. Dependent Contexts
- None. The example is a self-contained location model.
2. Concepts / Entities
1. Settlement
A settlement is a fixed observed point in the coordinate plane.
2. Facility
The facility is the point selected by the solver.
3. Variables
1. Decision Variables
IntVar("x") and IntVar("y") without explicit bounds; Rust creates integer combinations with
2. Auxiliary Variables
exampleAbsoluteSlack in Kotlin or AbsFunction in Rust. They are mechanism/function outputs, not user-selected decisions.
4. Predicates
1. Settlement Coordinate Predicate
Observed(s): settlement
2. Coordinate-Bound Predicate
RustBounded: true for the Rust implementation, which configures both coordinates in
5. Sets
1. Settlement Category
2. Coordinate Components
3. Entity Pairs / Relations
6. Intermediate Values
1. X-Axis Absolute Distance
Description: The non-negative horizontal distance between the selected facility and settlement
2. Y-Axis Absolute Distance
Description: The non-negative vertical distance between the selected facility and settlement
3. Settlement Distance
Description: Manhattan distance from the facility to settlement
4. Total Distance
Description: Sum of the Manhattan distances to all settlements; this is the minimized expression.
7. Assertions
1. Absolute Components Are Non-Negative
Description: Each helper output measures an absolute difference and cannot be negative.
2. Manhattan Decomposition
Description: Each settlement distance equals the sum of its horizontal and vertical components.
3. Sample Optimum Set
Description: The coordinate-wise integer medians characterize the optimum for the supplied six points.
8. Constraints
1. Facility Integer Domain (设施整数定义域约束)
Description: The selected facility coordinates are integers. Kotlin has no explicit geographic bounds; Rust additionally bounds them for the absolute-value mechanism.
and, for Rust only,
2. Absolute-Distance Linearization (绝对距离线性化约束)
Description: The source helper generates mechanism constraints whose semantic relation is the absolute value of each coordinate difference. No hand-written user-level inequality is added in Demo9.kt or demo9.rs.
3. Distance Definition (距离定义约束)
Description: Each settlement's Manhattan distance is the sum of its two components.
9. Objective Function (if applicable)
Description: Minimize total Manhattan distance.
10. Algorithm References
The page references helper mechanisms, but neither helper has a separate algorithm document in this example context.
| Algorithm Name | File Path | Referenced In | Brief Description |
|---|---|---|---|
exampleAbsoluteSlack | ospf-kotlin-example/src/main/.../ExampleModeling.kt | Section 6.1–6.2 | Kotlin adapter around SlackFunction with both positive and negative deviations. |
AbsFunction | ospf-rust-example/src/core/demo9.rs | Section 6.1–6.2 | Rust absolute-value function whose mechanism constraints are generated during conversion. |
11. Ubiquitous Language
| Term | Symbol | Definition |
|---|---|---|
| Settlement | Fixed point with observed coordinates. | |
| Facility | Integer point selected by the model. | |
| Component distance | Absolute difference on one coordinate axis. | |
| Manhattan distance | Sum of the two component distances. |
12. Design Decisions
| Decision | Alternatives | Rationale | Date |
|---|---|---|---|
| Use helper functions for absolute values | Hand-write a new Big-M formulation in each demo | The current Kotlin and Rust APIs generate the mechanism constraints | 2026-09-08 |
| Keep Kotlin coordinates unbounded | Add an arbitrary geographic box | Demo9.kt does not add bounds; documenting bounds would change its model | 2026-09-08 |
| Record Rust bounds explicitly | Claim both implementations have the same domain | demo9.rs uses VariableRange::bounded(-100.0, 100.0) | 2026-09-08 |
Minimal current model-building snippets
The settlement data comes from Demo9.kt/demo9.rs. The snippets intentionally show the source helper calls; the data and converter declarations are omitted as source-owned setup.
// `settlements` and `flt64Converter` come from Demo9.kt/ExampleModeling.kt.
val metaModel = LinearMetaModel<Flt64>("demo9", converter = flt64Converter)
val x = IntVar("x")
val y = IntVar("y")
metaModel.add(x)
metaModel.add(y)
val dx = LinearIntermediateSymbols1<Flt64>("dx", Shape1(settlements.size)) { i, _ ->
exampleAbsoluteSlack(
type = UInteger,
x = flt64Linear(x),
y = flt64Constant(settlements[i].x),
name = "dx_$i"
)
}
val dy = LinearIntermediateSymbols1<Flt64>("dy", Shape1(settlements.size)) { i, _ ->
exampleAbsoluteSlack(
type = UInteger,
x = flt64Linear(y),
y = flt64Constant(settlements[i].y),
name = "dy_$i"
)
}
val distance = LinearIntermediateSymbols1<Flt64>("distance", Shape1(settlements.size)) { i, _ ->
LinearExpressionSymbol(dx[i] + dy[i], name = "distance_$i")
}
metaModel.add(dx)
metaModel.add(dy)
metaModel.add(distance)
metaModel.minimize(sum(distance[_a]), "total distance")// `settlements` comes from demo9.rs; Rust intentionally keeps the source bounds.
let mut model = MetaModel::<f64>::new("demo9");
let x: VariableCombination1D<Integer> =
VariableCombination1D::with_range_generator(Shape::new([1]), "x", |_, _| {
VariableRange::bounded(-100.0, 100.0)
});
let y: VariableCombination1D<Integer> =
VariableCombination1D::with_range_generator(Shape::new([1]), "y", |_, _| {
VariableRange::bounded(-100.0, 100.0)
});
let x_idx = model.register_combination(&x)?;
let y_idx = model.register_combination(&y)?;
let dx_fn = SymbolCombination::new(Shape::new([settlements.len()]), "dx", |i, _| {
AbsFunction::named(
&format!("dx_{}", settlements[i].name),
ospf_rust_core::symbol::flatten::Linear::new(
vec![ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, x_idx[0])],
-settlements[i].x,
),
)
});
model.add_symbol_combination(&dx_fn)?;
let dy_fn = SymbolCombination::new(Shape::new([settlements.len()]), "dy", |i, _| {
AbsFunction::named(
&format!("dy_{}", settlements[i].name),
ospf_rust_core::symbol::flatten::Linear::new(
vec![ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, y_idx[0])],
-settlements[i].y,
),
)
});
model.add_symbol_combination(&dy_fn)?;
let dx_idx: Vec<_> = (0..settlements.len()).map(|i|
dx_fn.symbol_polynomial(i).monomials()[0].var_index()
).collect();
let dy_idx: Vec<_> = (0..settlements.len()).map(|i|
dy_fn.symbol_polynomial(i).monomials()[0].var_index()
).collect();
let distance_expr = flat_map1_indexed("distance", settlements, |i, settlement| {
ospf_rust_core::symbol::flatten::Linear::new(vec![
ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, dx_idx[i]),
ospf_rust_core::symbol::flatten::LinearMonomial::new(1.0, dy_idx[i]),
], 0.0)
}, |_, settlement| settlement.name.clone());
model.add_symbol_combination(&distance_expr)?;
let mut objective = Vec::new();
for i in 0..settlements.len() {
for monomial in distance_expr.symbol_polynomial(i).monomials() {
objective.push((monomial.var_index(), *monomial.coefficient()));
}
}
model.add_linear_objective(&objective, "distance");
model.set_objective_category(ObjectiveCategory::Minimum);
// AbsFunction 机制会在模型转换阶段自动生成绝对值线性化约束。13. Change Log
| Version | Change | Reason |
|---|---|---|
| 2026-09-08 | Reorganized the page into the domain-model template; documented helper-generated constraints, the Rust-only bounds, and Kotlin/Rust tabs | Make the two current implementations comparable without claiming identical domains |