@ohtaman/washi 0.6.0 → 0.7.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 +103 -421
- package/README.md +103 -421
- package/README.zh-CN.md +103 -421
- package/THIRD_PARTY_LICENSES.md +5 -3
- package/dist/browser/solvers/glpk.js +2 -2
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/washi.js +777 -261
- package/dist/browser/washi.min.js +3 -3
- package/dist/lib/{registry-C20sxTwm.js → registry-D7TZ3Wh_.js} +88 -18
- package/dist/lib/solvers/glpk.js +3 -3
- package/dist/lib/solvers/z3.js +1 -1
- package/dist/lib/washi.js +864 -421
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +47 -26
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +2 -2
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +12 -6
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/parser.d.ts +25 -5
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/worker-client.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/docs/examples/diet-sensitivity.md +15 -14
- package/docs/examples/ecommerce-coupons.md +15 -22
- package/docs/examples/index.md +14 -12
- package/docs/examples/knapsack.md +8 -7
- package/docs/examples/portfolio-qp.md +11 -4
- package/docs/examples/production.md +4 -4
- package/docs/examples/shift-scheduling.md +20 -7
- package/docs/examples/smt-security.md +16 -4
- package/docs/examples/transportation.md +46 -19
- package/docs/examples/traveling-salesperson.md +28 -44
- package/docs/examples/unsat-core.md +15 -5
- package/docs/examples/vehicle-routing.md +10 -8
- package/docs/getting-started/ai-agents.md +25 -14
- package/docs/getting-started/cross-origin.md +56 -28
- package/docs/getting-started/index.md +40 -69
- package/docs/getting-started/runtimes.md +66 -61
- package/docs/guide/constraints.md +46 -40
- package/docs/guide/decomposition.md +127 -259
- package/docs/guide/expressions.md +90 -39
- package/docs/guide/formulation-tips.md +212 -0
- package/docs/guide/objectives.md +7 -4
- package/docs/guide/unsat-core.md +18 -6
- package/docs/guide/variables.md +33 -38
- package/docs/ja/getting-started/ai-agents.md +24 -14
- package/docs/solvers/highs.md +27 -6
- package/docs/solvers/index.md +1 -1
- package/docs/solvers/z3.md +3 -0
- package/docs/zh/getting-started/ai-agents.md +24 -14
- package/llms-full.txt +929 -687
- package/llms.txt +21 -19
- package/package.json +3 -3
- package/skills/washi/SKILL.md +14 -10
package/docs/examples/index.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Examples & API Finder
|
|
2
2
|
|
|
3
|
-
Explore runnable optimization and logic formulations built with WaSHI. Every example can be executed directly inside its documentation page, or edited in the [Playground](https://ohtaman.
|
|
3
|
+
Explore runnable optimization and logic formulations built with WaSHI. Every example can be executed directly inside its documentation page, or edited in the [Playground](https://washi.ohtaman.dev/demo/).
|
|
4
4
|
|
|
5
5
|
## Examples
|
|
6
6
|
|
|
7
7
|
| Example | Problem Class | Solver | Key WaSHI APIs | Description | Code Editor |
|
|
8
8
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
9
|
-
| **[Production Planning](./production.md)** | LP / MIP | HiGHS | `m.integer()`, `m.constraint()`, `expr` | Basic variables, tagged template literal equations, and maximizing profit under resource limits. | [Playground](https://ohtaman.
|
|
10
|
-
| **[0-1 Knapsack](./knapsack.md)** | Binary MIP | HiGHS | `m.binary(keys)`, `
|
|
11
|
-
| **[Transportation Network](./transportation.md)** | LP | HiGHS | `m.continuous(rows, cols)`, `
|
|
12
|
-
| **[Diet & Sensitivity Analysis](./diet-sensitivity.md)** | LP | HiGHS | `constr.dual`, `constr.slack`, `var.reducedCost` | Reading shadow prices (bottlenecks), constraint surpluses, and reduced costs after solving. | [Playground](https://ohtaman.
|
|
13
|
-
| **[Staff Shift Scheduling](./shift-scheduling.md)** | Binary MIP + Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `check()`, `unsatCore` | 5×7 weekly roster planner (coverage, senior duty, max shifts, day-off requests) and diagnosing conflicting vacation requests with Z3 `unsatCore`. | [Playground](https://ohtaman.
|
|
14
|
-
| **[E-Commerce Coupon Optimizer](./ecommerce-coupons.md)** | Indicator MIP | HiGHS | `m.indicator()`, `sum
|
|
15
|
-
| **[Portfolio Optimization](./portfolio-qp.md)** | Convex QP | HiGHS | `m.continuous()`, `m.constraint()`, `dot()`, `QuadExpr` | Markowitz mean-variance risk minimization ($w^T \Sigma w$) using `QuadExpr` subject to return targets. | [Playground](https://ohtaman.
|
|
16
|
-
| **[SMT Security Policies](./smt-security.md)** | SMT Logic | Z3 | `m.bool()`, `m.constraint()`, `m.check()` | Propositional variables (`BoolVar`) and logical connectives (`implies`, `and`, `not`, `xor`) mixed with integer bounds. | [Playground](https://ohtaman.
|
|
17
|
-
| **[Conflict Diagnosis (`unsatCore`)](./unsat-core.md)** | Unsat Core | Z3 | `m.constraint()`, `m.check()`, `m.unsatCore` | Extracting a conflicting constraint subset (`unsatCore`) when staffing and budget rules contradict each other. | [Playground](https://ohtaman.
|
|
9
|
+
| **[Production Planning](./production.md)** | LP / MIP | HiGHS | `m.integer()`, `m.constraint()`, `expr` | Basic variables, tagged template literal equations, and maximizing profit under resource limits. | [Playground](https://washi.ohtaman.dev/demo/?preset=production) |
|
|
10
|
+
| **[0-1 Knapsack](./knapsack.md)** | Binary MIP | HiGHS | `m.binary(keys)`, `expr` (`dot`), `m.constraint()` | 1D dictionary of `Binary` variables and linear aggregation over item arrays. | [Playground](https://washi.ohtaman.dev/demo/?preset=knapsack) |
|
|
11
|
+
| **[Transportation Network](./transportation.md)** | LP | HiGHS | `m.continuous(rows, cols)`, `expr` (`sum`, `dot`), `m.constraint()` | 2D matrix indexing (`ship[factory][store]`) and row/column flow balance constraints. | [Playground](https://washi.ohtaman.dev/demo/?preset=transportation) |
|
|
12
|
+
| **[Diet & Sensitivity Analysis](./diet-sensitivity.md)** | LP | HiGHS | `constr.dual`, `constr.slack`, `var.reducedCost` | Reading shadow prices (bottlenecks), constraint surpluses, and reduced costs after solving. | [Playground](https://washi.ohtaman.dev/demo/?preset=diet_sensitivity) |
|
|
13
|
+
| **[Staff Shift Scheduling](./shift-scheduling.md)** | Binary MIP + Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `check()`, `unsatCore` | 5×7 weekly roster planner (coverage, senior duty, max shifts, day-off requests) and diagnosing conflicting vacation requests with Z3 `unsatCore`. | [Playground](https://washi.ohtaman.dev/demo/?preset=shift_scheduling) |
|
|
14
|
+
| **[E-Commerce Coupon Optimizer](./ecommerce-coupons.md)** | Indicator MIP | HiGHS | `m.indicator()`, `expr` (`sum`, `dot`), `m.constraint()` | Checkout optimizer using `m.indicator()` for minimum spend thresholds, exclusive vs. stackable rules, and smart filler add-on suggestions. | [Playground](https://washi.ohtaman.dev/demo/?preset=cart_coupon) |
|
|
15
|
+
| **[Portfolio Optimization](./portfolio-qp.md)** | Convex QP | HiGHS | `m.continuous()`, `m.constraint()`, `dot()`, `QuadExpr` | Markowitz mean-variance risk minimization ($w^T \Sigma w$) using `QuadExpr` subject to return targets. | [Playground](https://washi.ohtaman.dev/demo/?preset=portfolio_qp) |
|
|
16
|
+
| **[SMT Security Policies](./smt-security.md)** | SMT Logic | Z3 | `m.bool()`, `m.constraint()`, `m.check()` | Propositional variables (`BoolVar`) and logical connectives (`implies`, `and`, `not`, `xor`) mixed with integer bounds. | [Playground](https://washi.ohtaman.dev/demo/?preset=smt_security) |
|
|
17
|
+
| **[Conflict Diagnosis (`unsatCore`)](./unsat-core.md)** | Unsat Core | Z3 | `m.constraint()`, `m.check()`, `m.unsatCore` | Extracting a conflicting constraint subset (`unsatCore`) when staffing and budget rules contradict each other. | [Playground](https://washi.ohtaman.dev/demo/?preset=conflict_diagnosis_unsat) |
|
|
18
18
|
| **[Vehicle Routing (CVRP)](./vehicle-routing.md)** | Combinatorial OR / Dantzig-Wolfe | HiGHS | `m.solveColumnGeneration()`, `pricing` | Dynamic route column generation from customer dual shadow prices to minimize total fleet vehicles. | [Decomposition Guide](../guide/decomposition.md) |
|
|
19
19
|
| **[Traveling Salesperson (TSP)](./traveling-salesperson.md)** | Combinatorial OR / Benders | HiGHS | `m.solveBenders()`, `subproblem` | Subtour elimination on degree-assignment relaxation using dynamic Benders feasibility cuts. | [Decomposition Guide](../guide/decomposition.md) |
|
|
20
20
|
|
|
@@ -27,12 +27,12 @@ Looking for a specific modeling method or solver capability? Jump straight to th
|
|
|
27
27
|
| ``m.constraint(expr`...`)`` | Tagged template literal constraints | [Production Planning](./production.md) | [`01-production-planning.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/01-production-planning.ts) |
|
|
28
28
|
| `m.binary(keys)` / `m.continuous(keys)` | 1D dictionary of indexed variables | [0-1 Knapsack](./knapsack.md) | [`02-knapsack-problem.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/02-knapsack-problem.ts) |
|
|
29
29
|
| `m.continuous(rows, cols)` / `m.binary(rows, cols)` | 2D matrix of indexed variables | [Transportation Network](./transportation.md), [Shift Scheduling](./shift-scheduling.md) | [`09-transportation-problem.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts) |
|
|
30
|
-
| `dot(
|
|
30
|
+
| ``expr`dot(${A}, ${X})` `` / `dot(A, X)` | Fast vector/matrix dot product | [Transportation Network](./transportation.md), [Portfolio Optimization](./portfolio-qp.md) | [`09-transportation-problem.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts) |
|
|
31
31
|
| `constr.dual` & `constr.slack` | Shadow prices & constraint surpluses | [Diet & Sensitivity Analysis](./diet-sensitivity.md) | [`10-diet-sensitivity.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/10-diet-sensitivity.ts) |
|
|
32
32
|
| `var.reducedCost` | Variable reduced costs in LP | [Diet & Sensitivity Analysis](./diet-sensitivity.md) | [`10-diet-sensitivity.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/10-diet-sensitivity.ts) |
|
|
33
33
|
| `m.indicator()` | Big-M conditional constraints (`binVar => constr`) | [E-Commerce Coupon Optimizer](./ecommerce-coupons.md) | [`12-ecommerce-coupon-optimizer.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/12-ecommerce-coupon-optimizer.ts) |
|
|
34
34
|
| `new QuadExpr()` | Convex quadratic objectives ($x^T Q x$) | [Portfolio Optimization](./portfolio-qp.md) | [`03-portfolio-qp.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/03-portfolio-qp.ts) |
|
|
35
|
-
| `m.bool()` &
|
|
35
|
+
| `m.bool()` & `implies()` | Propositional logic & SMT rules | [SMT Security Policies](./smt-security.md) | [`04-smt-security-rules.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts) |
|
|
36
36
|
| `m.check()` & `m.unsatCore` | Feasibility check & conflict diagnosis | [Conflict Diagnosis](./unsat-core.md), [Shift Scheduling](./shift-scheduling.md) | [`05-unsat-core-diagnosis.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/05-unsat-core-diagnosis.ts) |
|
|
37
37
|
| `m.solveColumnGeneration()` | Dantzig-Wolfe Column Generation | [Vehicle Routing](./vehicle-routing.md) | [`13-vehicle-routing-column-generation.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/13-vehicle-routing-column-generation.ts) |
|
|
38
38
|
| `m.solveBenders()` | Benders Decomposition & cut separation | [Traveling Salesperson](./traveling-salesperson.md) | [`14-traveling-salesperson-benders.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts) |
|
|
@@ -54,4 +54,6 @@ npx tsx examples/node/09-transportation-problem.ts
|
|
|
54
54
|
npx tsx examples/node/10-diet-sensitivity.ts
|
|
55
55
|
npx tsx examples/node/11-shift-scheduling.ts
|
|
56
56
|
npx tsx examples/node/12-ecommerce-coupon-optimizer.ts
|
|
57
|
+
npx tsx examples/node/13-vehicle-routing-column-generation.ts
|
|
58
|
+
npx tsx examples/node/14-traveling-salesperson-benders.ts
|
|
57
59
|
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| 0-1 Binary MIP | HiGHS (default) | `m.binary()`, `m.constraint()`, `expr
|
|
5
|
+
| 0-1 Binary MIP | HiGHS (default) | `m.binary()`, `m.constraint()`, `expr` (`dot`), `maximize` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=knapsack">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/02-knapsack-problem.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
The 0-1 Knapsack problem is an essential combinatorial optimization problem: given a set of items, each with a weight and a value, determine which items to include in a collection so that the total weight does not exceed a given limit and the total value is maximized.
|
|
8
8
|
|
|
@@ -21,7 +21,7 @@ $$\begin{aligned}
|
|
|
21
21
|
<CodeRunner preset="knapsack">
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import { Model, maximize,
|
|
24
|
+
import { Model, maximize, expr } from "@ohtaman/washi";
|
|
25
25
|
|
|
26
26
|
async function solveKnapsack() {
|
|
27
27
|
const m = new Model("Knapsack");
|
|
@@ -41,17 +41,18 @@ async function solveKnapsack() {
|
|
|
41
41
|
const values = Object.fromEntries(items.map((it) => [it.name, it.value]));
|
|
42
42
|
const weights = Object.fromEntries(items.map((it) => [it.name, it.weight]));
|
|
43
43
|
|
|
44
|
-
// Maximize total value via dot
|
|
45
|
-
m.objective(maximize(dot(values, x)));
|
|
44
|
+
// Maximize total value via dot() inside expr
|
|
45
|
+
m.objective(maximize(expr`dot(${values}, ${x})`));
|
|
46
46
|
|
|
47
|
-
// Weight capacity constraint using
|
|
48
|
-
m.constraint(expr
|
|
47
|
+
// Weight capacity constraint using expr and constraint()
|
|
48
|
+
m.constraint(expr`dot(${weights}, ${x}) <= ${maxCapacity}`, "capacity_limit");
|
|
49
49
|
|
|
50
50
|
const status = await m.solve();
|
|
51
51
|
|
|
52
52
|
console.log(`Status: ${status}`);
|
|
53
53
|
console.log(`Max Total Value: $${m.objectiveValue}`);
|
|
54
|
-
|
|
54
|
+
const packed = items.filter((it) => x[it.name].value === 1).map((it) => it.name);
|
|
55
|
+
console.log("Packed Items:", packed.join(", "));
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
solveKnapsack();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Convex Quadratic Programming (QP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot`, `QuadExpr` | <ul><li><a href="https://ohtaman.
|
|
5
|
+
| Convex Quadratic Programming (QP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot`, `QuadExpr` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=portfolio_qp">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/03-portfolio-qp.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
The Markowitz mean-variance portfolio optimization problem is the foundation of modern quantitative finance: allocate capital across risky assets to minimize overall portfolio variance (risk) subject to achieving a minimum required expected return.
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ where $\boldsymbol{\Sigma}$ is the positive semi-definite asset return covarianc
|
|
|
26
26
|
<CodeRunner preset="portfolio_qp">
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
|
-
import { Model, QuadExpr,
|
|
29
|
+
import { Model, QuadExpr, dot, minimize, expr } from "@ohtaman/washi";
|
|
30
30
|
|
|
31
31
|
async function solvePortfolio() {
|
|
32
32
|
const m = new Model("PortfolioQP");
|
|
@@ -48,11 +48,12 @@ async function solvePortfolio() {
|
|
|
48
48
|
const w = m.continuous(assets, { lb: 0, ub: 1 });
|
|
49
49
|
|
|
50
50
|
// 1. Budget constraint: sum(w_i) == 1.0 (100%)
|
|
51
|
-
m.constraint(expr
|
|
51
|
+
m.constraint(expr`sum(${w}) == 1.0`, "budget_limit");
|
|
52
52
|
|
|
53
53
|
// 2. Expected return constraint: sum(r_i * w_i) >= 8.5%
|
|
54
54
|
const retMap = Object.fromEntries(assets.map((a, i) => [a, expectedReturns[i]]));
|
|
55
|
-
|
|
55
|
+
const portfolioReturn = dot(retMap, w);
|
|
56
|
+
m.constraint(expr`${portfolioReturn} >= ${targetReturn}`, "target_return");
|
|
56
57
|
|
|
57
58
|
// 3. Quadratic Objective: minimize risk w^T * Sigma * w
|
|
58
59
|
let risk = new QuadExpr();
|
|
@@ -71,6 +72,7 @@ async function solvePortfolio() {
|
|
|
71
72
|
console.log(`Status: ${status}`);
|
|
72
73
|
console.log(`Variance: ${m.objectiveValue?.toFixed(6)}`);
|
|
73
74
|
console.log(`Volatility (Std Dev): ${((Math.sqrt(m.objectiveValue || 0)) * 100).toFixed(2)}%`);
|
|
75
|
+
console.log(`Expected Return: ${((portfolioReturn.value ?? 0) * 100).toFixed(2)}% (target >= ${(targetReturn * 100).toFixed(2)}%)`);
|
|
74
76
|
|
|
75
77
|
assets.forEach((name) => {
|
|
76
78
|
console.log(` ${name.padEnd(10)}: ${((w[name].value ?? 0) * 100).toFixed(2)}%`);
|
|
@@ -81,3 +83,8 @@ solvePortfolio();
|
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
</CodeRunner>
|
|
86
|
+
|
|
87
|
+
## Practical Notes
|
|
88
|
+
|
|
89
|
+
- **Tracing the Efficient Frontier (`targetReturn` & `portfolioReturn.value`)**: Because the achieved return (`8.50%`) matches `targetReturn` exactly, the return constraint is tightly binding on the Markowitz efficient frontier. Varying `targetReturn` across `[0.05, 0.12]` traces out the full risk-return frontier.
|
|
90
|
+
- **Risk-Aversion Scalarization vs. Target Return Constraint**: If `targetReturn` is set above the highest single-asset return ($12\%$ for `Tech` under long-only weights), the hard constraint $\mathbf{r}^T \mathbf{w} \ge R_{\text{target}}$ becomes `"Infeasible"`. An alternative formulation that is always feasible for any risk appetite $\lambda > 0$ is to minimize the risk-adjusted objective $\min (\lambda \, \mathbf{w}^T \boldsymbol{\Sigma} \mathbf{w} - \mathbf{r}^T \mathbf{w})$.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Linear / Integer Programming (LP / MIP) | HiGHS (default) | `m.integer()`, `m.constraint()`, `expr` | <ul><li><a href="https://ohtaman.
|
|
5
|
+
| Linear / Integer Programming (LP / MIP) | HiGHS (default) | `m.integer()`, `m.constraint()`, `expr` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=production">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/01-production-planning.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
Production planning is a classic Linear Programming problem: determine the optimal quantity of each product to manufacture in order to maximize total profit while respecting resource constraints (raw materials, machine time, labor hours).
|
|
8
8
|
|
|
@@ -34,9 +34,9 @@ async function runProductionOptimization() {
|
|
|
34
34
|
// Objective: Maximize total profit ($45 per chair, $80 per table)
|
|
35
35
|
m.objective(maximize(expr`45 * ${chairs} + 80 * ${tables}`));
|
|
36
36
|
|
|
37
|
-
// Resource constraints: declarative formulas via expr and
|
|
38
|
-
m.constraint(expr`5 * ${chairs} + 20 * ${tables} <= 400`, "mahogany_limit")
|
|
39
|
-
|
|
37
|
+
// Resource constraints: declarative formulas via expr and constraint()
|
|
38
|
+
m.constraint(expr`5 * ${chairs} + 20 * ${tables} <= 400`, "mahogany_limit");
|
|
39
|
+
m.constraint(expr`10 * ${chairs} + 15 * ${tables} <= 450`, "labor_limit");
|
|
40
40
|
|
|
41
41
|
const status = await m.solve();
|
|
42
42
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Binary MIP & SMT Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `m.constraint()`, `expr
|
|
5
|
+
| Binary MIP & SMT Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `m.constraint()`, `expr` (`sum`, `dot`), `minimize`, `m.check()` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=shift_scheduling">Playground (Roster)</a></li><li><a href="https://washi.ohtaman.dev/demo/?preset=shift_conflict_unsat">Playground (Conflict)</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/11-shift-scheduling.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
Staff shift scheduling is a classic real-world application of Mixed-Integer Programming in SaaS and workforce management tools. However, in real applications, **staff vacation requests frequently collide with minimum coverage or leadership rules**, causing the solver to return `Infeasible`.
|
|
8
8
|
|
|
@@ -35,7 +35,7 @@ We evaluate two distinct, named scenarios with different vacation inputs to illu
|
|
|
35
35
|
<CodeRunner preset="shift_scheduling">
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
|
-
import { Model,
|
|
38
|
+
import { Model, minimize, expr } from "@ohtaman/washi";
|
|
39
39
|
import "@ohtaman/washi/solvers/z3";
|
|
40
40
|
|
|
41
41
|
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
|
|
@@ -68,12 +68,12 @@ function buildShiftModel(name: string, dayOffRequests: Array<{ staff: string; da
|
|
|
68
68
|
),
|
|
69
69
|
])
|
|
70
70
|
);
|
|
71
|
-
m.objective(minimize(dot(costMatrix, shift)));
|
|
71
|
+
m.objective(minimize(expr`dot(${costMatrix}, ${shift})`));
|
|
72
72
|
|
|
73
73
|
// Rule 1: Daily headcount coverage
|
|
74
74
|
for (const d of days) {
|
|
75
75
|
m.constraint(
|
|
76
|
-
expr
|
|
76
|
+
expr`sum(${staffList}, ${(s) => shift[s.name][d]}) >= ${requiredCoverage[d]}`,
|
|
77
77
|
`coverage_${d}`
|
|
78
78
|
);
|
|
79
79
|
}
|
|
@@ -82,7 +82,7 @@ function buildShiftModel(name: string, dayOffRequests: Array<{ staff: string; da
|
|
|
82
82
|
const seniors = staffList.filter((s) => s.isSenior);
|
|
83
83
|
for (const d of days) {
|
|
84
84
|
m.constraint(
|
|
85
|
-
expr
|
|
85
|
+
expr`sum(${seniors}, ${(s) => shift[s.name][d]}) >= 1`,
|
|
86
86
|
`senior_duty_${d}`
|
|
87
87
|
);
|
|
88
88
|
}
|
|
@@ -90,7 +90,7 @@ function buildShiftModel(name: string, dayOffRequests: Array<{ staff: string; da
|
|
|
90
90
|
// Rule 3: Max working days per staff member
|
|
91
91
|
for (const s of staffList) {
|
|
92
92
|
m.constraint(
|
|
93
|
-
expr
|
|
93
|
+
expr`sum(${shift[s.name]}) <= ${s.maxShifts}`,
|
|
94
94
|
`max_shifts_${s.name}`
|
|
95
95
|
);
|
|
96
96
|
}
|
|
@@ -126,7 +126,7 @@ async function runScenario(
|
|
|
126
126
|
if (status === "Optimal") {
|
|
127
127
|
console.log(`Optimal Weekly Payroll: $${model.objectiveValue}`);
|
|
128
128
|
for (const s of staffList) {
|
|
129
|
-
const schedule = days.map((d) => (
|
|
129
|
+
const schedule = days.map((d) => (shift[s.name][d].value === 1 ? "ON" : " ·"));
|
|
130
130
|
console.log(` ${s.name.padEnd(8)}: ${schedule.join(" ")}`);
|
|
131
131
|
}
|
|
132
132
|
console.log("Schedule resolved successfully (no UNSAT diagnosis needed).\n");
|
|
@@ -173,3 +173,16 @@ main();
|
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
</CodeRunner>
|
|
176
|
+
|
|
177
|
+
## Auto-Repairing Conflicts with Soft Constraints (Elastic Requests)
|
|
178
|
+
|
|
179
|
+
Once Z3 `unsatCore` diagnoses that `senior_duty_Sat` conflicts with `day_off_Alice_Sat` and `day_off_Bob_Sat`, you can offer managers an automatic repair option by relaxing day-off requests from hard constraints (`shift[staff][day] == 0`) into **soft preferences** penalized in the objective (e.g., `+$500` per overridden day off):
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Soft constraint pattern: penalize working on a requested day off by $500
|
|
183
|
+
// instead of enforcing hard shift[req.staff][req.day] == 0
|
|
184
|
+
const requestedShiftVars = dayOffRequests.map((req) => shift[req.staff][req.day]);
|
|
185
|
+
m.objective(minimize(expr`dot(${costMatrix}, ${shift}) + 500 * sum(${requestedShiftVars})`));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With this soft formulation on Scenario B, HiGHS automatically finds a valid roster that honors 3 of the 4 requests (`Alice (Sat)`, `Charlie (Sat)`, and `Diana (Wed)`) and overrides only `Bob (Sat)`—choosing Bob over Alice because Bob's daily wage (`$210`) is `$10` lower than Alice's (`$220`).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Propositional Logic & SMT | Z3 (`@ohtaman/washi/solvers/z3`) | `m.bool()`, `m.integer()`, `m.constraint()`, `expr`, `m.check()` | <ul><li><a href="https://ohtaman.
|
|
5
|
+
| Propositional Logic & SMT | Z3 (`@ohtaman/washi/solvers/z3`) | `m.bool()`, `m.integer()`, `implies()`, `m.constraint()`, `expr`, `m.check()` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=smt_security">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/04-smt-security-rules.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
Satisfiability Modulo Theories (SMT) combines propositional logic (Boolean SAT) with first-order theories (linear integer arithmetic, real inequalities).
|
|
8
8
|
|
|
@@ -18,10 +18,10 @@ We model a security rule system with sensor readings and logic rules:
|
|
|
18
18
|
|
|
19
19
|
## Implementation in WaSHI
|
|
20
20
|
|
|
21
|
-
<CodeRunner preset="
|
|
21
|
+
<CodeRunner preset="smt_security">
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import { Model, not, expr } from "@ohtaman/washi";
|
|
24
|
+
import { Model, implies, not, expr } from "@ohtaman/washi";
|
|
25
25
|
import "@ohtaman/washi/solvers/z3";
|
|
26
26
|
|
|
27
27
|
async function verifySecurityRules() {
|
|
@@ -39,7 +39,7 @@ async function verifySecurityRules() {
|
|
|
39
39
|
const luxLevel = m.integer("luxLevel", { lb: 0, ub: 1000 });
|
|
40
40
|
|
|
41
41
|
// Rule 1: luxLevel <= 50 implies isNight
|
|
42
|
-
m.constraint(luxLevel
|
|
42
|
+
m.constraint(implies(expr`${luxLevel} <= 50`, isNight), "dark_implies_night");
|
|
43
43
|
|
|
44
44
|
// Rule 2: (motionDetected AND isNight AND alarmArmed) => sirenActive
|
|
45
45
|
m.constraint(
|
|
@@ -66,9 +66,21 @@ async function verifySecurityRules() {
|
|
|
66
66
|
console.log(`Lights On : ${lightsOn.value}`);
|
|
67
67
|
console.log(`Door Locked : ${frontDoorUnlocked.value ? "No" : "Yes"}`);
|
|
68
68
|
console.log(`Is Night : ${isNight.value}`);
|
|
69
|
+
|
|
70
|
+
// 4. Formal Safety Verification (Counterexample Search):
|
|
71
|
+
// Prove that the siren CANNOT remain off under these sensor readings
|
|
72
|
+
m.constraint(not(sirenActive), "counterexample_siren_off");
|
|
73
|
+
const proof = await m.check({ solver: "z3" });
|
|
74
|
+
console.log(`Can siren stay off? ${proof.status} (Proven safe when Unsat)`);
|
|
75
|
+
console.log(`Active proof core : ${(proof.conflicts ?? []).join(", ")}`);
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
verifySecurityRules();
|
|
72
79
|
```
|
|
73
80
|
|
|
74
81
|
</CodeRunner>
|
|
82
|
+
|
|
83
|
+
## Practical Notes
|
|
84
|
+
|
|
85
|
+
- **Proof by Refutation (Counterexample Search)**: A single `Sat` check shows that *a* valid assignment exists. To formally prove that a safety property $P$ (such as `sirenActive`) **must** hold in *every* valid state, assert its negation `not(P)` and verify that `m.check()` returns `"Unsat"`. If it returns `"Sat"`, the solver's variable values give you a concrete counterexample (security loophole).
|
|
86
|
+
- **One-Way Implication (`implies`) vs. Equivalence**: In classical logic, `` implies(expr`${luxLevel} <= 50`, isNight) `` only constrains `isNight` when `luxLevel <= 50`. During daytime (`luxLevel > 50`), `isNight` is unconstrained ("open-world" assumption) and could be assigned either `true` or `false` unless you also add the converse rule `` implies(expr`${luxLevel} >= 51`, not(isNight)) ``.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr
|
|
5
|
+
| Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr` (`sum`, `dot`), `minimize` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=transportation">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/09-transportation-problem.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
The transportation problem is one of the most fundamental network optimization models: determine how many units to ship from each supply origin (factory) to each destination (store) to minimize total shipping cost while respecting factory supply limits and meeting store demands.
|
|
8
8
|
|
|
@@ -35,7 +35,7 @@ $$\begin{aligned}
|
|
|
35
35
|
<CodeRunner preset="transportation">
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
|
-
import { Model,
|
|
38
|
+
import { Model, minimize, expr } from "@ohtaman/washi";
|
|
39
39
|
|
|
40
40
|
const factories = ["Tokyo", "Nagoya", "Osaka"] as const;
|
|
41
41
|
const stores = ["North", "East", "West", "South"] as const;
|
|
@@ -49,42 +49,69 @@ const shippingCost = {
|
|
|
49
49
|
const supplies = { Tokyo: 120, Nagoya: 100, Osaka: 80 };
|
|
50
50
|
const demands = { North: 70, East: 90, West: 60, South: 60 };
|
|
51
51
|
|
|
52
|
-
async function
|
|
52
|
+
async function solveScenario(label: string, cap: typeof supplies) {
|
|
53
53
|
const m = new Model("TransportationNetwork");
|
|
54
54
|
|
|
55
55
|
// 1. Create 2D continuous variables: ship[factory][store] >= 0
|
|
56
56
|
const ship = m.continuous([...factories], [...stores], { lb: 0 });
|
|
57
57
|
|
|
58
58
|
// 2. Objective: Minimize total shipping cost across all routes via 2D dot product
|
|
59
|
-
m.objective(minimize(dot(shippingCost, ship)));
|
|
59
|
+
m.objective(minimize(expr`dot(${shippingCost}, ${ship})`));
|
|
60
60
|
|
|
61
|
-
// 3. Supply constraints (row sums): outgoing from f <=
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`supply_${f}`
|
|
66
|
-
)
|
|
67
|
-
|
|
61
|
+
// 3. Supply constraints (row sums): outgoing from f <= cap[f]
|
|
62
|
+
const supplyConstrs = Object.fromEntries(
|
|
63
|
+
factories.map((f) => [
|
|
64
|
+
f,
|
|
65
|
+
m.constraint(expr`sum(${ship[f]}) <= ${cap[f]}`, `supply_${f}`),
|
|
66
|
+
])
|
|
67
|
+
);
|
|
68
68
|
|
|
69
69
|
// 4. Demand constraints (column sums): incoming to s >= demands[s]
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const demandConstrs = Object.fromEntries(
|
|
71
|
+
stores.map((s) => [
|
|
72
|
+
s,
|
|
73
|
+
m.constraint(
|
|
74
|
+
expr`sum(${factories}, ${(f) => ship[f][s]}) >= ${demands[s]}`,
|
|
75
|
+
`demand_${s}`
|
|
76
|
+
),
|
|
77
|
+
])
|
|
78
|
+
);
|
|
76
79
|
|
|
77
80
|
const status = await m.solve();
|
|
78
|
-
console.log(`Status: ${status}, Total Shipping Cost: $${m.objectiveValue}`);
|
|
81
|
+
console.log(`[${label}] Status: ${status}, Total Shipping Cost: $${m.objectiveValue}`);
|
|
79
82
|
for (const f of factories) {
|
|
83
|
+
const dual = supplyConstrs[f].dual ?? 0;
|
|
80
84
|
const routes = stores
|
|
81
85
|
.map((s) => `${s}:${Math.round(ship[f][s].value ?? 0)}`)
|
|
82
86
|
.join(", ");
|
|
83
|
-
console.log(` ${f.padEnd(7)} -> ${routes}`);
|
|
87
|
+
console.log(` ${f.padEnd(7)} -> ${routes} (supply dual: $${dual}/u)`);
|
|
84
88
|
}
|
|
89
|
+
const demandDuals = stores
|
|
90
|
+
.map((s) => `${s}:$${demandConstrs[s].dual ?? 0}`)
|
|
91
|
+
.join(", ");
|
|
92
|
+
console.log(` Marginal Delivered Cost (demand duals) -> ${demandDuals}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runTransportation() {
|
|
96
|
+
// Scenario A: Baseline capacities ($920)
|
|
97
|
+
await solveScenario("Scenario A: Baseline", supplies);
|
|
98
|
+
|
|
99
|
+
// Scenario B: Tokyo capacity reduced (120 -> 80) due to maintenance ($1,000)
|
|
100
|
+
await solveScenario("Scenario B: Tokyo Maintenance", {
|
|
101
|
+
Tokyo: 80,
|
|
102
|
+
Nagoya: 120,
|
|
103
|
+
Osaka: 100,
|
|
104
|
+
});
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
runTransportation();
|
|
88
108
|
```
|
|
89
109
|
|
|
90
110
|
</CodeRunner>
|
|
111
|
+
|
|
112
|
+
## Practical Notes
|
|
113
|
+
|
|
114
|
+
- **Interpreting Supply & Demand Shadow Prices (`.dual`)**:
|
|
115
|
+
- **`demand_s.dual` ($\ge 0$)**: The marginal delivered cost of supplying 1 additional unit to store $s$.
|
|
116
|
+
- **`supply_f.dual` ($\le 0$)**: How much total network shipping cost would decrease if factory $f$'s capacity were expanded by 1 unit. In Scenario B, when Tokyo is constrained at 80 units, `supply_Tokyo.dual` becomes negative (`-$2/u`), indicating that each extra unit of capacity in Tokyo saves \$2 by avoiding rerouting through Nagoya.
|
|
117
|
+
- **Total Supply vs. Total Demand ($\sum \text{Supply}_f \ge \sum \text{Demand}_s$)**: If store demands exceed aggregate factory capacity, the hard demand constraints become `"Infeasible"`. In production logistics systems, add an elastic "spot-carrier / backorder" variable $u_s \ge 0$ to each store's demand constraint ($\sum_f x_{f,s} + u_s \ge \text{Demand}_s$) with a high penalty cost so the solver allocates scarce inventory optimally rather than failing.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Combinatorial OR / Benders | HiGHS (default) | `m.solveBenders()`, `subproblem`, `m.binary()`, `m.constraint()`, `expr` | <ul><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts">Source Code</a></li><li><a href="
|
|
5
|
+
| Combinatorial OR / Benders | HiGHS (default) | `m.solveBenders()`, `subproblem`, `m.binary()`, `m.constraint()`, `expr` | <ul><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts">Source Code</a></li><li><a href="/guide/decomposition">Decomposition Guide</a></li></ul> |
|
|
6
6
|
|
|
7
7
|
The **Traveling Salesperson Problem (TSP)** seeks the shortest closed tour that visits every city in a given set once and returns to the starting city.
|
|
8
8
|
|
|
@@ -31,7 +31,7 @@ We consider **5 cities** on a 2D Euclidean coordinate grid:
|
|
|
31
31
|
<CodeRunner>
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
-
import { Model, expr, minimize
|
|
34
|
+
import { Model, expr, minimize } from "@ohtaman/washi";
|
|
35
35
|
|
|
36
36
|
async function runTSP() {
|
|
37
37
|
const cities = [
|
|
@@ -52,39 +52,26 @@ async function runTSP() {
|
|
|
52
52
|
// 1. Master Problem: Degree assignment relaxation
|
|
53
53
|
const master = new Model("TSP_BendersMaster");
|
|
54
54
|
const x = {};
|
|
55
|
+
const distMatrix = {};
|
|
55
56
|
|
|
56
57
|
for (let i = 0; i < n; i++) {
|
|
57
58
|
x[i] = {};
|
|
59
|
+
distMatrix[i] = {};
|
|
58
60
|
for (let j = 0; j < n; j++) {
|
|
59
61
|
if (i !== j) {
|
|
60
62
|
x[i][j] = master.binary(`x_${i}_${j}`);
|
|
63
|
+
distMatrix[i][j] = dist(i, j);
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
// Objective: Minimize total tour distance
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i < n; i++) {
|
|
68
|
-
for (let j = 0; j < n; j++) {
|
|
69
|
-
if (i !== j) {
|
|
70
|
-
totalDistance = totalDistance.add(x[i][j].mul(dist(i, j)));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
master.objective(minimize(totalDistance));
|
|
69
|
+
master.objective(minimize(expr`dot(${distMatrix}, ${x})`));
|
|
75
70
|
|
|
76
71
|
// Each city must have 1 outgoing and 1 incoming edge
|
|
77
72
|
for (let i = 0; i < n; i++) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (let j = 0; j < n; j++) {
|
|
81
|
-
if (i !== j) {
|
|
82
|
-
outExpr = outExpr.add(x[i][j]);
|
|
83
|
-
inExpr = inExpr.add(x[j][i]);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
master.constraint(expr`${outExpr} == 1`, `out_${i}`);
|
|
87
|
-
master.constraint(expr`${inExpr} == 1`, `in_${i}`);
|
|
73
|
+
master.constraint(expr`sum(${x[i]}) == 1`, `out_${i}`);
|
|
74
|
+
master.constraint(expr`sum(${n}, ${(j) => x[j][i]}) == 1`, `in_${i}`);
|
|
88
75
|
}
|
|
89
76
|
|
|
90
77
|
console.log("Solving TSP with Benders lazy subtour elimination...");
|
|
@@ -97,7 +84,7 @@ async function runTSP() {
|
|
|
97
84
|
const succ = {};
|
|
98
85
|
for (let i = 0; i < n; i++) {
|
|
99
86
|
for (let j = 0; j < n; j++) {
|
|
100
|
-
if (i !== j &&
|
|
87
|
+
if (i !== j && x[i][j].value === 1) {
|
|
101
88
|
succ[i] = j;
|
|
102
89
|
}
|
|
103
90
|
}
|
|
@@ -119,27 +106,22 @@ async function runTSP() {
|
|
|
119
106
|
}
|
|
120
107
|
}
|
|
121
108
|
|
|
122
|
-
// If more than 1 cycle, subtours exist: inject Benders feasibility
|
|
109
|
+
// If more than 1 cycle, subtours exist: inject Benders feasibility cuts for all cycles
|
|
123
110
|
if (cycles.length > 1) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
kind: "feasibility",
|
|
140
|
-
constraint: expr`${cutLhs} >= 1`,
|
|
141
|
-
name: `subtour_cut_${cutCounter}`,
|
|
142
|
-
};
|
|
111
|
+
return cycles.map((cycle) => {
|
|
112
|
+
const S = new Set(cycle);
|
|
113
|
+
cutCounter++;
|
|
114
|
+
console.log(`Subtour detected: [${cycle.map(k => cities[k].name).join(" -> ")}]. Adding Benders cut #${cutCounter}.`);
|
|
115
|
+
|
|
116
|
+
const outCities = Array.from({ length: n }, (_, j) => j).filter((j) => !S.has(j));
|
|
117
|
+
const cutEdges = cycle.flatMap((i) => outCities.map((j) => x[i][j]));
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
kind: "feasibility",
|
|
121
|
+
constraint: expr`sum(${cutEdges}) >= 1`,
|
|
122
|
+
name: `subtour_cut_${cutCounter}`,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
143
125
|
}
|
|
144
126
|
|
|
145
127
|
return null; // Single connected tour found
|
|
@@ -154,7 +136,7 @@ async function runTSP() {
|
|
|
154
136
|
console.log("\nOptimal tour edges:");
|
|
155
137
|
for (let i = 0; i < n; i++) {
|
|
156
138
|
for (let j = 0; j < n; j++) {
|
|
157
|
-
if (i !== j &&
|
|
139
|
+
if (i !== j && result.values[`x_${i}_${j}`] === 1) {
|
|
158
140
|
console.log(` ${cities[i].name} -> ${cities[j].name} (distance: ${dist(i, j).toFixed(3)})`);
|
|
159
141
|
}
|
|
160
142
|
}
|
|
@@ -169,5 +151,7 @@ runTSP();
|
|
|
169
151
|
## Notes
|
|
170
152
|
|
|
171
153
|
- **Lazy Subtour Elimination**: The master problem starts with only the $2n$ degree constraints, and subtour elimination cuts are added on demand when disconnected cycles appear in the master solution.
|
|
172
|
-
- **Separation
|
|
154
|
+
- **Multi-Cut Separation**: Returning an array of cuts (`cycles.map(...)`) from `subproblem` eliminates all disconnected cycles discovered in the current candidate solution simultaneously, reducing the number of master MIP re-solves.
|
|
155
|
+
- **Two Equivalent DFJ Cut Forms**: Because every city has out-degree $\sum_{j \ne i} x_{ij} = 1$, summing over $i \in S$ gives $\sum_{i \in S, j \in S} x_{ij} + \sum_{i \in S, j \notin S} x_{ij} = |S|$. Thus the cut-set inequality $\sum_{i \in S, j \notin S} x_{ij} \ge 1$ used above and the internal-edge inequality $\sum_{i \in S, j \in S} x_{ij} \le |S| - 1$ used in [`14-traveling-salesperson-benders.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/14-traveling-salesperson-benders.ts) are algebraically identical.
|
|
156
|
+
- **Upfront 2-City Subtour Elimination**: In larger TSP instances, mutual 2-cycles ($i \leftrightarrow j$) are the most frequent initial subtours. Adding the $\binom{n}{2}$ pairwise constraints $x_{ij} + x_{ji} \le 1$ directly to the initial master problem often cuts the number of Benders iterations substantially.
|
|
173
157
|
- **Termination Condition**: Returning `null` from `subproblem` indicates that no violated subtour constraints remain.
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
| Problem Class | Solver | Key APIs | Links |
|
|
4
4
|
| :--- | :--- | :--- | :--- |
|
|
5
|
-
| Infeasibility Diagnosis / Unsat Core | Z3 (`@ohtaman/washi/solvers/z3`) | `m.integer()`, `m.continuous()`, `m.constraint()`, `expr`, `m.check()`, `m.conflicts` | <ul><li><a href="https://ohtaman.
|
|
5
|
+
| Infeasibility Diagnosis / Unsat Core | Z3 (`@ohtaman/washi/solvers/z3`) | `m.integer()`, `m.continuous()`, `implies()`, `m.constraint()`, `expr`, `m.check()`, `m.conflicts` | <ul><li><a href="https://washi.ohtaman.dev/demo/?preset=conflict_diagnosis_unsat">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/05-unsat-core-diagnosis.ts">Source Code</a></li></ul> |
|
|
6
6
|
|
|
7
|
-
When business rules, budgets, and operational policies grow complex, constraints
|
|
7
|
+
When business rules, budgets, and operational policies grow complex, constraints can easily conflict. Because a standard optimization call (`m.solve()`) only reports `"Infeasible"`, WaSHI provides `m.check()` to pinpoint conflicting rules without manual trial-and-error.
|
|
8
8
|
|
|
9
9
|
This example shows how to isolate the conflicting constraint subset using Z3 Unsat Core extraction.
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ $$8 \text{ engineers} \times \$8,000 = \$64,000 > \$50,000 \text{ budget!}$$
|
|
|
26
26
|
<CodeRunner preset="conflict_diagnosis_unsat">
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
|
-
import { Model, expr } from "@ohtaman/washi";
|
|
29
|
+
import { Model, implies, expr } from "@ohtaman/washi";
|
|
30
30
|
import "@ohtaman/washi/solvers/z3";
|
|
31
31
|
|
|
32
32
|
async function diagnoseConflicts() {
|
|
@@ -43,7 +43,7 @@ async function diagnoseConflicts() {
|
|
|
43
43
|
m.constraint(rushDelivery, "rush_required");
|
|
44
44
|
|
|
45
45
|
// Rule 3: Rush delivery requires at least 8 engineers
|
|
46
|
-
m.constraint(
|
|
46
|
+
m.constraint(implies(rushDelivery, expr`${engineers} >= 8`), "rush_staffing_rule");
|
|
47
47
|
|
|
48
48
|
// Rule 4: Board budget cap of $50,000 (Conflict!)
|
|
49
49
|
m.constraint(expr`${budget} <= 50000`, "strict_board_budget_cap");
|
|
@@ -55,10 +55,20 @@ async function diagnoseConflicts() {
|
|
|
55
55
|
const res = await m.check({ solver: "z3" });
|
|
56
56
|
|
|
57
57
|
console.log(`Status: ${res.status}`);
|
|
58
|
-
console.log("
|
|
58
|
+
console.log("Z3 Unsat Core (scale-invariant):", m.conflicts);
|
|
59
|
+
|
|
60
|
+
// Built-in HiGHS can also diagnose conflicts via single-solve L1 elastic slack relaxation:
|
|
61
|
+
const highsDiag = await m.check({ solver: "highs" });
|
|
62
|
+
console.log("HiGHS Elastic Repair Set:", highsDiag.conflicts);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
diagnoseConflicts();
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
</CodeRunner>
|
|
69
|
+
|
|
70
|
+
## Practical Notes
|
|
71
|
+
|
|
72
|
+
- **Z3 `unsatCore` vs. HiGHS Elastic Slack `conflicts`**:
|
|
73
|
+
- **Z3 (`m.check({ solver: "z3" })`)** performs logical assumption tracking and returns all four rules that jointly form the contradiction (`budget_limit`, `rush_required`, `rush_staffing_rule`, `strict_board_budget_cap`), while excluding the innocent `minimum_one_employee` rule.
|
|
74
|
+
- **HiGHS (`m.check({ solver: "highs" })`)** solves a single $L_1$ elastic slack minimization problem ($\min \sum s_i$). Because relaxing `rush_staffing_rule` (or `rush_required`) requires a slack of only `1`–`2` units whereas relaxing `strict_board_budget_cap` requires a slack of `14,000` dollars, unweighted $L_1$ minimization naturally highlights the smallest-magnitude repair (`rush_required`) needed to restore feasibility.
|