Example 4: Production with material and balance limits
1. Overview
This bounded context chooses production quantities for two products, maximising profit under material-availability, product-yield, and pairwise-difference rules.
1. Dependent Contexts
- Core continuous linear optimisation context (real variables, linear intermediate symbols, constraints, and solver adapter).
The Kotlin and Rust implementations are mathematically close but have different variable domains. The snippets below are model-building fragments whose data come from the linked Demo4 implementations.
2. Concepts / Entities
1. Material
A material is a consumable resource with a finite availability.
The current material data are
2. Product
A product has a profit, a maximum production quantity, and material usage coefficients.
The current product data are:
| Product | Profit | Maximum yield | A per unit | B per unit |
|---|---|---|---|---|
| P1 | 5 | 3 | 6 | 1 |
| P2 | 4 | 2 | 4 | 2 |
The source sets
3. Variables
1. Decision Variables
The Kotlin source uses RealVariable1 and sets only the upper bound
2. Auxiliary Variables
None. Material use and pairwise differences are derived linear values.
4. Predicates
1. Production Status
Predicates classify products and ordered product pairs.
WithinYield
MaterialFeasible
OrderedDistinct
5. Sets
1. Material Category
2. Product Category
3. Entity Pairs / Relations
Both
6. Intermediate Values
1. Total Profit
Description: Total profit is the profit per product multiplied by the selected production quantity; it is the objective expression.
2. Material Use
Description: Material use records the total consumption of each material across all products.
3. Ordered Production Difference
Description: The ordered production difference is a derived value used by the pairwise rule; it is not a separate registered decision variable.
7. Assertions
1. Non-negative Current Data
Description: All current profits, capacities, yields, material uses, and difference limits are non-negative data.
2. Reverse Ordered Pairs
Description: Every distinct product pair has both orientations, which makes the one-sided difference family equivalent to an absolute-difference bound.
8. Constraints
These are the constraints present in the current Kotlin model; the Rust counterpart additionally encodes non-negativity in the variable range.
1. Maximum Product Yield
[最大产品产量]: each product's production quantity cannot exceed its configured maximum.
2. Material Availability
[物料可用量上限]: total use of each material cannot exceed the available amount.
3. Pairwise Production Difference
[产品间产量差上限]: the production quantity of one product may exceed another by at most
Corollary: because both orientations are present, the pairwise family is equivalent to an absolute-difference bound.
The current Kotlin model does not add
9. Objective Function (if applicable)
Description: maximise total production profit.
For the displayed data,
10. Algorithm References
No standalone algorithm document is referenced. This is a direct continuous linear formulation.
| Algorithm Name | File Path | Referenced In | Brief Description |
|---|---|---|---|
| Not applicable | — | — | No domain-specific algorithm is needed. |
11. Ubiquitous Language
| Term | Symbol | Definition |
|---|---|---|
| Material | Consumable resource with an availability limit. | |
| Product | Product with profit and an upper production limit. | |
| Production quantity | Continuous quantity of product | |
| Material use | Total consumption of material | |
| Pairwise difference | Ordered difference | |
| Maximum yield | Upper production bound for product | |
| Difference limit | Maximum allowed ordered production difference. |
12. Design Decisions
| Decision | Alternatives | Rationale | Date |
|---|---|---|---|
| Keep Kotlin RealVariable1 signed | Add an explicit non-negative lower bound | The current Kotlin source sets only x[p].range.ls(maxYield); negative values remain allowed. | Current implementation |
| Keep Rust's bounded non-negative range documented as a difference | Force identical variable domains | Rust uses UContinuous and VariableRange::bounded(0.0, maxYield), so it excludes the Kotlin-only negative region. | Current implementation |
| Register both orientations of the difference rule | Add one absolute-value constraint | The source loops over every ordered pair p1 != p2; the two orientations give the same business effect. | Current implementation |
Minimal current implementation fragments
The following are non-standalone fragments from Demo4. products, materials, maxDiff, model setup, converter, and solver setup come from the linked source.
// Fragment from Demo4.initVariable/initSymbol/initObject/initConstraint.
// Data source: Demo4's private materials/products lists and maxDiff.
val x = RealVariable1("x", Shape1(products.size))
val profit = LinearExpressionSymbol(
sum(products) { p -> p.profit * x[p] },
name = "profit"
)
val use = LinearIntermediateSymbols1<Flt64>(
"use",
Shape1(materials.size)
) { m, _ ->
val material = materials[m]
LinearExpressionSymbol(
sum(products.filter { it.use.contains(material) }) { p ->
p.use[material]!! * x[p]
},
name = "use"
)
}
metaModel.add(x)
metaModel.add(profit)
metaModel.add(use)
metaModel.maximize(profit, "profit")
for (p in products) {
x[p].range.ls(p.maxYield)
}
for (m in materials) {
metaModel.addConstraint(use[m] leq m.available)
}
for (p1 in products) {
for (p2 in products) {
if (p1.index != p2.index) {
metaModel.addConstraint((x[p1] - x[p2]) leq maxDiff.toFlt64())
}
}
}// Fragment from demo4.rs::ProductionModel::register/add_constraints.
// Data source: build_materials/build_products; the Rust range is explicit.
let x = VariableCombination1D::with_range_generator(
Shape::new([products.len()]),
"x",
|i, _| VariableRange::bounded(0.0, products[i].max_yield),
);
let x_idx = model.register_combination(&x)?;
let profit = flat_map1_indexed(
"profit",
products,
|i, product| {
ospf_rust_core::symbol::flatten::Linear::new(
vec![ospf_rust_core::symbol::flatten::LinearMonomial::new(
product.profit,
x_idx[i],
)],
0.0,
)
},
|_, product| product.name.clone(),
);
model.add_symbol_combination(&profit)?;
let r#use = flat_map1_indexed(
"usage",
materials,
|m, _material| {
let monomials: Vec<_> = products
.iter()
.enumerate()
.map(|(p, product)| {
ospf_rust_core::symbol::flatten::LinearMonomial::new(
product.usage_by_material[m],
x_idx[p],
)
})
.collect();
ospf_rust_core::symbol::flatten::Linear::new(monomials, 0.0)
},
|_, material| material.name.clone(),
);
model.add_symbol_combination(&r#use)?;
let profit_coeffs = extract_coeffs(&profit[0]);
model.add_linear_objective(&profit_coeffs, "profit");
model.set_objective_category(ObjectiveCategory::Maximum);
for (m, material) in materials.iter().enumerate() {
let coeffs = extract_coeffs(&r#use[m]);
model.add_linear_constraint(
&coeffs,
ConstraintRelation::LessEqual,
material.available,
&format!("material_{}_{}", m, material.name),
)?;
}
for p1 in 0..products.len() {
for p2 in 0..products.len() {
if p1 != p2 {
let coefficients = vec![(x_idx[p1], 1.0), (x_idx[p2], -1.0)];
model.add_linear_constraint(
&coefficients,
ConstraintRelation::LessEqual,
1.0,
&format!("diff_{}_{}", p1, p2),
)?;
}
}
}Source and verification
The Rust counterpart is mathematically close but not domain-identical: it bounds production by 0 <= x <= maxYield, while the current Kotlin implementation uses RealVariable1 with only x <= maxYield.
The Kotlin source uses LinearExpressionSymbol for profit, LinearIntermediateSymbols1 for material use, and RealVariable1 for production. The Rust source uses VariableCombination1D<UContinuous> and a bounded VariableRange. The fragments are excerpts rather than complete runnable programs.
13. Change Log
| Version | Change | Reason |
|---|---|---|
| 1.0 | Reorganised the example into the domain-model template and added quantified constraints, a pairwise corollary, and Kotlin/Rust fragments. | Make the signed-domain discrepancy explicit. |