@ohtaman/washi 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +55 -20
- package/README.md +55 -20
- package/README.zh-CN.md +55 -20
- package/THIRD_PARTY_LICENSES.md +3 -4
- package/dist/browser/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/browser/browser-runtime-TLCY0hkc.js +1793 -0
- package/dist/browser/highs-worker.js +1 -1
- package/dist/browser/highs.wasm +0 -0
- package/dist/browser/highs.wasm.LICENSE +1 -2
- package/dist/browser/loader-DewlSho8.js +643 -0
- package/dist/browser/solvers/glpk.js +1 -1
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +7 -1
- package/dist/browser/solvers/z3.min.js +1 -1
- package/dist/browser/washi.js +1096 -444
- package/dist/browser/washi.min.js +3 -4
- package/dist/lib/_washi-node-only-BvRk9kiK.js +0 -0
- package/dist/lib/highs-browser-runtime.js +2120 -4124
- package/dist/lib/highs-node-runtime.js +24 -1
- package/dist/lib/highs-runtime.js +2 -0
- package/dist/lib/highs-worker.js +1 -1
- package/dist/lib/highs.wasm +0 -0
- package/dist/lib/highs.wasm.LICENSE +1 -2
- package/dist/lib/loader-BZUQNvh4.js +649 -0
- package/dist/lib/{registry-BEfvpZW3.js → registry-C20sxTwm.js} +54 -41
- package/dist/lib/solvers/glpk.js +2 -2
- package/dist/lib/solvers/z3.js +1 -1
- package/dist/lib/washi.js +973 -305
- package/dist/types/decomposition/benders.d.ts +32 -0
- package/dist/types/decomposition/benders.d.ts.map +1 -0
- package/dist/types/decomposition/column-generation.d.ts +34 -0
- package/dist/types/decomposition/column-generation.d.ts.map +1 -0
- package/dist/types/decomposition/index.d.ts +4 -0
- package/dist/types/decomposition/index.d.ts.map +1 -0
- package/dist/types/decomposition/types.d.ts +230 -0
- package/dist/types/decomposition/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
- package/dist/types/modeler/constraint.d.ts +11 -0
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +10 -0
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +2 -1
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/linearizer.d.ts +15 -0
- package/dist/types/modeler/linearizer.d.ts.map +1 -0
- package/dist/types/modeler/lp-writer.d.ts +3 -1
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +147 -2
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/browser-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/driver.d.ts +232 -0
- package/dist/types/modeler/solvers/highs/driver.d.ts.map +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts +1 -0
- package/dist/types/modeler/solvers/highs/index.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/loader.d.ts +4 -4
- package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts +2 -1
- package/dist/types/modeler/solvers/highs/node-runtime.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +2 -0
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/dist/types/modeler/types.d.ts +27 -0
- package/dist/types/modeler/types.d.ts.map +1 -1
- package/docs/examples/index.md +4 -0
- package/docs/examples/traveling-salesperson.md +173 -0
- package/docs/examples/vehicle-routing.md +206 -0
- package/docs/getting-started/ai-agents.md +8 -4
- package/docs/guide/constraints.md +1 -1
- package/docs/guide/decomposition.md +399 -0
- package/docs/guide/unsat-core.md +10 -3
- package/docs/ja/getting-started/ai-agents.md +8 -4
- package/docs/solvers/custom-solver.md +1 -1
- package/docs/solvers/highs.md +25 -3
- package/docs/solvers/index.md +3 -3
- package/docs/zh/getting-started/ai-agents.md +8 -4
- package/llms-full.txt +831 -15
- package/llms.txt +9 -5
- package/package.json +17 -14
- package/skills/washi/SKILL.md +8 -8
- package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
- package/dist/browser/loader-CtaDHef_.js +0 -273
- package/dist/lib/loader-DmNC_UNk.js +0 -279
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Advanced Decomposition Methods & Best Practices
|
|
2
|
+
|
|
3
|
+
Mixed-Integer Programming (MIP) formulations can become slow when symmetry or coupling constraints cause branch-and-bound trees to grow large.
|
|
4
|
+
WaSHI provides decomposition orchestrators (`master.solveColumnGeneration` and `master.solveBenders`) along with lower-level modeling primitives for dual inspection and dynamic model modification.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Why Decomposition? (When Monolithic MIP Stalls)
|
|
9
|
+
|
|
10
|
+
There are two common problem structures where monolithic MIP formulations struggle:
|
|
11
|
+
|
|
12
|
+
| Problem Structure | Monolithic MIP Bottleneck | Technique | WaSHI API |
|
|
13
|
+
| :--- | :--- | :--- | :--- |
|
|
14
|
+
| **Symmetry & Loose Continuous Bounds**<br>*(Cutting Stock, Vehicle Routing/VRP, Crew Rostering)* | Symmetric permutations and a weak continuous relaxation enlarge the branch-and-bound tree. | **Column Generation**<br>*(Dantzig-Wolfe Decomposition)* | [`master.solveColumnGeneration()`](#column-generation-solvecolumngeneration) |
|
|
15
|
+
| **Complicating Decisions & Multi-Stage Structure**<br>*(Traveling Salesperson/TSP, Facility Location with Routing, Two-Stage Stochastic Planning)* | Coupling master decisions with large scenario or operational subproblems increases the matrix size. | **Benders Decomposition** | [`master.solveBenders()`](#benders-decomposition-solvebenders) |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Foundational Primitives
|
|
20
|
+
|
|
21
|
+
WaSHI provides fine-grained modeling primitives to inspect dual information and dynamically modify models:
|
|
22
|
+
|
|
23
|
+
### 1. Constraint Duals (`dual`)
|
|
24
|
+
Access the dual values (marginal impact on the objective per unit change in RHS) after an LP solve:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
m.constraint(expr`${x} + ${y} <= 10`, "capacity");
|
|
28
|
+
await m.solve();
|
|
29
|
+
|
|
30
|
+
const c = m.getConstraintByName("capacity")!;
|
|
31
|
+
console.log(c.dual); // Dual value / shadow price
|
|
32
|
+
console.log(m.solution?.duals.capacity);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Continuous LP Relaxation (`model.relax()`)
|
|
36
|
+
Decomposition algorithms require solving the continuous LP relaxation to extract dual values. `model.relax()` temporarily relaxes all `Integer` and `Binary` variables to continuous variables within their bounds:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Solves LP relaxation and populates shadow prices in m.solution.duals
|
|
40
|
+
const status = await m.relax();
|
|
41
|
+
console.log("Theoretical dual bound:", m.objectiveValue);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Dynamic Column Injection (`model.addColumn()`)
|
|
45
|
+
Inject a new decision variable along with its objective coefficient and constraint contributions in a single call:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const col = m.addColumn({
|
|
49
|
+
name: "pattern_1",
|
|
50
|
+
type: "Integer", // or "Continuous"
|
|
51
|
+
lb: 0,
|
|
52
|
+
cost: 1, // objective coefficient
|
|
53
|
+
coefficients: {
|
|
54
|
+
demand_itemA: 2,
|
|
55
|
+
demand_itemB: 1,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 4. Dynamic Constraint Modification (`addTerm` / `setTerm`)
|
|
61
|
+
Update the coefficients of individual variables in an existing constraint:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const c = m.getConstraintByName("budget")!;
|
|
65
|
+
c.addTerm(newVar, 5); // Increments newVar's coefficient by 5
|
|
66
|
+
c.setTerm(newVar, 0); // Setting to 0 cleanly removes the term
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Column Generation (`solveColumnGeneration`)
|
|
72
|
+
|
|
73
|
+
Column Generation starts with a **Restricted Master Problem (RMP)** with a small subset of columns. In each iteration:
|
|
74
|
+
1. It solves the RMP's LP relaxation via `master.relax()`.
|
|
75
|
+
2. Evaluates a pricing subproblem using constraint shadow prices.
|
|
76
|
+
3. Finds candidate columns with negative reduced costs ($\bar{c} = \text{cost} - \sum \pi_i a_i < 0$) and dynamically injects them into the master problem.
|
|
77
|
+
4. Repeats until no improving columns exist.
|
|
78
|
+
5. Finally solves the full master model as a Mixed-Integer Program (`finalizeAsMIP`, default: `true`) to obtain optimal integer solutions.
|
|
79
|
+
|
|
80
|
+
### Column Generation with the `pricing` Callback
|
|
81
|
+
|
|
82
|
+
Provide a `pricing` callback that receives constraint shadow prices (duals), solves the subproblem (using a WaSHI model or any custom algorithm), and returns new column proposals. WaSHI automatically evaluates reduced costs and injects improving columns:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Model, expr, minimize, maximize } from "@ohtaman/washi";
|
|
86
|
+
|
|
87
|
+
const rollWidth = 100;
|
|
88
|
+
const items = [
|
|
89
|
+
{ id: "A", width: 45, demand: 10 },
|
|
90
|
+
{ id: "B", width: 36, demand: 14 },
|
|
91
|
+
{ id: "C", width: 16, demand: 20 },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// 1. Define Restricted Master Problem (RMP) with naive initial single-cut patterns
|
|
95
|
+
const master = new Model("CuttingStockMaster");
|
|
96
|
+
|
|
97
|
+
const pA = master.integer("init_A", { lb: 0 }); // 2 of A per roll (45*2 = 90 <= 100)
|
|
98
|
+
const pB = master.integer("init_B", { lb: 0 }); // 2 of B per roll (36*2 = 72 <= 100)
|
|
99
|
+
const pC = master.integer("init_C", { lb: 0 }); // 6 of C per roll (16*6 = 96 <= 100)
|
|
100
|
+
|
|
101
|
+
master.objective(minimize(expr`${pA} + ${pB} + ${pC}`));
|
|
102
|
+
master.constraint(expr`2 * ${pA} >= 10`, "demand_A");
|
|
103
|
+
master.constraint(expr`2 * ${pB} >= 14`, "demand_B");
|
|
104
|
+
master.constraint(expr`6 * ${pC} >= 20`, "demand_C");
|
|
105
|
+
const [demandA, demandB, demandC] = master.constraints;
|
|
106
|
+
|
|
107
|
+
// 2. Solve with master.solveColumnGeneration()
|
|
108
|
+
const result = await master.solveColumnGeneration({
|
|
109
|
+
finalizeAsMIP: true,
|
|
110
|
+
pricing: async () => {
|
|
111
|
+
// Pricing subproblem: Bounded Knapsack
|
|
112
|
+
const sub = new Model("KnapsackPricing");
|
|
113
|
+
const a_A = sub.integer("a_A", { lb: 0, ub: 2 });
|
|
114
|
+
const a_B = sub.integer("a_B", { lb: 0, ub: 2 });
|
|
115
|
+
const a_C = sub.integer("a_C", { lb: 0, ub: 6 });
|
|
116
|
+
|
|
117
|
+
// Maximize shadow price collection
|
|
118
|
+
const dualA = demandA.dual ?? 0;
|
|
119
|
+
const dualB = demandB.dual ?? 0;
|
|
120
|
+
const dualC = demandC.dual ?? 0;
|
|
121
|
+
sub.objective(maximize(expr`${dualA} * ${a_A} + ${dualB} * ${a_B} + ${dualC} * ${a_C}`));
|
|
122
|
+
sub.constraint(expr`45 * ${a_A} + 36 * ${a_B} + 16 * ${a_C} <= ${rollWidth}`);
|
|
123
|
+
|
|
124
|
+
await sub.solve();
|
|
125
|
+
|
|
126
|
+
// Return the generated cutting pattern
|
|
127
|
+
return {
|
|
128
|
+
cost: 1,
|
|
129
|
+
type: "Integer",
|
|
130
|
+
coefficients: {
|
|
131
|
+
demand_A: Math.round(a_A.value ?? 0),
|
|
132
|
+
demand_B: Math.round(a_B.value ?? 0),
|
|
133
|
+
demand_C: Math.round(a_C.value ?? 0),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log("Optimal rolls needed:", result.objectiveValue); // 14
|
|
140
|
+
console.log("Iterations executed:", result.iterations);
|
|
141
|
+
console.log("Dynamic columns added:", result.columnsAdded);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom Pricing Algorithms (Graph / Dynamic Programming)
|
|
145
|
+
|
|
146
|
+
Because `pricing` is a standard callback returning column definitions, you can solve the subproblem using any algorithm (e.g. Dijkstra, Bellman-Ford, or custom heuristics):
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const result = await master.solveColumnGeneration({
|
|
150
|
+
pricing: async ({ getDual }) => {
|
|
151
|
+
// Solve subproblem with specialized custom graph algorithm
|
|
152
|
+
const route = solveSpecializedShortestPath(getDual);
|
|
153
|
+
if (!route) return null;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
cost: route.distance,
|
|
157
|
+
type: "Integer",
|
|
158
|
+
coefficients: route.visitedCustomers,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Benders Decomposition (`solveBenders`)
|
|
167
|
+
|
|
168
|
+
Benders Decomposition partitions the problem into:
|
|
169
|
+
- **Master Problem**: Contains complicating variables (e.g., 0-1 facility opening decisions $y$) and an auxiliary variable $\theta \ge 0$ representing estimated subproblem costs.
|
|
170
|
+
- **Subproblem**: Solves operational decisions given fixed master values $y^*$, feeding back **Optimality Cuts** to the master.
|
|
171
|
+
|
|
172
|
+
### Recommended: Automated Cut Generation with `sub.link()`
|
|
173
|
+
|
|
174
|
+
Return the subproblem `Model` from your callback and register linking constraints with `sub.link(...)`. WaSHI automatically manages $\theta$, evaluates master candidate values, extracts duals, and injects Benders optimality cuts:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
178
|
+
|
|
179
|
+
// 1. Master problem: Facility location decisions
|
|
180
|
+
const master = new Model("FacilityMaster");
|
|
181
|
+
const y1 = master.binary("open_F1");
|
|
182
|
+
const y2 = master.binary("open_F2");
|
|
183
|
+
|
|
184
|
+
// Objective: Fixed facility costs (theta is managed automatically)
|
|
185
|
+
master.objective(minimize(expr`100 * ${y1} + 120 * ${y2}`));
|
|
186
|
+
// Minimal aggregate capacity requirement
|
|
187
|
+
master.constraint(expr`50 * ${y1} + 60 * ${y2} >= 75`, "capacity_check");
|
|
188
|
+
|
|
189
|
+
// 2. Solve automatically with master.solveBenders()
|
|
190
|
+
const result = await master.solveBenders({
|
|
191
|
+
gapTolerance: 1e-4,
|
|
192
|
+
subproblem: () => {
|
|
193
|
+
// Operational dispatch subproblem
|
|
194
|
+
const sub = new Model("TransportSubproblem");
|
|
195
|
+
const x11 = sub.continuous("x_11", { lb: 0 });
|
|
196
|
+
const x12 = sub.continuous("x_12", { lb: 0 });
|
|
197
|
+
const x13 = sub.continuous("x_13", { lb: 0 });
|
|
198
|
+
const x21 = sub.continuous("x_21", { lb: 0 });
|
|
199
|
+
const x22 = sub.continuous("x_22", { lb: 0 });
|
|
200
|
+
const x23 = sub.continuous("x_23", { lb: 0 });
|
|
201
|
+
|
|
202
|
+
sub.objective(
|
|
203
|
+
minimize(
|
|
204
|
+
expr`3 * ${x11} + 4 * ${x12} + 2 * ${x13} + 4 * ${x21} + 2 * ${x22} + 3 * ${x23}`
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Customer demand constraints
|
|
209
|
+
sub.constraint(expr`${x11} + ${x21} >= 20`, "demand_C1");
|
|
210
|
+
sub.constraint(expr`${x12} + ${x22} >= 25`, "demand_C2");
|
|
211
|
+
sub.constraint(expr`${x13} + ${x23} >= 30`, "demand_C3");
|
|
212
|
+
|
|
213
|
+
// Link subproblem flows with master decisions y1, y2 via linking constraints
|
|
214
|
+
sub.link(expr`${x11} + ${x12} + ${x13} <= 50 * ${y1}`);
|
|
215
|
+
sub.link(expr`${x21} + ${x22} + ${x23} <= 60 * ${y2}`);
|
|
216
|
+
|
|
217
|
+
return sub; // Returning the model automatically generates cuts and manages theta
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
console.log("Optimal total cost:", result.objectiveValue); // 390
|
|
222
|
+
console.log("Iterations:", result.iterations);
|
|
223
|
+
console.log("Cuts added:", result.cutsAdded);
|
|
224
|
+
console.log("Facility decisions:", {
|
|
225
|
+
F1: result.values.open_F1,
|
|
226
|
+
F2: result.values.open_F2,
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Low-Level Control: Custom `BendersCut` Callback
|
|
231
|
+
|
|
232
|
+
You can also construct Benders cuts manually by returning `BendersCut` objects:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const result = await master.solveBenders({
|
|
236
|
+
gapTolerance: 1e-4,
|
|
237
|
+
subproblem: async () => {
|
|
238
|
+
const open1 = y1.value ?? 0;
|
|
239
|
+
const open2 = y2.value ?? 0;
|
|
240
|
+
const estTheta = theta.value ?? 0;
|
|
241
|
+
|
|
242
|
+
const sub = new Model("TransportSubproblem");
|
|
243
|
+
// ...
|
|
244
|
+
await sub.solve();
|
|
245
|
+
|
|
246
|
+
const actualCost = sub.objectiveValue!;
|
|
247
|
+
if (estTheta < actualCost - 1e-4) {
|
|
248
|
+
return {
|
|
249
|
+
kind: "optimality",
|
|
250
|
+
constraint: expr`${theta} - 50 * ${sub.solution!.duals.cap1} * ${y1} - 60 * ${sub.solution!.duals.cap2} * ${y2} >= ${rhs}`,
|
|
251
|
+
subproblemObjective: 100 * open1 + 120 * open2 + actualCost,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return null; // Converged
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Application Examples
|
|
262
|
+
|
|
263
|
+
Two common applications of decomposition methods:
|
|
264
|
+
|
|
265
|
+
### 1. Vehicle Routing Problem (CVRP) $\times$ Column Generation
|
|
266
|
+
*(Full executable source: [`examples/node/13-vehicle-routing-column-generation.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/13-vehicle-routing-column-generation.ts))*
|
|
267
|
+
|
|
268
|
+
* **Problem Structure**: Enumerating every feasible customer tour upfront produces a large number of variables.
|
|
269
|
+
* **Column Generation Formulation**:
|
|
270
|
+
* **Master Problem**: Set covering model that selects the minimum number of vehicle routes such that each customer is served.
|
|
271
|
+
* **Pricing Subproblem**: Knapsack model that dynamically generates a capacity-feasible route (column) with negative reduced cost based on customer dual shadow prices.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { Model, expr, minimize, LinExpr, type Var } from "@ohtaman/washi";
|
|
275
|
+
|
|
276
|
+
const vehicleCapacity = 8;
|
|
277
|
+
const customers = [
|
|
278
|
+
{ id: "C1", demand: 4 },
|
|
279
|
+
{ id: "C2", demand: 3 },
|
|
280
|
+
{ id: "C3", demand: 5 },
|
|
281
|
+
{ id: "C4", demand: 2 },
|
|
282
|
+
{ id: "C5", demand: 4 },
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
// 1. Master Problem: Customer coverage
|
|
286
|
+
const master = new Model("CVRP_Master");
|
|
287
|
+
const initialRoutes: Var[] = [];
|
|
288
|
+
for (const cust of customers) {
|
|
289
|
+
initialRoutes.push(master.integer(`init_${cust.id}`, { lb: 0, ub: 1 }));
|
|
290
|
+
}
|
|
291
|
+
master.objective(minimize(initialRoutes.reduce((acc, r) => acc.add(r), new LinExpr())));
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < customers.length; i++) {
|
|
294
|
+
master.constraint(expr`${initialRoutes[i]} >= 1`, `visit_${customers[i].id}`);
|
|
295
|
+
}
|
|
296
|
+
const visitConstrs = master.constraints;
|
|
297
|
+
|
|
298
|
+
// 2. Solve via Column Generation
|
|
299
|
+
const result = await master.solveColumnGeneration({
|
|
300
|
+
finalizeAsMIP: true,
|
|
301
|
+
pricing: async () => {
|
|
302
|
+
const sub = new Model("RoutePricing");
|
|
303
|
+
const a: Record<string, Var> = {};
|
|
304
|
+
const obj = new LinExpr();
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < customers.length; i++) {
|
|
307
|
+
const cust = customers[i];
|
|
308
|
+
a[cust.id] = sub.binary(`v_${cust.id}`);
|
|
309
|
+
obj.addTerm(a[cust.id], visitConstrs[i].dual ?? 0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
sub.objective(maximize(obj));
|
|
313
|
+
|
|
314
|
+
// Capacity limit: sum(demand_i * a_i) <= vehicleCapacity
|
|
315
|
+
const load = customers.reduce((acc, c) => acc.add(a[c.id].mul(c.demand)), new LinExpr());
|
|
316
|
+
sub.constraint(expr`${load} <= ${vehicleCapacity}`);
|
|
317
|
+
|
|
318
|
+
await sub.solve();
|
|
319
|
+
|
|
320
|
+
const coefficients: Record<string, number> = {};
|
|
321
|
+
for (const cust of customers) {
|
|
322
|
+
coefficients[`visit_${cust.id}`] = Math.round(a[cust.id].value ?? 0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
cost: 1,
|
|
327
|
+
type: "Integer",
|
|
328
|
+
coefficients,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
console.log("Optimal fleet size:", result.objectiveValue); // 3 vehicles
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 2. Traveling Salesperson Problem (TSP) $\times$ Benders Decomposition (Lazy Subtour Elimination)
|
|
337
|
+
*(Full executable source: [`examples/node/14-traveling-salesperson-benders.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts))*
|
|
338
|
+
|
|
339
|
+
* **Problem Structure**: Preventing isolated sub-loops (subtours) requires Subtour Elimination Constraints (SECs), which grow as $O(2^n)$ constraints for $n$ cities.
|
|
340
|
+
* **Benders Decomposition Formulation**:
|
|
341
|
+
* **Master Problem**: Solves the relaxed assignment problem (each city has 1 incoming and 1 outgoing edge).
|
|
342
|
+
* **Subproblem**: Evaluates the candidate tour graph for disconnected components. If isolated subtours exist, it lazily injects a Benders feasibility cut ($\sum_{i,j \in S} x_{ij} \le |S| - 1$) cutting off that cycle.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { Model, expr, minimize, LinExpr, type Var } from "@ohtaman/washi";
|
|
346
|
+
|
|
347
|
+
const n = cities.length;
|
|
348
|
+
const master = new Model("TSP_Master");
|
|
349
|
+
const x: Record<string, Var> = {};
|
|
350
|
+
const objExpr = new LinExpr();
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < n; i++) {
|
|
353
|
+
for (let j = 0; j < n; j++) {
|
|
354
|
+
if (i !== j) {
|
|
355
|
+
const v = master.binary(`x_${i}_${j}`);
|
|
356
|
+
x[`${i}_${j}`] = v;
|
|
357
|
+
objExpr.addTerm(v, dist(i, j));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
master.objective(minimize(objExpr));
|
|
362
|
+
|
|
363
|
+
// Degree constraints
|
|
364
|
+
for (let i = 0; i < n; i++) {
|
|
365
|
+
const outE = new LinExpr();
|
|
366
|
+
const inE = new LinExpr();
|
|
367
|
+
for (let j = 0; j < n; j++) {
|
|
368
|
+
if (i !== j) {
|
|
369
|
+
outE.addTerm(x[`${i}_${j}`], 1);
|
|
370
|
+
inE.addTerm(x[`${j}_${i}`], 1);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
master.constraint(expr`${outE} == 1`, `out_${i}`);
|
|
374
|
+
master.constraint(expr`${inE} == 1`, `in_${i}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Solve with Benders lazy subtour elimination
|
|
378
|
+
const result = await master.solveBenders({
|
|
379
|
+
subproblem: () => {
|
|
380
|
+
const cycles = findCycles(n, (i, j) => (x[`${i}_${j}`].value ?? 0) > 0.5);
|
|
381
|
+
if (cycles.length === 1 && cycles[0].length === n) {
|
|
382
|
+
return null; // Single tour covers all cities
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Subtour detected: inject Benders feasibility cut for smallest cycle
|
|
386
|
+
const S = cycles.reduce((min, c) => (c.length < min.length ? c : min), cycles[0]);
|
|
387
|
+
const secExpr = new LinExpr();
|
|
388
|
+
for (const i of S) {
|
|
389
|
+
for (const j of S) {
|
|
390
|
+
if (i !== j) secExpr.addTerm(x[`${i}_${j}`], 1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
kind: "feasibility",
|
|
395
|
+
constraint: expr`${secExpr} <= ${S.length - 1}`,
|
|
396
|
+
};
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
```
|
package/docs/guide/unsat-core.md
CHANGED
|
@@ -42,16 +42,23 @@ Notice that `routine_operating_cost` is **not** in the core, because only `minim
|
|
|
42
42
|
> - **Use `m.check({ solver: "z3" })` for Unsat Core extraction**: `m.solve()` runs optimization (`Z3.Optimize`), whereas `m.check()` runs tracked satisfiability checking (`Z3.Solver.assertAndTrack`) and populates `result.conflicts` / `m.conflicts` (and alias `m.unsatCore`).
|
|
43
43
|
> - **Name your constraints**: Variable bounds (`lb`, `ub`) act as background domain declarations. To include a bound or business rule in the reported `unsatCore` list, register it as a named constraint via `m.constraint(expr`...`, "rule_name")`.
|
|
44
44
|
|
|
45
|
-
### Workflow:
|
|
45
|
+
### Workflow: Infeasibility Diagnosis with HiGHS or Z3
|
|
46
46
|
|
|
47
|
-
Because WaSHI models are solver-independent, you can
|
|
47
|
+
Because WaSHI models are solver-independent, you can diagnose infeasibilities using either solver:
|
|
48
|
+
1. **Built-in HiGHS & GLPK (default)**: `await m.check()` uses a single-solve elastic slack formulation ($\min \sum s_i$) to identify which constraints required relaxation (`diag.conflicts`), requiring no extra dependencies.
|
|
49
|
+
2. **Z3 SMT Solver**: `await m.check({ solver: "z3" })` uses SMT tracking to extract an exact unsatisfiable core (`diag.unsatCore` / `diag.conflicts`).
|
|
48
50
|
|
|
49
51
|
```typescript
|
|
50
52
|
const status = await m.solve(); // Fast MIP solve with default HiGHS
|
|
51
53
|
|
|
52
54
|
if (status === "Infeasible") {
|
|
53
|
-
|
|
55
|
+
// Option 1: Built-in single-solve elastic diagnosis with HiGHS
|
|
56
|
+
const diag = await m.check();
|
|
54
57
|
console.error("Model is infeasible due to conflicting rules:", diag.conflicts);
|
|
58
|
+
|
|
59
|
+
// Option 2: Exact SMT unsat core diagnosis with Z3 (requires import "@ohtaman/washi/solvers/z3")
|
|
60
|
+
// const z3Diag = await m.check({ solver: "z3" });
|
|
61
|
+
// console.error("Z3 Unsat Core:", z3Diag.unsatCore);
|
|
55
62
|
}
|
|
56
63
|
```
|
|
57
64
|
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
> **スコープに関する注記**: このドキュメントは、`@ohtaman/washi` を用いて数理最適化モデルや充足可能性(SAT/SMT)モデルを構築する AI コーディングエージェントおよび開発者向けの**利用者向けモデリング・クイックリファレンス**です。
|
|
4
4
|
> - **ポータブル Agent Skill (`SKILL.md`)**: モデリング・`toLpString()` による式検査・`validateSolution()` による解検証・Z3 `unsatCore` による矛盾診断までを一貫して実行するワークフロースキル(Claude Code, Antigravity, Cursor, Codex 等に対応)として、[`skills/washi/SKILL.md`](https://github.com/ohtaman/washi/blob/main/skills/washi/SKILL.md)(npm パッケージ内の `node_modules/@ohtaman/washi/skills/washi/SKILL.md` にも同梱)を利用できます:
|
|
5
5
|
> ```bash
|
|
6
|
-
> #
|
|
6
|
+
> # skills CLI (skills.sh / vercel-labs/skills) 経由で導入
|
|
7
|
+
> npx skills add ohtaman/washi
|
|
8
|
+
>
|
|
9
|
+
> # またはパッケージ同梱のスキルをプロジェクトのエージェントスキルディレクトリへ配置
|
|
7
10
|
> mkdir -p .agents/skills/washi
|
|
8
11
|
> cp node_modules/@ohtaman/washi/skills/washi/SKILL.md .agents/skills/washi/SKILL.md
|
|
9
12
|
> ```
|
|
@@ -132,9 +135,10 @@ m.indicator(b, expr`${x} <= 50`, { activeValue: 1, name: "cap_when_b" });
|
|
|
132
135
|
- `SolverStatus`(`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`, `"Sat"`, または `"Unsat"`)を返します。組み込みの最適化ソルバーは最適化ステータス(`"Optimal"`, `"Feasible"`, `"Infeasible"`, `"Unbounded"`, `"TimeLimit"`, `"IterationLimit"`, `"Error"`, `"Unknown"`, `"NotSolved"`)を返し、時間・反復・解数制限や中断で停止した際に有効な実行可能解(Incumbent)が見つかっている場合は `"Feasible"` を返します。`"Sat"` と `"Unsat"` は充足可能性ソルバーやカスタムソルバーアダプター(`MockSolver` など)向けに `SolverStatus` に含まれています。
|
|
133
136
|
- 解の値は `x.value`, `expr`...`.value`, `m.getValues(x)`, `m.getValues(container)`, `m.objectiveValue`, `constr.dual`, `constr.slack`, `x.reducedCost` から読み取ります。
|
|
134
137
|
- 求解後に `m.validateSolution()` を呼び出すことで、変数の有限性、境界条件(`lb`/`ub`)、整数・バイナリ・真偽値領域、線形制約の残差、および論理制約を独立して検証できます(`SolutionValidationReport`: `{ valid, complete, maxViolation, violations, ... }`)。なお、`validateSolution()` は変数および制約条件の主実行可能性を検査するものであり、`m.objectiveValue` の再計算や比較は行いません。
|
|
135
|
-
-
|
|
136
|
-
- `CheckResult`(`{ status: "Sat" | "Unsat" | "Unknown", values: Record<string, number | boolean>, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }`)を返します。`conflicts`
|
|
137
|
-
-
|
|
138
|
+
- **充足可能性判定と矛盾診断 (`await m.check(options?)`)**:
|
|
139
|
+
- `CheckResult`(`{ status: "Sat" | "Unsat" | "Unknown", values: Record<string, number | boolean>, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }`)を返します。`conflicts` はモデルが `"Unsat"` と証明されたときに設定されます。
|
|
140
|
+
- 線形算術制約または論理制約(`BoolVar`, `and`, `or`, `implies`, `xor`)を含むモデルでは、`m.check()` はデフォルトの HiGHS や GLPK を含む任意のソルバーで動作します(単一 solve の弾性スラック緩和を用い、論理制約は 0-1 MIP へ自動線形化)。矛盾時は緩和が必要な制約名を `conflicts` として診断します(意味論の厳密さのため `unsatCore: undefined`)。
|
|
141
|
+
- 厳密な SMT Unsat Core 抽出(`result.unsatCore` および `result.conflicts`)は SMT 対応ソルバー(`{ solver: "z3" }`)使用時に提供されます。Z3 は登録された制約を追跡しつつ、変数の上下限(`lb`/`ub`)およびバイナリ領域 `{0, 1}` を直接アサートするため、`unsatCore` は*モデルの固定された変数境界・領域と合わせたときに*矛盾する登録済み制約名の部分集合を返します(最小性は保証されません)。
|
|
138
142
|
- **非同期 `solve` / `check` 実行中のモデル変更禁止**:
|
|
139
143
|
- `await m.solve()` または `await m.check()` の完了待機中に変数・制約・目的関数を変更すると、進行中の実行は無効化され、古い Promise は reject されます。
|
|
140
144
|
|
|
@@ -119,7 +119,7 @@ export interface SolveModelData extends CheckModelData {
|
|
|
119
119
|
|
|
120
120
|
Custom solvers declare their supported features using `capabilities`. Before dispatching `solve()` or `check()`, `Model` validates:
|
|
121
121
|
- `supportsQP === false` when the objective contains quadratic terms (`QuadExpr`),
|
|
122
|
-
-
|
|
122
|
+
- when the model contains logical constraints (`m.logicalConstraints`) and the solver does not support SMT (`supportsSMT !== true`), `Model` automatically linearizes them into equivalent 0-1 MIP binary variables and linear constraints via `linearizeModelForMip`, and
|
|
123
123
|
- `supportsWarmStart !== true` when any variable has a primal `initialValue` set.
|
|
124
124
|
|
|
125
125
|
```typescript
|
package/docs/solvers/highs.md
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
# HiGHS Solver Engine
|
|
2
2
|
|
|
3
|
-
**HiGHS** is high-performance mathematical optimization software developed at the University of Edinburgh. It is widely recognized as the state-of-the-art open-source solver for Linear Programming (LP), Mixed-Integer Programming (MIP), and Convex Quadratic Programming (QP).
|
|
3
|
+
**HiGHS** is high-performance mathematical optimization software developed at the University of Edinburgh ([ERGO-Code/HiGHS](https://github.com/ERGO-Code/HiGHS)). It is widely recognized as the state-of-the-art open-source solver for Linear Programming (LP), Mixed-Integer Programming (MIP), and Convex Quadratic Programming (QP).
|
|
4
|
+
|
|
5
|
+
In `@ohtaman/washi`, HiGHS runs entirely in WebAssembly compiled directly from official upstream source ([ERGO-Code/HiGHS](https://github.com/ERGO-Code/HiGHS)) using its native C API (`highs_c_api.h`), providing zero-config execution across Node.js, Web Workers, and browsers without third-party wrapper dependencies.
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Key Features in WaSHI
|
|
8
10
|
|
|
9
|
-
- **Built-in & Zero-Config**: Bundled directly with `@ohtaman/washi
|
|
11
|
+
- **Built-in & Zero-Config**: Bundled directly with `@ohtaman/washi` with zero external runtime dependencies. No separate peer dependency or C++ compiler required.
|
|
10
12
|
- **Permissive MIT License**: 100% commercially friendly.
|
|
11
13
|
- **Web Worker Multithreading**: Automatically offloads intensive solving to background Web Workers in browser environments to avoid freezing the UI thread.
|
|
12
14
|
- **Warm-Start Support (`initialValue`)**: Supports passing variable primal initial value hints (`initialValue`) to HiGHS `setSolution` for warm starting.
|
|
13
|
-
- **Fast WASM Execution**:
|
|
15
|
+
- **Fast WASM Execution**: Runs the directly compiled 32-bit WebAssembly binary at near-native speed.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Architecture & WebAssembly Runtime
|
|
20
|
+
|
|
21
|
+
WaSHI compiles official HiGHS C++ source directly to WebAssembly (`highs.wasm`) with `-fwasm-exceptions` and single-threaded universal portability (`-DHIGHS_NO_DEFAULT_THREADS=ON`). By interfacing directly with HiGHS's native C API (`highs_c_api.h`), WaSHI achieves zero-copy solution extraction via typed array views on WebAssembly linear memory, rapid upstream release tracking, and a self-contained zero-dependency distribution.
|
|
22
|
+
|
|
23
|
+
### Callbacks & Solver Interactivity
|
|
24
|
+
|
|
25
|
+
- **Progress Monitoring & Cancellation (Supported)**:
|
|
26
|
+
Through HiGHS's internal logging and interrupt hooks, WaSHI provides real-time progress callbacks (`onProgress`) and cooperative mid-solve cancellation via standard `AbortSignal`.
|
|
27
|
+
- **Dynamic Cuts / Lazy Constraints (Not Supported in WASM Wrapper)**:
|
|
28
|
+
The upstream WebAssembly wrapper intentionally excludes low-level native callback type 8 (`MIP lazy constraints`) due to WASM-to-JS boundary and synchronous memory safety constraints. For problems requiring subtour elimination or Benders decomposition, use an **iterative cutting-plane loop** (solve $\to$ detect violation $\to$ `model.constraint(...)` $\to$ re-solve).
|
|
29
|
+
|
|
30
|
+
### WebAssembly Asset Lifecycle & On-Demand Builds
|
|
31
|
+
|
|
32
|
+
HiGHS's WebAssembly binary (`public/highs.wasm`) and runtime driver (`runtime.js`) are managed via `highs.config.json` with cryptographic SHA-256 verification:
|
|
33
|
+
- **Zero-Friction Verification**: Build and distribution pipelines run `node scripts/ensure-highs-wasm.mjs`, verifying local assets in milliseconds before bundling.
|
|
34
|
+
- **On-Demand Release Download**: When assets are missing or updated, `ensure-highs-wasm.mjs` automatically downloads verified binaries from official GitHub Release assets.
|
|
35
|
+
- **Compiling from Source**: When updating HiGHS or modifying compiler flags, run `npm run build:highs-wasm` with the Emscripten SDK (`emcc`), CMake, and Ninja installed, and upload the rebuilt assets to the `highs-<version>` GitHub Release tag (`gh release upload highs-v1.15.1 public/highs.wasm src/modeler/solvers/highs/runtime.js --clobber`) so on-demand downloads match `highs.config.json` (or run `.github/workflows/build-wasm.yml` and commit the `highs-wasm-assets` artifact including `highs.config.json`).
|
|
14
36
|
|
|
15
37
|
---
|
|
16
38
|
|
package/docs/solvers/index.md
CHANGED
|
@@ -8,9 +8,9 @@ WaSHI is designed around a pluggable, decoupled solver architecture. You formula
|
|
|
8
8
|
|
|
9
9
|
| Solver | Engine License | Problem Classes | Best For | Status in WaSHI |
|
|
10
10
|
| :--- | :--- | :--- | :--- | :--- |
|
|
11
|
-
| **HiGHS** | **MIT** | **LP, MIP, Convex QP** | Industrial-scale optimization, operations research, finance | **Built-in default** (zero configuration) |
|
|
12
|
-
| **Z3** | **MIT** | **SMT, SAT, Logic, MIP** | Propositional logic, conflict diagnosis (`unsatCore`), rule engines | Optional plugin (`@ohtaman/washi/solvers/z3`) |
|
|
13
|
-
| **GLPK** | **GPL-3.0** | **LP, MIP** | Academic benchmarks, legacy GLPK compatibility | Optional plugin (`@ohtaman/washi/solvers/glpk`) |
|
|
11
|
+
| **[HiGHS](https://github.com/ERGO-Code/HiGHS)** | **MIT** | **LP, MIP, Convex QP** | Industrial-scale optimization, operations research, finance | **Built-in default** (zero configuration) |
|
|
12
|
+
| **[Z3](https://github.com/Z3Prover/z3)** (via [`z3-solver`](https://www.npmjs.com/package/z3-solver)) | **MIT** | **SMT, SAT, Logic, MIP** | Propositional logic, conflict diagnosis (`unsatCore`), rule engines | Optional plugin (`@ohtaman/washi/solvers/z3`) |
|
|
13
|
+
| **[GLPK](https://www.gnu.org/software/glpk/)** (via [`glpk.js`](https://www.npmjs.com/package/glpk.js)) | **GPL-3.0** | **LP, MIP** | Academic benchmarks, legacy GLPK compatibility | Optional plugin (`@ohtaman/washi/solvers/glpk`) |
|
|
14
14
|
| **Mock** | **MIT** | *Any* | Ultra-fast unit testing in CI without loading WebAssembly | Built-in |
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
> **范围说明**:本文档是面向使用 `@ohtaman/washi` 构建数理优化或可满足性(SAT/SMT)模型的 AI 编程助手与开发者的**使用端建模快速参考**。
|
|
4
4
|
> - **可移植 Agent Skill (`SKILL.md`)**:如需涵盖端到端建模、`toLpString()` 导出检查、`validateSolution()` 解验证以及 Z3 `unsatCore` 冲突诊断的完整工作流技能(兼容 Claude Code、Antigravity、Cursor、Codex 等支持 `SKILL.md` 的代理),请使用 [`skills/washi/SKILL.md`](https://github.com/ohtaman/washi/blob/main/skills/washi/SKILL.md)(同时随 npm 包分发于 `node_modules/@ohtaman/washi/skills/washi/SKILL.md`):
|
|
5
5
|
> ```bash
|
|
6
|
-
> #
|
|
6
|
+
> # 通过 skills CLI (skills.sh / vercel-labs/skills) 直接安装
|
|
7
|
+
> npx skills add ohtaman/washi
|
|
8
|
+
>
|
|
9
|
+
> # 或将随包附带的技能复制到项目的代理技能目录中
|
|
7
10
|
> mkdir -p .agents/skills/washi
|
|
8
11
|
> cp node_modules/@ohtaman/washi/skills/washi/SKILL.md .agents/skills/washi/SKILL.md
|
|
9
12
|
> ```
|
|
@@ -132,9 +135,10 @@ m.indicator(b, expr`${x} <= 50`, { activeValue: 1, name: "cap_when_b" });
|
|
|
132
135
|
- 返回 `SolverStatus`(`"Optimal"`、`"Feasible"`、`"Infeasible"`、`"Unbounded"`、`"TimeLimit"`、`"IterationLimit"`、`"Error"`、`"Unknown"`、`"NotSolved"`、`"Sat"` 或 `"Unsat"`)。内置优化求解器返回优化状态(`"Optimal"`、`"Feasible"`、`"Infeasible"`、`"Unbounded"`、`"TimeLimit"`、`"IterationLimit"`、`"Error"`、`"Unknown"`、`"NotSolved"`),其中因时间、迭代或解数量上限(或中断)停止但已找到合法可行解(Incumbent)的运行会返回 `"Feasible"`;`"Sat"` 与 `"Unsat"` 包含在 `SolverStatus` 中以供可满足性/自定义求解器适配器(如 `MockSolver`)使用。
|
|
133
136
|
- 通过 `x.value`、`expr`...`.value`、`m.getValues(x)`、`m.getValues(container)`、`m.objectiveValue`、`constr.dual`、`constr.slack` 和 `x.reducedCost` 读取结果。
|
|
134
137
|
- 求解后调用 `m.validateSolution()` 可独立校验变量有限性、变量上下界(`lb`/`ub`)、整数/二元/布尔域、线性约束残差以及逻辑约束(`SolutionValidationReport`:`{ valid, complete, maxViolation, violations, ... }`)。注意:`validateSolution()` 校验变量与约束的原问题可行性,不会重新计算或比对 `m.objectiveValue`。
|
|
135
|
-
-
|
|
136
|
-
- 返回 `CheckResult`(`{ status: "Sat" | "Unsat" | "Unknown", values: Record<string, number | boolean>, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }
|
|
137
|
-
-
|
|
138
|
+
- **可满足性检查与冲突诊断 (`await m.check(options?)`)**:
|
|
139
|
+
- 返回 `CheckResult`(`{ status: "Sat" | "Unsat" | "Unknown", values: Record<string, number | boolean>, conflicts?: readonly string[], unsatCore?: readonly string[], runTime?: number }`),其中在模型证明为 `"Unsat"` 时填充 `conflicts`。
|
|
140
|
+
- 对于包含线性算术约束或逻辑约束(`BoolVar`、`and`、`or`、`implies`、`xor`)的模型,`m.check()` 均可在任意求解器(包括默认的 HiGHS 与 GLPK,通过单次求解弹性松弛模型,并将逻辑约束自动线性化为 0-1 MIP)上运行,并在不可行时将需要松弛的冲突约束诊断为 `conflicts`(为保证语义严谨,`unsatCore: undefined`)。
|
|
141
|
+
- 精确的 SMT 不满足核提取(`result.unsatCore` 与 `result.conflicts`)在选用 SMT 求解器(`{ solver: "z3" }`)时提供。由于 Z3 在跟踪已注册约束的同时直接断言变量边界(`lb`/`ub`)和二元 `{0, 1}` 域,`unsatCore` 报告的是*结合模型固定变量边界/域时*发生冲突的已注册约束名称子集(不保证最小性)。
|
|
138
142
|
- **异步 `solve` / `check` 期间禁止并发修改模型**:
|
|
139
143
|
- 在 `await m.solve()` 或 `await m.check()` 挂起期间修改变量、约束或目标函数会使当前执行失效,并使过期的 Promise 被 reject。
|
|
140
144
|
|