@ohtaman/washi 0.3.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.
Files changed (97) hide show
  1. package/README.ja.md +111 -118
  2. package/README.md +109 -116
  3. package/README.zh-CN.md +112 -119
  4. package/dist/browser/highs-worker.js +21 -3
  5. package/dist/browser/loader-CtaDHef_.js +273 -0
  6. package/dist/browser/solvers/glpk.js +433 -286
  7. package/dist/browser/solvers/glpk.min.js +1 -1
  8. package/dist/browser/solvers/z3.js +1002 -981
  9. package/dist/browser/solvers/z3.min.js +2 -2
  10. package/dist/browser/washi.js +2178 -622
  11. package/dist/browser/washi.min.js +4 -4
  12. package/dist/lib/highs-worker.js +21 -3
  13. package/dist/lib/loader-DmNC_UNk.js +279 -0
  14. package/dist/lib/registry-BEfvpZW3.js +951 -0
  15. package/dist/lib/solvers/glpk.js +201 -90
  16. package/dist/lib/solvers/z3.js +399 -259
  17. package/dist/lib/washi.js +1674 -932
  18. package/dist/types/index.d.ts +4 -4
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/modeler/big-m.d.ts +75 -0
  21. package/dist/types/modeler/big-m.d.ts.map +1 -0
  22. package/dist/types/modeler/boolean-expr.d.ts +103 -14
  23. package/dist/types/modeler/boolean-expr.d.ts.map +1 -1
  24. package/dist/types/modeler/constraint.d.ts +36 -9
  25. package/dist/types/modeler/constraint.d.ts.map +1 -1
  26. package/dist/types/modeler/expression.d.ts +117 -27
  27. package/dist/types/modeler/expression.d.ts.map +1 -1
  28. package/dist/types/modeler/index.d.ts +10 -6
  29. package/dist/types/modeler/index.d.ts.map +1 -1
  30. package/dist/types/modeler/lp-writer.d.ts +27 -2
  31. package/dist/types/modeler/lp-writer.d.ts.map +1 -1
  32. package/dist/types/modeler/model.d.ts +491 -140
  33. package/dist/types/modeler/model.d.ts.map +1 -1
  34. package/dist/types/modeler/objective.d.ts +59 -0
  35. package/dist/types/modeler/objective.d.ts.map +1 -0
  36. package/dist/types/modeler/parser.d.ts +39 -14
  37. package/dist/types/modeler/parser.d.ts.map +1 -1
  38. package/dist/types/modeler/quad-expr.d.ts +28 -7
  39. package/dist/types/modeler/quad-expr.d.ts.map +1 -1
  40. package/dist/types/modeler/solution-validator.d.ts +93 -0
  41. package/dist/types/modeler/solution-validator.d.ts.map +1 -0
  42. package/dist/types/modeler/solvers/glpk/solver.d.ts +26 -2
  43. package/dist/types/modeler/solvers/glpk/solver.d.ts.map +1 -1
  44. package/dist/types/modeler/solvers/highs/loader.d.ts +36 -11
  45. package/dist/types/modeler/solvers/highs/loader.d.ts.map +1 -1
  46. package/dist/types/modeler/solvers/highs/solver.d.ts +11 -4
  47. package/dist/types/modeler/solvers/highs/solver.d.ts.map +1 -1
  48. package/dist/types/modeler/solvers/highs/worker-client.d.ts.map +1 -1
  49. package/dist/types/modeler/solvers/mock.d.ts +14 -5
  50. package/dist/types/modeler/solvers/mock.d.ts.map +1 -1
  51. package/dist/types/modeler/solvers/types.d.ts +71 -11
  52. package/dist/types/modeler/solvers/types.d.ts.map +1 -1
  53. package/dist/types/modeler/solvers/z3/loader.d.ts +2 -3
  54. package/dist/types/modeler/solvers/z3/loader.d.ts.map +1 -1
  55. package/dist/types/modeler/solvers/z3/solver.d.ts +15 -5
  56. package/dist/types/modeler/solvers/z3/solver.d.ts.map +1 -1
  57. package/dist/types/modeler/types.d.ts +60 -2
  58. package/dist/types/modeler/types.d.ts.map +1 -1
  59. package/dist/types/modeler/validation.d.ts +56 -0
  60. package/dist/types/modeler/validation.d.ts.map +1 -0
  61. package/dist/types/modeler/variable.d.ts +53 -13
  62. package/dist/types/modeler/variable.d.ts.map +1 -1
  63. package/docs/examples/diet-sensitivity.md +90 -0
  64. package/docs/examples/ecommerce-coupons.md +106 -0
  65. package/docs/examples/index.md +53 -0
  66. package/docs/examples/knapsack.md +60 -0
  67. package/docs/examples/portfolio-qp.md +83 -0
  68. package/docs/examples/production.md +51 -0
  69. package/docs/examples/shift-scheduling.md +175 -0
  70. package/docs/examples/smt-security.md +74 -0
  71. package/docs/examples/transportation.md +90 -0
  72. package/docs/examples/unsat-core.md +64 -0
  73. package/docs/getting-started/ai-agents.md +267 -0
  74. package/docs/getting-started/cross-origin.md +87 -0
  75. package/docs/getting-started/index.md +116 -0
  76. package/docs/getting-started/runtimes.md +227 -0
  77. package/docs/guide/constraints.md +177 -0
  78. package/docs/guide/expressions.md +204 -0
  79. package/docs/guide/objectives.md +170 -0
  80. package/docs/guide/unsat-core.md +147 -0
  81. package/docs/guide/variables.md +188 -0
  82. package/docs/ja/getting-started/ai-agents.md +266 -0
  83. package/docs/solvers/custom-solver.md +525 -0
  84. package/docs/solvers/glpk.md +66 -0
  85. package/docs/solvers/highs.md +93 -0
  86. package/docs/solvers/index.md +42 -0
  87. package/docs/solvers/z3.md +78 -0
  88. package/docs/zh/getting-started/ai-agents.md +266 -0
  89. package/llms-full.txt +3249 -0
  90. package/llms.txt +197 -0
  91. package/package.json +19 -6
  92. package/skills/washi/SKILL.md +197 -0
  93. package/dist/browser/loader-CVt4CloI.js +0 -58
  94. package/dist/lib/async-mutex-Duo4jwzn.js +0 -195
  95. package/dist/lib/constraint-BRuB3ooC.js +0 -90
  96. package/dist/lib/loader-Dti0v9yC.js +0 -50
  97. 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>