@ohtaman/washi 0.4.0 → 0.5.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 +100 -107
- package/README.md +98 -105
- package/README.zh-CN.md +101 -108
- package/dist/browser/highs-worker.js +21 -3
- package/dist/browser/loader-CtaDHef_.js +273 -0
- package/dist/browser/solvers/glpk.js +433 -286
- package/dist/browser/solvers/glpk.min.js +1 -1
- package/dist/browser/solvers/z3.js +1002 -981
- package/dist/browser/solvers/z3.min.js +2 -2
- package/dist/browser/washi.js +2129 -676
- package/dist/browser/washi.min.js +4 -4
- package/dist/lib/highs-worker.js +21 -3
- package/dist/lib/loader-DmNC_UNk.js +279 -0
- package/dist/lib/registry-BEfvpZW3.js +951 -0
- package/dist/lib/solvers/glpk.js +201 -90
- package/dist/lib/solvers/z3.js +399 -259
- package/dist/lib/washi.js +1627 -988
- package/dist/types/index.d.ts +4 -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 +36 -9
- package/dist/types/modeler/constraint.d.ts.map +1 -1
- package/dist/types/modeler/expression.d.ts +113 -28
- package/dist/types/modeler/expression.d.ts.map +1 -1
- package/dist/types/modeler/index.d.ts +10 -6
- package/dist/types/modeler/index.d.ts.map +1 -1
- package/dist/types/modeler/lp-writer.d.ts +27 -2
- package/dist/types/modeler/lp-writer.d.ts.map +1 -1
- package/dist/types/modeler/model.d.ts +478 -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/loader.d.ts +36 -11
- package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
- package/dist/types/modeler/solvers/highs/solver.d.ts +11 -4
- 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 +71 -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 +59 -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 +53 -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/unsat-core.md +64 -0
- package/docs/getting-started/ai-agents.md +267 -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/expressions.md +204 -0
- package/docs/guide/objectives.md +170 -0
- package/docs/guide/unsat-core.md +147 -0
- package/docs/guide/variables.md +188 -0
- package/docs/ja/getting-started/ai-agents.md +266 -0
- package/docs/solvers/custom-solver.md +525 -0
- package/docs/solvers/glpk.md +66 -0
- package/docs/solvers/highs.md +93 -0
- package/docs/solvers/index.md +42 -0
- package/docs/solvers/z3.md +78 -0
- package/docs/zh/getting-started/ai-agents.md +266 -0
- package/llms-full.txt +3249 -0
- package/llms.txt +197 -0
- package/package.json +18 -5
- package/skills/washi/SKILL.md +197 -0
- 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,106 @@
|
|
|
1
|
+
# E-Commerce Cart & Coupon Optimizer (Indicator MIP)
|
|
2
|
+
|
|
3
|
+
| Problem Class | Solver | Key APIs | Links |
|
|
4
|
+
| :--- | :--- | :--- | :--- |
|
|
5
|
+
| Mixed-Integer Programming (MIP) | HiGHS (default) | `m.binary()`, `m.constraint()`, `m.indicator()`, `expr`, `minimize` | <ul><li><a href="https://ohtaman.github.io/washi/demo/?preset=cart_coupon">Open in Playground</a></li><li><a href="https://github.com/ohtaman/washi/blob/main/examples/node/12-ecommerce-coupon-optimizer.ts">Source Code</a></li></ul> |
|
|
6
|
+
|
|
7
|
+
Modern e-commerce checkouts often feature complex promotional rules: minimum spend thresholds, category-specific discounts, stackable vs. mutually exclusive coupons, and maximum coupon limits per order. Running an optimizer directly in the browser (or an edge worker) lets the checkout UI automatically apply the best coupon combination—and even recommend a small "filler" item when crossing a spend threshold lowers the customer's net bill.
|
|
8
|
+
|
|
9
|
+
This example demonstrates **`m.indicator(binVar, constraint, { activeValue, M? })`**, which automatically linearizes conditional *"if coupon is selected, minimum spend must hold"* constraints using Big-M reformulation for MIP solvers like HiGHS. When `M` is omitted, WaSHI automatically derives safe, tight Big-M values from finite variable bounds.
|
|
10
|
+
|
|
11
|
+
<EcommerceCouponDemo />
|
|
12
|
+
|
|
13
|
+
## Scenario & Promotion Rules
|
|
14
|
+
|
|
15
|
+
A customer's shopping cart currently contains **$242** of merchandise (**$210** in Tech items and **$32** in Apparel):
|
|
16
|
+
- **Optional Filler Add-on:** A **$10** Braided USB-C Cable (Tech) can be added to bring the cart total from `$242` to `$252`.
|
|
17
|
+
- **Available Coupons** (maximum **2 coupons** total per checkout; at most **1 Exclusive** coupon):
|
|
18
|
+
|
|
19
|
+
| Coupon Code | Discount | Condition | Stacking Rule |
|
|
20
|
+
| :--- | :---: | :--- | :--- |
|
|
21
|
+
| `WELCOME15` | **-$15** | Total cart spend $\ge \$100$ | **Stackable** |
|
|
22
|
+
| `TECH30` | **-$30** | Tech category spend $\ge \$200$ | **Exclusive** (cannot combine with `MEGA45_250`) |
|
|
23
|
+
| `MEGA45_250` | **-$45** | Total cart spend $\ge \$250$ | **Exclusive** (cannot combine with `TECH30`) |
|
|
24
|
+
| `MEMBER10` | **-$10** | Total cart spend $\ge \$150$ | **Stackable** |
|
|
25
|
+
|
|
26
|
+
Without adding the `$10` cable, the cart (`$242`) cannot use `MEGA45_250` and pays `$242 - $30 (TECH30) - $15 (WELCOME15) = $197`.
|
|
27
|
+
By adding the `$10` cable (`$252` gross), the customer unlocks `MEGA45_250` (`-$45`) + `WELCOME15` (`-$15`) for a net total of **$192**—saving an extra **$5** while getting an extra item!
|
|
28
|
+
|
|
29
|
+
## Implementation in WaSHI
|
|
30
|
+
|
|
31
|
+
<CodeRunner preset="cart_coupon">
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Model, sum, minimize, expr } from "@ohtaman/washi";
|
|
35
|
+
|
|
36
|
+
async function optimizeCheckoutCart() {
|
|
37
|
+
const m = new Model("CartCouponOptimizer");
|
|
38
|
+
|
|
39
|
+
const baseCartSubtotal = 242;
|
|
40
|
+
const baseTechSubtotal = 210;
|
|
41
|
+
|
|
42
|
+
// Binary decision: add optional $10 USB-C cable to unlock $250 tier?
|
|
43
|
+
const addUsbCable = m.binary("addon_USB_Cable");
|
|
44
|
+
const usbCablePrice = 10;
|
|
45
|
+
|
|
46
|
+
const coupons = [
|
|
47
|
+
{ id: "WELCOME15", discount: 15, minCart: 100, minTech: 0, stackable: true },
|
|
48
|
+
{ id: "TECH30", discount: 30, minCart: 0, minTech: 200, stackable: false },
|
|
49
|
+
{ id: "MEGA45_250", discount: 45, minCart: 250, minTech: 0, stackable: false },
|
|
50
|
+
{ id: "MEMBER10", discount: 10, minCart: 150, minTech: 0, stackable: true },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Binary decision variables for each coupon
|
|
54
|
+
const useCoupon = m.binary(coupons.map((c) => c.id));
|
|
55
|
+
|
|
56
|
+
const merchSpend = addUsbCable.mul(usbCablePrice).add(baseCartSubtotal);
|
|
57
|
+
const techSpend = addUsbCable.mul(usbCablePrice).add(baseTechSubtotal);
|
|
58
|
+
|
|
59
|
+
// 1. Indicator constraints: If useCoupon[id] == 1, minimum spend threshold must hold
|
|
60
|
+
for (const cpn of coupons) {
|
|
61
|
+
if (cpn.minCart > 0) {
|
|
62
|
+
m.indicator(useCoupon[cpn.id], merchSpend.ge(cpn.minCart), {
|
|
63
|
+
activeValue: 1,
|
|
64
|
+
M: 500,
|
|
65
|
+
name: `min_spend_${cpn.id}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (cpn.minTech > 0) {
|
|
69
|
+
m.indicator(useCoupon[cpn.id], techSpend.ge(cpn.minTech), {
|
|
70
|
+
activeValue: 1,
|
|
71
|
+
M: 500,
|
|
72
|
+
name: `min_tech_${cpn.id}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Mutual exclusion among non-stackable ("Exclusive") coupons
|
|
78
|
+
const exclusiveCoupons = coupons.filter((c) => !c.stackable);
|
|
79
|
+
m.constraint(
|
|
80
|
+
expr`${sum(exclusiveCoupons, (c) => useCoupon[c.id])} <= 1`,
|
|
81
|
+
"at_most_one_exclusive"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 3. Max 2 coupons per order
|
|
85
|
+
m.constraint(
|
|
86
|
+
expr`${sum(coupons, (c) => useCoupon[c.id])} <= 2`,
|
|
87
|
+
"max_two_coupons"
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Objective: Minimize final net payable amount (merchSpend - totalDiscounts)
|
|
91
|
+
const totalDiscount = sum(coupons, (c) => useCoupon[c.id].mul(c.discount));
|
|
92
|
+
m.objective(minimize(merchSpend.sub(totalDiscount)));
|
|
93
|
+
|
|
94
|
+
const status = await m.solve();
|
|
95
|
+
console.log(`Status: ${status}, Optimal Net Payable: $${m.objectiveValue}`);
|
|
96
|
+
console.log(`Recommended Add-on (+$10 USB-C Cable): ${(addUsbCable.value ?? 0) > 0.5 ? "YES" : "NO"}`);
|
|
97
|
+
for (const cpn of coupons) {
|
|
98
|
+
const applied = (useCoupon[cpn.id].value ?? 0) > 0.5;
|
|
99
|
+
console.log(` ${applied ? "✅ [APPLIED]" : " [skipped]"} ${cpn.id.padEnd(11)} (-$${cpn.discount})`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
optimizeCheckoutCart();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
</CodeRunner>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Examples & API Finder
|
|
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.github.io/washi/demo/).
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
| Example | Problem Class | Solver | Key WaSHI APIs | Description | Code Editor |
|
|
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.github.io/washi/demo/?preset=production) |
|
|
10
|
+
| **[0-1 Knapsack](./knapsack.md)** | Binary MIP | HiGHS | `m.binary(keys)`, `sum()`, `m.constraint()` | 1D dictionary of `Binary` variables and linear aggregation over item arrays. | [Playground](https://ohtaman.github.io/washi/demo/?preset=knapsack) |
|
|
11
|
+
| **[Transportation Network](./transportation.md)** | LP | HiGHS | `m.continuous(rows, cols)`, `dot()`, `m.constraint()` | 2D matrix indexing (`ship[factory][store]`) and row/column flow balance constraints. | [Playground](https://ohtaman.github.io/washi/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://ohtaman.github.io/washi/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://ohtaman.github.io/washi/demo/?preset=shift_scheduling) |
|
|
14
|
+
| **[E-Commerce Coupon Optimizer](./ecommerce-coupons.md)** | Indicator MIP | HiGHS | `m.indicator()`, `sum()`, `m.constraint()` | Checkout optimizer using `m.indicator()` for minimum spend thresholds, exclusive vs. stackable rules, and smart filler add-on suggestions. | [Playground](https://ohtaman.github.io/washi/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://ohtaman.github.io/washi/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://ohtaman.github.io/washi/demo/?preset=smt_sat_scheduling) |
|
|
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.github.io/washi/demo/?preset=conflict_diagnosis_unsat) |
|
|
18
|
+
|
|
19
|
+
## Find by WaSHI API (Reverse Index)
|
|
20
|
+
|
|
21
|
+
Looking for a specific modeling method or solver capability? Jump straight to the example that demonstrates it:
|
|
22
|
+
|
|
23
|
+
| Feature / API | Description | Example Page | Node.js Source |
|
|
24
|
+
| :--- | :--- | :--- | :--- |
|
|
25
|
+
| ``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) |
|
|
26
|
+
| `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) |
|
|
27
|
+
| `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) |
|
|
28
|
+
| `dot(matrix, vars2d)` / `dot(vector, vars)` | 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) |
|
|
29
|
+
| `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) |
|
|
30
|
+
| `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) |
|
|
31
|
+
| `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) |
|
|
32
|
+
| `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) |
|
|
33
|
+
| `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) |
|
|
34
|
+
| `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) |
|
|
35
|
+
| `solver: "glpk"` | Solving LP/MIP with GLPK plugin | [GLPK Solver Guide](../solvers/glpk.md) | [`08-glpk-solver-plugin.ts`](https://github.com/ohtaman/washi/blob/main/examples/node/08-glpk-solver-plugin.ts) |
|
|
36
|
+
|
|
37
|
+
## Running Examples Locally in Node.js
|
|
38
|
+
|
|
39
|
+
All standalone scripts are located in [`examples/node/`](https://github.com/ohtaman/washi/tree/main/examples/node):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx tsx examples/node/01-production-planning.ts
|
|
43
|
+
npx tsx examples/node/02-knapsack-problem.ts
|
|
44
|
+
npx tsx examples/node/03-portfolio-qp.ts
|
|
45
|
+
npx tsx examples/node/04-smt-security-rules.ts
|
|
46
|
+
npx tsx examples/node/05-unsat-core-diagnosis.ts
|
|
47
|
+
npx tsx examples/node/07-hybrid-smt-mip.ts
|
|
48
|
+
npx tsx examples/node/08-glpk-solver-plugin.ts
|
|
49
|
+
npx tsx examples/node/09-transportation-problem.ts
|
|
50
|
+
npx tsx examples/node/10-diet-sensitivity.ts
|
|
51
|
+
npx tsx examples/node/11-shift-scheduling.ts
|
|
52
|
+
npx tsx examples/node/12-ecommerce-coupon-optimizer.ts
|
|
53
|
+
```
|
|
@@ -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>
|