@ohtaman/washi 0.4.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 +149 -121
- package/README.md +149 -121
- package/README.zh-CN.md +150 -122
- 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 +21 -3
- 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 +433 -286
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +1008 -981
- package/dist/browser/solvers/z3.min.js +2 -2
- package/dist/browser/washi.js +2778 -673
- 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 +21 -3
- 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-C20sxTwm.js +964 -0
- package/dist/lib/solvers/glpk.js +201 -90
- package/dist/lib/solvers/z3.js +399 -259
- package/dist/lib/washi.js +2363 -1056
- 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 +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/modeler/big-m.d.ts +75 -0
- package/dist/types/modeler/big-m.d.ts.map +1 -0
- package/dist/types/modeler/boolean-expr.d.ts +103 -14
- package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
- package/dist/types/modeler/constraint.d.ts +47 -9
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +123 -28
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +11 -6
- 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 +30 -3
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +623 -131
- package/dist/types/modeler/model.d.ts.map +1 -1
- package/dist/types/modeler/objective.d.ts +59 -0
- package/dist/types/modeler/objective.d.ts.map +1 -0
- package/dist/types/modeler/parser.d.ts +34 -10
- package/dist/types/modeler/parser.d.ts.map +1 -1
- package/dist/types/modeler/quad-expr.d.ts +26 -7
- package/dist/types/modeler/quad-expr.d.ts.map +1 -1
- package/dist/types/modeler/solution-validator.d.ts +93 -0
- package/dist/types/modeler/solution-validator.d.ts.map +1 -0
- package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
- package/dist/types/modeler/solvers/glpk/solver.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 +38 -13
- 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 +12 -5
- 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/mock.d.ts +14 -5
- package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
- package/dist/types/modeler/solvers/types.d.ts +73 -11
- package/dist/types/modeler/solvers/types.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
- package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
- package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
- package/dist/types/modeler/types.d.ts +86 -1
- package/dist/types/modeler/types.d.ts.map +1 -1
- package/dist/types/modeler/validation.d.ts +56 -0
- package/dist/types/modeler/validation.d.ts.map +1 -0
- package/dist/types/modeler/variable.d.ts +48 -14
- package/dist/types/modeler/variable.d.ts.map +1 -1
- package/docs/examples/diet-sensitivity.md +90 -0
- package/docs/examples/ecommerce-coupons.md +106 -0
- package/docs/examples/index.md +57 -0
- package/docs/examples/knapsack.md +60 -0
- package/docs/examples/portfolio-qp.md +83 -0
- package/docs/examples/production.md +51 -0
- package/docs/examples/shift-scheduling.md +175 -0
- package/docs/examples/smt-security.md +74 -0
- package/docs/examples/transportation.md +90 -0
- package/docs/examples/traveling-salesperson.md +173 -0
- package/docs/examples/unsat-core.md +64 -0
- package/docs/examples/vehicle-routing.md +206 -0
- package/docs/getting-started/ai-agents.md +271 -0
- package/docs/getting-started/cross-origin.md +87 -0
- package/docs/getting-started/index.md +116 -0
- package/docs/getting-started/runtimes.md +227 -0
- package/docs/guide/constraints.md +177 -0
- package/docs/guide/decomposition.md +399 -0
- package/docs/guide/expressions.md +204 -0
- package/docs/guide/objectives.md +170 -0
- package/docs/guide/unsat-core.md +154 -0
- package/docs/guide/variables.md +188 -0
- package/docs/ja/getting-started/ai-agents.md +270 -0
- package/docs/solvers/custom-solver.md +525 -0
- package/docs/solvers/glpk.md +66 -0
- package/docs/solvers/highs.md +115 -0
- package/docs/solvers/index.md +42 -0
- package/docs/solvers/z3.md +78 -0
- package/docs/zh/getting-started/ai-agents.md +270 -0
- package/llms-full.txt +4065 -0
- package/llms.txt +201 -0
- package/package.json +32 -16
- package/skills/washi/SKILL.md +197 -0
- package/dist/browser/browser-runtime-D13gHOT5.js +0 -4189
- package/dist/browser/loader-CVt4CloI.js +0 -58
- package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
- package/dist/lib/constraint-BRuB3ooC.js +0 -90
- package/dist/lib/loader-Dti0v9yC.js +0 -50
- package/dist/lib/registry-DksqCSjI.js +0 -32
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 0-1 Knapsack Problem (Binary MIP)
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
4
|
+
| :--- | :--- | :--- | :--- |
|
|
5
|
+
| 0-1 Binary MIP | HiGHS (default) | `m.binary()`, `m.constraint()`, `expr`, `maximize`, `dot` | <ul><li><a href="https://ohtaman.github.io/washi/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
|
+
|
|
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
|
+
|
|
9
|
+
## Mathematical Formulation
|
|
10
|
+
|
|
11
|
+
Let $x_i \in \{0, 1\}$ be binary decision variables indicating whether item $i$ is selected ($x_i = 1$) or not ($x_i = 0$):
|
|
12
|
+
|
|
13
|
+
$$\begin{aligned}
|
|
14
|
+
\text{Maximize} \quad & \sum_{i} v_i x_i & (\text{Total Value}) \\
|
|
15
|
+
\text{Subject to} \quad & \sum_{i} w_i x_i \le W & (\text{Weight Capacity}) \\
|
|
16
|
+
& x_i \in \{0, 1\} & (\text{Binary Decisions})
|
|
17
|
+
\end{aligned}$$
|
|
18
|
+
|
|
19
|
+
## Implementation in WaSHI
|
|
20
|
+
|
|
21
|
+
<CodeRunner preset="knapsack">
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Model, maximize, dot, expr } from "@ohtaman/washi";
|
|
25
|
+
|
|
26
|
+
async function solveKnapsack() {
|
|
27
|
+
const m = new Model("Knapsack");
|
|
28
|
+
|
|
29
|
+
const items = [
|
|
30
|
+
{ name: "Laptop", weight: 3, value: 50 },
|
|
31
|
+
{ name: "Camera", weight: 2, value: 40 },
|
|
32
|
+
{ name: "Drone", weight: 4, value: 65 },
|
|
33
|
+
{ name: "FirstAid", weight: 1, value: 15 },
|
|
34
|
+
{ name: "Tent", weight: 5, value: 70 },
|
|
35
|
+
];
|
|
36
|
+
const maxCapacity = 7; // kg
|
|
37
|
+
|
|
38
|
+
// Create binary variables: x[name] (0 or 1)
|
|
39
|
+
const x = m.binary(items.map((it) => it.name));
|
|
40
|
+
|
|
41
|
+
const values = Object.fromEntries(items.map((it) => [it.name, it.value]));
|
|
42
|
+
const weights = Object.fromEntries(items.map((it) => [it.name, it.weight]));
|
|
43
|
+
|
|
44
|
+
// Maximize total value via dot product
|
|
45
|
+
m.objective(maximize(dot(values, x)));
|
|
46
|
+
|
|
47
|
+
// Weight capacity constraint using natural expr and constraint() syntax
|
|
48
|
+
m.constraint(expr`${dot(weights, x)} <= ${maxCapacity}`, "capacity_limit");
|
|
49
|
+
|
|
50
|
+
const status = await m.solve();
|
|
51
|
+
|
|
52
|
+
console.log(`Status: ${status}`);
|
|
53
|
+
console.log(`Max Total Value: $${m.objectiveValue}`);
|
|
54
|
+
console.log("Selected Items:", m.getValues(x));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
solveKnapsack();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
</CodeRunner>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Portfolio Optimization (QP)
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
4
|
+
| :--- | :--- | :--- | :--- |
|
|
5
|
+
| Convex Quadratic Programming (QP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot`, `QuadExpr` | <ul><li><a href="https://ohtaman.github.io/washi/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
|
+
|
|
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
|
+
|
|
9
|
+
<PortfolioQpDemo />
|
|
10
|
+
|
|
11
|
+
## Mathematical Formulation
|
|
12
|
+
|
|
13
|
+
Let $w_i \ge 0$ denote the allocation weight for asset $i$:
|
|
14
|
+
|
|
15
|
+
$$\begin{aligned}
|
|
16
|
+
\text{Minimize} \quad & \mathbf{w}^T \boldsymbol{\Sigma} \mathbf{w} & (\text{Portfolio Variance}) \\
|
|
17
|
+
\text{Subject to} \quad & \sum_{i=1}^n w_i = 1 & (\text{Full Capital Budget}) \\
|
|
18
|
+
& \sum_{i=1}^n r_i w_i \ge R_{\text{target}} & (\text{Minimum Target Return}) \\
|
|
19
|
+
& 0 \le w_i \le 1 & (\text{Long-Only Weights})
|
|
20
|
+
\end{aligned}$$
|
|
21
|
+
|
|
22
|
+
where $\boldsymbol{\Sigma}$ is the positive semi-definite asset return covariance matrix.
|
|
23
|
+
|
|
24
|
+
## Implementation in WaSHI
|
|
25
|
+
|
|
26
|
+
<CodeRunner preset="portfolio_qp">
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Model, QuadExpr, sum, dot, minimize, expr } from "@ohtaman/washi";
|
|
30
|
+
|
|
31
|
+
async function solvePortfolio() {
|
|
32
|
+
const m = new Model("PortfolioQP");
|
|
33
|
+
|
|
34
|
+
const assets = ["Tech", "Bonds", "GlobalEq", "RealEstate", "Gold"];
|
|
35
|
+
const expectedReturns = [0.12, 0.04, 0.09, 0.07, 0.05];
|
|
36
|
+
const targetReturn = 0.085; // 8.5% minimum expected return
|
|
37
|
+
|
|
38
|
+
// 5x5 Covariance Matrix
|
|
39
|
+
const cov = [
|
|
40
|
+
[0.040, 0.002, 0.025, 0.015, 0.005],
|
|
41
|
+
[0.002, 0.005, 0.003, 0.002, 0.001],
|
|
42
|
+
[0.025, 0.003, 0.030, 0.018, 0.008],
|
|
43
|
+
[0.015, 0.002, 0.018, 0.025, 0.004],
|
|
44
|
+
[0.005, 0.001, 0.008, 0.004, 0.020],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Allocation weights w_i in [0, 1]
|
|
48
|
+
const w = m.continuous(assets, { lb: 0, ub: 1 });
|
|
49
|
+
|
|
50
|
+
// 1. Budget constraint: sum(w_i) == 1.0 (100%)
|
|
51
|
+
m.constraint(expr`${sum(assets, (a) => w[a])} == 1.0`, "budget_limit");
|
|
52
|
+
|
|
53
|
+
// 2. Expected return constraint: sum(r_i * w_i) >= 8.5%
|
|
54
|
+
const retMap = Object.fromEntries(assets.map((a, i) => [a, expectedReturns[i]]));
|
|
55
|
+
m.constraint(expr`${dot(retMap, w)} >= ${targetReturn}`, "target_return");
|
|
56
|
+
|
|
57
|
+
// 3. Quadratic Objective: minimize risk w^T * Sigma * w
|
|
58
|
+
let risk = new QuadExpr();
|
|
59
|
+
for (let i = 0; i < assets.length; i++) {
|
|
60
|
+
for (let j = 0; j < assets.length; j++) {
|
|
61
|
+
if (cov[i][j] !== 0) {
|
|
62
|
+
risk = risk.add(w[assets[i]].mul(w[assets[j]]).mul(cov[i][j]));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
m.objective(minimize(risk));
|
|
67
|
+
|
|
68
|
+
// Solve natively with HiGHS Convex QP engine
|
|
69
|
+
const status = await m.solve();
|
|
70
|
+
|
|
71
|
+
console.log(`Status: ${status}`);
|
|
72
|
+
console.log(`Variance: ${m.objectiveValue?.toFixed(6)}`);
|
|
73
|
+
console.log(`Volatility (Std Dev): ${((Math.sqrt(m.objectiveValue || 0)) * 100).toFixed(2)}%`);
|
|
74
|
+
|
|
75
|
+
assets.forEach((name) => {
|
|
76
|
+
console.log(` ${name.padEnd(10)}: ${((w[name].value ?? 0) * 100).toFixed(2)}%`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
solvePortfolio();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
</CodeRunner>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Production Planning (LP / MIP)
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
4
|
+
| :--- | :--- | :--- | :--- |
|
|
5
|
+
| Linear / Integer Programming (LP / MIP) | HiGHS (default) | `m.integer()`, `m.constraint()`, `expr` | <ul><li><a href="https://ohtaman.github.io/washi/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
|
+
|
|
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
|
+
|
|
9
|
+
## Mathematical Formulation
|
|
10
|
+
|
|
11
|
+
Let $x$ be the number of chairs and $y$ be the number of tables.
|
|
12
|
+
|
|
13
|
+
$$\begin{aligned}
|
|
14
|
+
\text{Maximize} \quad & 45x + 80y & (\text{Profit}) \\
|
|
15
|
+
\text{Subject to} \quad & 5x + 20y \le 400 & (\text{Mahogany Wood limit}) \\
|
|
16
|
+
& 10x + 15y \le 450 & (\text{Carpentry Labor limit}) \\
|
|
17
|
+
& x, y \ge 0, \quad x, y \in \mathbb{Z} & (\text{Integer Quantities})
|
|
18
|
+
\end{aligned}$$
|
|
19
|
+
|
|
20
|
+
## Implementation in WaSHI
|
|
21
|
+
|
|
22
|
+
<CodeRunner preset="production">
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Model, expr, maximize } from "@ohtaman/washi";
|
|
26
|
+
|
|
27
|
+
async function runProductionOptimization() {
|
|
28
|
+
const m = new Model("FactoryProduction");
|
|
29
|
+
|
|
30
|
+
// Decision variables: non-negative integer quantities
|
|
31
|
+
const chairs = m.integer("chairs", { lb: 0 });
|
|
32
|
+
const tables = m.integer("tables", { lb: 0 });
|
|
33
|
+
|
|
34
|
+
// Objective: Maximize total profit ($45 per chair, $80 per table)
|
|
35
|
+
m.objective(maximize(expr`45 * ${chairs} + 80 * ${tables}`));
|
|
36
|
+
|
|
37
|
+
// Resource constraints: declarative formulas via expr and fluent constraint()
|
|
38
|
+
m.constraint(expr`5 * ${chairs} + 20 * ${tables} <= 400`, "mahogany_limit")
|
|
39
|
+
.constraint(expr`10 * ${chairs} + 15 * ${tables} <= 450`, "labor_limit");
|
|
40
|
+
|
|
41
|
+
const status = await m.solve();
|
|
42
|
+
|
|
43
|
+
console.log(`Status: ${status}`);
|
|
44
|
+
console.log(`Optimal Profit: $${m.objectiveValue}`);
|
|
45
|
+
console.log(`Chairs: ${chairs.value}, Tables: ${tables.value}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runProductionOptimization();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</CodeRunner>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Staff Shift Scheduling & Conflict Diagnosis (MIP + Z3)
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
4
|
+
| :--- | :--- | :--- | :--- |
|
|
5
|
+
| Binary MIP & SMT Unsat Core | HiGHS + Z3 | `m.binary(staff, days)`, `m.constraint()`, `expr`, `minimize`, `dot`, `m.check()` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=shift_scheduling">Playground (Roster)</a></li><li><a href="https://ohtaman.github.io/washi/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
|
+
|
|
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
|
+
|
|
9
|
+
This example demonstrates a practical production workflow:
|
|
10
|
+
1. **Try HiGHS MIP Optimization First:** Solve the shift model under the given vacation requests and operational constraints.
|
|
11
|
+
2. **Accept Solution if Optimal:** If a feasible roster is found, display the schedule and minimized payroll cost without unnecessary diagnostic overhead.
|
|
12
|
+
3. **Diagnose with Z3 `unsatCore` Only When Infeasible:** If HiGHS reports `Infeasible`, diagnose the **exact same model and constraints** with Z3 to isolate the conflicting constraint subset (`unsatCore`), pinpointing which day's coverage rules and which staff requests collided.
|
|
13
|
+
|
|
14
|
+
> [!IMPORTANT]
|
|
15
|
+
> **Vacation Requests are Hard Constraints:**
|
|
16
|
+
> In this model, vacation requests are enforced as strict hard constraints (`shift[staff][day] == 0`), not soft constraints with penalty relaxation. When requests collide with mandatory staffing requirements, the model is strictly infeasible.
|
|
17
|
+
|
|
18
|
+
<ShiftSchedulingDemo />
|
|
19
|
+
|
|
20
|
+
## Scenario & Rules
|
|
21
|
+
|
|
22
|
+
We schedule **5 staff members** (`Alice`, `Bob`, `Charlie`, `Diana`, `Evan`) across **7 days** (`Mon`–`Sun`):
|
|
23
|
+
|
|
24
|
+
- **Daily Headcount:** `Mon–Thu`: 3 staff, `Fri`: 4 staff, `Sat`: 3 staff, `Sun`: 2 staff.
|
|
25
|
+
- **Senior Duty:** At least 1 senior staff member (`Alice` or `Bob`) must be on duty every day.
|
|
26
|
+
- **Work Limit:** Each staff member works at most **5 days** per week.
|
|
27
|
+
- **Weekend Premium:** Weekend shifts (`Sat`, `Sun`) incur an extra `+$30` shift premium on top of base daily wage.
|
|
28
|
+
|
|
29
|
+
We evaluate two distinct, named scenarios with different vacation inputs to illustrate the full workflow:
|
|
30
|
+
- **Scenario A (Balanced Requests — Feasible):** Alice (Sun), Bob (Mon), Charlie (Sat), Diana (Wed). All constraints can be satisfied simultaneously. HiGHS outputs an optimal roster.
|
|
31
|
+
- **Scenario B (Conflicting Requests — Infeasible & Diagnosis):** Alice (Sat), Bob (Sat), Charlie (Sat), Diana (Wed). Because both senior leaders (Alice and Bob) request Saturday off simultaneously, the rule requiring at least 1 senior on duty every day cannot be satisfied. HiGHS reports `Infeasible`, triggering Z3 conflict diagnosis.
|
|
32
|
+
|
|
33
|
+
## Implementation in WaSHI
|
|
34
|
+
|
|
35
|
+
<CodeRunner preset="shift_scheduling">
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { Model, sum, dot, minimize, expr } from "@ohtaman/washi";
|
|
39
|
+
import "@ohtaman/washi/solvers/z3";
|
|
40
|
+
|
|
41
|
+
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
|
|
42
|
+
type Day = (typeof days)[number];
|
|
43
|
+
|
|
44
|
+
const staffList = [
|
|
45
|
+
{ name: "Alice", isSenior: true, dailyWage: 220, maxShifts: 5 },
|
|
46
|
+
{ name: "Bob", isSenior: true, dailyWage: 210, maxShifts: 5 },
|
|
47
|
+
{ name: "Charlie", isSenior: false, dailyWage: 160, maxShifts: 5 },
|
|
48
|
+
{ name: "Diana", isSenior: false, dailyWage: 160, maxShifts: 5 },
|
|
49
|
+
{ name: "Evan", isSenior: false, dailyWage: 150, maxShifts: 5 },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const requiredCoverage: Record<Day, number> = {
|
|
53
|
+
Mon: 3, Tue: 3, Wed: 3, Thu: 3, Fri: 4, Sat: 3, Sun: 2,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function buildShiftModel(name: string, dayOffRequests: Array<{ staff: string; day: Day }>) {
|
|
57
|
+
const m = new Model(name);
|
|
58
|
+
|
|
59
|
+
// 2D binary matrix: shift[staff][day] ∈ {0, 1}
|
|
60
|
+
const shift = m.binary(staffList.map((s) => s.name), [...days]);
|
|
61
|
+
|
|
62
|
+
// Objective: Minimize weekly payroll cost via 2D dot product
|
|
63
|
+
const costMatrix = Object.fromEntries(
|
|
64
|
+
staffList.map((s) => [
|
|
65
|
+
s.name,
|
|
66
|
+
Object.fromEntries(
|
|
67
|
+
days.map((d) => [d, s.dailyWage + (d === "Sat" || d === "Sun" ? 30 : 0)])
|
|
68
|
+
),
|
|
69
|
+
])
|
|
70
|
+
);
|
|
71
|
+
m.objective(minimize(dot(costMatrix, shift)));
|
|
72
|
+
|
|
73
|
+
// Rule 1: Daily headcount coverage
|
|
74
|
+
for (const d of days) {
|
|
75
|
+
m.constraint(
|
|
76
|
+
expr`${sum(staffList, (s) => shift[s.name][d])} >= ${requiredCoverage[d]}`,
|
|
77
|
+
`coverage_${d}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Rule 2: At least 1 senior staff member per day
|
|
82
|
+
const seniors = staffList.filter((s) => s.isSenior);
|
|
83
|
+
for (const d of days) {
|
|
84
|
+
m.constraint(
|
|
85
|
+
expr`${sum(seniors, (s) => shift[s.name][d])} >= 1`,
|
|
86
|
+
`senior_duty_${d}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Rule 3: Max working days per staff member
|
|
91
|
+
for (const s of staffList) {
|
|
92
|
+
m.constraint(
|
|
93
|
+
expr`${sum(days, (d) => shift[s.name][d])} <= ${s.maxShifts}`,
|
|
94
|
+
`max_shifts_${s.name}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Rule 4: Named day-off requests as HARD constraints (shift[staff][day] == 0)
|
|
99
|
+
for (const req of dayOffRequests) {
|
|
100
|
+
m.constraint(
|
|
101
|
+
expr`${shift[req.staff][req.day]} == 0`,
|
|
102
|
+
`day_off_${req.staff}_${req.day}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { model: m, shift };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runScenario(
|
|
110
|
+
title: string,
|
|
111
|
+
modelName: string,
|
|
112
|
+
dayOffRequests: Array<{ staff: string; day: Day }>
|
|
113
|
+
) {
|
|
114
|
+
console.log(`=== ${title} ===`);
|
|
115
|
+
console.log(
|
|
116
|
+
"Hard Vacation Requests: " +
|
|
117
|
+
dayOffRequests.map((r) => `${r.staff} (${r.day})`).join(", ")
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const { model, shift } = buildShiftModel(modelName, dayOffRequests);
|
|
121
|
+
|
|
122
|
+
// Step 1: Optimize with HiGHS
|
|
123
|
+
const status = await model.solve();
|
|
124
|
+
console.log(`HiGHS Optimize Status: ${status}`);
|
|
125
|
+
|
|
126
|
+
if (status === "Optimal") {
|
|
127
|
+
console.log(`Optimal Weekly Payroll: $${model.objectiveValue}`);
|
|
128
|
+
for (const s of staffList) {
|
|
129
|
+
const schedule = days.map((d) => (Math.round(shift[s.name][d].value ?? 0) === 1 ? "ON" : " ·"));
|
|
130
|
+
console.log(` ${s.name.padEnd(8)}: ${schedule.join(" ")}`);
|
|
131
|
+
}
|
|
132
|
+
console.log("Schedule resolved successfully (no UNSAT diagnosis needed).\n");
|
|
133
|
+
} else if (status === "Infeasible") {
|
|
134
|
+
// Step 2: Diagnose the exact same model and constraints with Z3
|
|
135
|
+
console.log("HiGHS returned Infeasible! Diagnosing conflict with Z3 unsatCore...");
|
|
136
|
+
const checkRes = await model.check({ solver: "z3" });
|
|
137
|
+
console.log(`Z3 Check Status: ${checkRes.status}`);
|
|
138
|
+
console.log("Conflicting Constraint Subset (unsatCore):", model.unsatCore);
|
|
139
|
+
console.log(
|
|
140
|
+
"Note: unsatCore returns a conflicting subset (minimality/order not guaranteed). " +
|
|
141
|
+
"Independent requests like Diana's Wednesday off are excluded.\n"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function main() {
|
|
147
|
+
// Scenario A: Balanced requests (Solvable)
|
|
148
|
+
await runScenario(
|
|
149
|
+
"Scenario A: Balanced Vacation Requests",
|
|
150
|
+
"FeasibleRoster",
|
|
151
|
+
[
|
|
152
|
+
{ staff: "Alice", day: "Sun" },
|
|
153
|
+
{ staff: "Bob", day: "Mon" },
|
|
154
|
+
{ staff: "Charlie", day: "Sat" },
|
|
155
|
+
{ staff: "Diana", day: "Wed" },
|
|
156
|
+
]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Scenario B: Conflicting Saturday requests (Infeasible)
|
|
160
|
+
await runScenario(
|
|
161
|
+
"Scenario B: Conflicting Saturday Requests",
|
|
162
|
+
"ConflictRoster",
|
|
163
|
+
[
|
|
164
|
+
{ staff: "Alice", day: "Sat" }, // Senior
|
|
165
|
+
{ staff: "Bob", day: "Sat" }, // Senior
|
|
166
|
+
{ staff: "Charlie", day: "Sat" },
|
|
167
|
+
{ staff: "Diana", day: "Wed" },
|
|
168
|
+
]
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
</CodeRunner>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SMT Security Policies
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
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.github.io/washi/demo/?preset=smt_sat_scheduling">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
|
+
|
|
7
|
+
Satisfiability Modulo Theories (SMT) combines propositional logic (Boolean SAT) with first-order theories (linear integer arithmetic, real inequalities).
|
|
8
|
+
|
|
9
|
+
This example demonstrates how to formally verify and deduce system states in an IoT home security system using WaSHI and the Z3 solver engine.
|
|
10
|
+
|
|
11
|
+
## The Scenario
|
|
12
|
+
|
|
13
|
+
We model a security rule system with sensor readings and logic rules:
|
|
14
|
+
1. When ambient light $\le 50$, it implies night time (`isNight`).
|
|
15
|
+
2. When motion is detected at night and the alarm is armed, the siren must sound.
|
|
16
|
+
3. If the alarm is armed, the front door must never be unlocked.
|
|
17
|
+
4. When motion is detected at night, comfort lights should turn on.
|
|
18
|
+
|
|
19
|
+
## Implementation in WaSHI
|
|
20
|
+
|
|
21
|
+
<CodeRunner preset="smt_sat_scheduling">
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Model, not, expr } from "@ohtaman/washi";
|
|
25
|
+
import "@ohtaman/washi/solvers/z3";
|
|
26
|
+
|
|
27
|
+
async function verifySecurityRules() {
|
|
28
|
+
const m = new Model({ name: "SmartHomeSecurity", solver: "z3" });
|
|
29
|
+
|
|
30
|
+
// 1. Boolean Propositional Variables
|
|
31
|
+
const isNight = m.bool("isNight");
|
|
32
|
+
const motionDetected = m.bool("motionDetected");
|
|
33
|
+
const alarmArmed = m.bool("alarmArmed");
|
|
34
|
+
const frontDoorUnlocked = m.bool("frontDoorUnlocked");
|
|
35
|
+
const sirenActive = m.bool("sirenActive");
|
|
36
|
+
const lightsOn = m.bool("lightsOn");
|
|
37
|
+
|
|
38
|
+
// 2. Arithmetic metric (lux sensor)
|
|
39
|
+
const luxLevel = m.integer("luxLevel", { lb: 0, ub: 1000 });
|
|
40
|
+
|
|
41
|
+
// Rule 1: luxLevel <= 50 implies isNight
|
|
42
|
+
m.constraint(luxLevel.le(50).implies(isNight), "dark_implies_night");
|
|
43
|
+
|
|
44
|
+
// Rule 2: (motionDetected AND isNight AND alarmArmed) => sirenActive
|
|
45
|
+
m.constraint(
|
|
46
|
+
motionDetected.and(isNight).and(alarmArmed).implies(sirenActive),
|
|
47
|
+
"intrusion_rule"
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Rule 3: alarmArmed => NOT(frontDoorUnlocked)
|
|
51
|
+
m.constraint(alarmArmed.implies(not(frontDoorUnlocked)), "lock_safety_rule");
|
|
52
|
+
|
|
53
|
+
// Rule 4: (motionDetected AND isNight) => lightsOn
|
|
54
|
+
m.constraint(motionDetected.and(isNight).implies(lightsOn), "comfort_lights");
|
|
55
|
+
|
|
56
|
+
// 3. Observed Context
|
|
57
|
+
m.constraint(expr`${luxLevel} <= 20`, "sensor_reading_dark");
|
|
58
|
+
m.constraint(alarmArmed, "user_armed_alarm");
|
|
59
|
+
m.constraint(motionDetected, "sensor_motion_triggered");
|
|
60
|
+
|
|
61
|
+
// Check satisfiability and deduce state
|
|
62
|
+
const res = await m.check({ solver: "z3" });
|
|
63
|
+
|
|
64
|
+
console.log(`Status: ${res.status}`);
|
|
65
|
+
console.log(`Siren Active : ${sirenActive.value}`);
|
|
66
|
+
console.log(`Lights On : ${lightsOn.value}`);
|
|
67
|
+
console.log(`Door Locked : ${frontDoorUnlocked.value ? "No" : "Yes"}`);
|
|
68
|
+
console.log(`Is Night : ${isNight.value}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
verifySecurityRules();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
</CodeRunner>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Transportation & Allocation Network (LP)
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
4
|
+
| :--- | :--- | :--- | :--- |
|
|
5
|
+
| Linear Programming (LP) | HiGHS (default) | `m.continuous()`, `m.constraint()`, `expr`, `minimize`, `dot` | <ul><li><a href="https://ohtaman.github.io/washi/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
|
+
|
|
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
|
+
|
|
9
|
+
This example demonstrates how to construct a **2D matrix of decision variables** using `m.continuous(factories, stores)` and write aggregated constraints declaratively via `dot()` and `m.constraint(expr`...`)`.
|
|
10
|
+
|
|
11
|
+
## Problem Inputs
|
|
12
|
+
|
|
13
|
+
We have **3 factories** (`Tokyo`, `Nagoya`, `Osaka`) shipping to **4 regional stores** (`North`, `East`, `West`, `South`):
|
|
14
|
+
|
|
15
|
+
| Factory \ Store | North ($/unit) | East ($/unit) | West ($/unit) | South ($/unit) | Supply Capacity |
|
|
16
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
17
|
+
| **Tokyo** | $4 | $2 | $7 | $8 | **120** units |
|
|
18
|
+
| **Nagoya** | $6 | $4 | $3 | $5 | **100** units |
|
|
19
|
+
| **Osaka** | $9 | $7 | $2 | $3 | **80** units |
|
|
20
|
+
| **Store Demand** | **70** units | **90** units | **60** units | **60** units | Total: 280 / 300 |
|
|
21
|
+
|
|
22
|
+
## Mathematical Formulation
|
|
23
|
+
|
|
24
|
+
Let $x_{f, s} \ge 0$ be the quantity shipped from factory $f \in F$ to store $s \in S$, and $c_{f, s}$ be the unit shipping cost.
|
|
25
|
+
|
|
26
|
+
$$\begin{aligned}
|
|
27
|
+
\text{Minimize} \quad & \sum_{f \in F} \sum_{s \in S} c_{f,s} \, x_{f,s} \\
|
|
28
|
+
\text{Subject to} \quad & \sum_{s \in S} x_{f,s} \le \text{Supply}_f \quad \forall f \in F \quad (\text{Factory capacity}) \\
|
|
29
|
+
& \sum_{f \in F} x_{f,s} \ge \text{Demand}_s \quad \forall s \in S \quad (\text{Store demand}) \\
|
|
30
|
+
& x_{f,s} \ge 0 \quad \forall f \in F, s \in S
|
|
31
|
+
\end{aligned}$$
|
|
32
|
+
|
|
33
|
+
## Implementation in WaSHI
|
|
34
|
+
|
|
35
|
+
<CodeRunner preset="transportation">
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { Model, sum, dot, minimize, expr } from "@ohtaman/washi";
|
|
39
|
+
|
|
40
|
+
const factories = ["Tokyo", "Nagoya", "Osaka"] as const;
|
|
41
|
+
const stores = ["North", "East", "West", "South"] as const;
|
|
42
|
+
|
|
43
|
+
const shippingCost = {
|
|
44
|
+
Tokyo: { North: 4, East: 2, West: 7, South: 8 },
|
|
45
|
+
Nagoya: { North: 6, East: 4, West: 3, South: 5 },
|
|
46
|
+
Osaka: { North: 9, East: 7, West: 2, South: 3 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const supplies = { Tokyo: 120, Nagoya: 100, Osaka: 80 };
|
|
50
|
+
const demands = { North: 70, East: 90, West: 60, South: 60 };
|
|
51
|
+
|
|
52
|
+
async function runTransportation() {
|
|
53
|
+
const m = new Model("TransportationNetwork");
|
|
54
|
+
|
|
55
|
+
// 1. Create 2D continuous variables: ship[factory][store] >= 0
|
|
56
|
+
const ship = m.continuous([...factories], [...stores], { lb: 0 });
|
|
57
|
+
|
|
58
|
+
// 2. Objective: Minimize total shipping cost across all routes via 2D dot product
|
|
59
|
+
m.objective(minimize(dot(shippingCost, ship)));
|
|
60
|
+
|
|
61
|
+
// 3. Supply constraints (row sums): outgoing from f <= supplies[f]
|
|
62
|
+
for (const f of factories) {
|
|
63
|
+
m.constraint(
|
|
64
|
+
expr`${sum(stores, (s) => ship[f][s])} <= ${supplies[f]}`,
|
|
65
|
+
`supply_${f}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Demand constraints (column sums): incoming to s >= demands[s]
|
|
70
|
+
for (const s of stores) {
|
|
71
|
+
m.constraint(
|
|
72
|
+
expr`${sum(factories, (f) => ship[f][s])} >= ${demands[s]}`,
|
|
73
|
+
`demand_${s}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const status = await m.solve();
|
|
78
|
+
console.log(`Status: ${status}, Total Shipping Cost: $${m.objectiveValue}`);
|
|
79
|
+
for (const f of factories) {
|
|
80
|
+
const routes = stores
|
|
81
|
+
.map((s) => `${s}:${Math.round(ship[f][s].value ?? 0)}`)
|
|
82
|
+
.join(", ");
|
|
83
|
+
console.log(` ${f.padEnd(7)} -> ${routes}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
runTransportation();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
</CodeRunner>
|