代数表达式 (expr, sum, dot)
数学表达式是决策变量与数值系数的线性或二次组合。
在不支持运算符重载的 JavaScript 与 TypeScript 中,WaSHI 提供了统一标签模板字面量 (expr)、求和函数 sum() 以及向量/矩阵点积函数 dot(),让您在保持静态类型安全的前提下编写代数公式。
| 方式 | 示例 | 中缀可读性 | 类型安全与 IDE 重命名 | 多维索引变量 (flow[i][j]) |
|---|---|---|---|---|
| 方法链 | x.mul(30).add(y.mul(50)).le(40) | 低 | 支持 | 支持 |
| 纯字符串解析 | parseConstraint("30 * x + 50 * y <= 40", resolver) | 高 | 不支持(运行时按名称查找) | 脆弱(需手动拼接字符串) |
标签模板 (expr) | expr`30 * ${x} + 50 * ${y} <= 40` | 高 | 支持(真实 TS 引用) | 支持(直接绑定对象引用) |
标签模板字面量 (expr)
类似于参数化的 sql 标签模板,expr 将静态运算符文本(+、-、*、^2、<=、>=、==)与插值 ${...} 中的值(Var、LinExpr、QuadExpr 或有限 number)分开接收。变量通过对象引用直接绑定,而非按字符串名称解析,从而避免命名冲突,并完整保留 TypeScript 类型检查与 IDE 符号重命名能力。
算术表达式(LinExpr | QuadExpr)和关系比较约束(Constraint)均可使用 expr 构建:
import { Model, expr } from "@ohtaman/washi";
const m = new Model();
const x = m.continuous("x", { lb: 0 });
const y = m.continuous("y", { lb: 0 });
// 1. 线性组合:3*x + 5*y - 10
const linearExpr = expr`3 * ${x} + 5 * ${y} - 10`;
// 2. 二次表达式:x * y + 2 * x^2
const quadExpr = expr`${x} * ${y} + 2 * ${x} * ${x}`;
// 3. 关系比较约束:
const balanceConstraint = expr`${x} + 2 * ${y} <= 40`;expr 支持的语法
- 加法与减法:
${x} + ${y} - 5 - 标量乘法与除法:
4 * ${x}、${x} / 2 - 变量相乘(二次项):
${x} * ${y}、2 * ${x} * ${x} - 幂运算与平方:
${x}^2、${x}**2 - 一元正负号:
-${x} + 10、+${x} - 圆括号分组:
2 * (${x} + ${y}) - 聚合函数 (
sum&dot):sum(${xVec}) <= 100、dot(${costs}, ${xVec})、sum(${rows}, ${(r) => grid[r][col]}) - 容器逐元素四则运算:
sum((${price} - ${cost}) * ${qty})、sum((${xVec} - ${target})^2)、dot(${price} - ${cost}, ${qty}) - 嵌套表达式:
expr可插值变量、数字、Array/Record容器(在sum或dot内部)以及嵌套的LinExpr/QuadExpr对象。
NOTE
对于包含关系比较运算符(<=、>=、==)的式子,expr(例如 expr`${x} + 2 * ${y} <= 40` )会返回一等的 Constraint 对象。 对于不含比较运算符的算术表达式,expr 会统一处理线性与二次表达式(最高 2 次):若包含二次项则返回 QuadExpr,否则返回 LinExpr。插值的 Array 或 Record 容器必须通过 sum(...) 或 dot(..., ...) 归约为标量。次数 的多项式项(如 ${x} * ${y} * ${z}、${x}^3)以及非线性约束(如 ${x} * ${y} <= 10)会抛出错误,因为 LP/MIP/QP 规范要求约束为线性且目标函数最高为二次。
高性能求和 (sum)
在对一维或二维 Array、Iterable 或 Record(字典)容器求和时,既可以在 expr`sum(...)` 内部直接书写,也可以调用独立的 sum() 辅助函数:
import { expr, sum } from "@ohtaman/washi";
const items = ["A", "B", "C"] as const;
const weights = { A: 10, B: 20, C: 30 };
const x = m.binary(items);
// 1. 在 expr 内直接对 Record 或数组求和(无需 Object.values()):
m.constraint(expr`sum(${x}) <= 2`, "max_selected");
// 2. 在 expr`sum(...)` 内进行逐元素算术运算:
const weightedExpr = expr`sum(${weights} * ${x})`;
// 3. 使用映射回调函数转换并汇总元素:
const totalWeight = sum(items, (item) => x[item].mul(weights[item]));
// 4. 独立 sum() 同样支持直接传入一维/二维 Record 或数组:
const count = sum(x);sum() 在无 AST 解析开销的情况下以 线性时间运行。
TIP
避免在循环中用 expr 逐项累加():由于 LinExpr 与 expr 的返回值是不可变的,在循环中编写 total = expr`${total} + ${x[i]}` 会在每次迭代时复制已有的全部项,导致 的时间与内存开销。汇总大量变量时,请使用 sum(x) 或 dot(coeffs, x) 以保持 线性复杂度。
向量与矩阵点积 (dot)
dot(a, b)(既可在 expr`dot(${a}, ${b})` 内部使用,也可作为独立函数 dot(a, b) 调用)用于计算数值系数(number)与变量或线性表达式(Var | LinExpr)的线性组合,支持任意操作数顺序(dot(coeffs, vars) 或 dot(vars, coeffs))。
支持的一维和二维容器结构:
- 一维数组:
number[](Var | LinExpr)[] - 一维字典:
Record<K, number>Record<K, Var | LinExpr> - 二维数组:
number[][](Var | LinExpr)[][] - 二维字典:
Record<R, Record<C, number>>Record<R, Record<C, Var | LinExpr>> - 二维混合容器:
Array<Record<C, ...>>或Record<R, Array<...>>
当数组长度不匹配、字典键缺失或叶节点包含非有限数值/非线性项时,dot() 会抛出指明具体出错路径(例如 at '[0].StoreB')的错误。
一维向量点积
import { expr, dot } from "@ohtaman/washi";
const profits = { desk: 30, chair: 50 };
const vars = m.integer(["desk", "chair"] as const);
// 在 expr 内计算 30 * vars.desk + 50 * vars.chair(等同于 dot(profits, vars))
const totalProfit = expr`dot(${profits}, ${vars})`;二维矩阵内积(Frobenius 内积)
直接支持二维数组(m.continuous(rows, cols))、嵌套字典(m.continuous(rowKeys, colKeys))或混合二维容器:
const factories = ["Tokyo", "Osaka"] as const;
const stores = ["StoreA", "StoreB"] as const;
const costs = {
Tokyo: { StoreA: 4, StoreB: 7 },
Osaka: { StoreA: 8, StoreB: 3 },
};
const ship = m.continuous(factories, stores, { lb: 0 });
// 一行代码表达所有路线的总运输成本(等同于 expr`sum(${costs} * ${ship})`):
const totalShippingCost = expr`dot(${costs}, ${ship})`;NOTE
二维 dot(A, B) 是 Frobenius 内积(Frobenius Inner Product):对于二维矩阵或嵌套字典,dot(A, B) 计算的是标量 Frobenius 内积 (即所有行列对应元素乘积之和,等同于 expr`sum(${A} * ${B})`),而不是矩阵乘法 。
expr 聚合与批量书写方式
分离运算符与数据
将数学运算符、sum(...)、dot(..., ...) 和比较符号(<=、>=、==)写在模板字符串中,并通过 ${...} 传入变量、标量和容器(Array / Record):
// 嵌套模板调用与 Object.values()(较繁琐)
const totalProfit1 = expr`${dot(profits, vars)}`;
const cap1 = expr`${sum(Object.values(vars))} <= 100`;
// 在 expr 内直接书写 sum() 与 dot()(推荐)
const totalProfit2 = expr`dot(${profits}, ${vars})`;
const cap2 = expr`sum(${vars}) <= 100`;常见 expr 聚合语法速查
| 操作 | 数学公式 | WaSHI 写法 |
|---|---|---|
| 全体求和(1D/2D 数组或字典) | m.constraint(expr`sum(${x}) <= ${C}`) | |
| 内积(1D 向量或 2D Frobenius 内积) | m.constraint(expr`dot(${c}, ${x}) <= ${B}`) | |
| 逐元素差与乘积求和 | expr`sum((${price} - ${cost}) * ${x})` | |
| 平方误差求和(QP) | expr`sum((${x} - ${target})^2)` | |
| 逐元素批量约束() | m.constraint(items.map((i) => expr`${a[i]} * ${x[i]} <= ${b[i]}`)) | |
| 二维变量按行求和(每行 ) | m.constraint(factories.map((f) => expr`sum(${ship[f]}) <= ${supply[f]}`)) | |
| 二维变量按列求和(每列 ) | m.constraint(stores.map((s) => expr`sum(${factories}, ${(f) => ship[f][s]}) >= ${demand[s]}`)) |
二次表达式 (QuadExpr 与 expr)
WaSHI 通过 HiGHS 求解器支持凸二次规划(Convex QP)目标函数。二次表达式既可以使用 expr 模板字面量编写,也可以使用 QuadExpr 与 .mul() 编程构建。
1. 使用 expr(推荐)
使用 *、^2 或 **2 书写二次项:
import { Model, expr, minimize } from "@ohtaman/washi";
const m = new Model();
const x = m.continuous("x", { lb: 0 });
const y = m.continuous("y", { lb: 0 });
// 使用 expr 定义二次目标函数:
m.objective(minimize(expr`3 * ${x} + ${x}^2 + ${y}^2`));
// 使用 expr 定义线性约束条件:
m.constraint(expr`${x} + ${y} >= 4`);2. 使用 .mul() 或 QuadExpr 方法链
也可以通过编程方式构建二次表达式:
import { Model, QuadExpr, minimize } from "@ohtaman/washi";
const m = new Model();
const x = m.continuous("x", { lb: 0 });
const y = m.continuous("y", { lb: 0 });
// 1. 使用 .mul() 构建二次项 (x * y + 2 * x^2)
const quad = x.mul(y).add(x.mul(x).mul(2));
// 2. 构建投资组合方差目标函数:w^T * Sigma * w
const assets = ["Tech", "Bonds"] as const;
const w = m.continuous(assets, { lb: 0, ub: 1 });
let risk = new QuadExpr();
risk = risk.add(w.Tech.mul(w.Tech).mul(0.04));
risk = risk.add(w.Tech.mul(w.Bonds).mul(0.005));
risk = risk.add(w.Bonds.mul(w.Bonds).mul(0.01));
m.objective(minimize(risk));NOTE
凸性要求(半正定矩阵 ):包括 HiGHS 在内的 QP 求解器要求最小化目标中的二次型矩阵 必须为半正定(PSD)(最大化时为半负定)。平方和()与协方差矩阵()天然满足半正定性;而连续变量之间的不定乘积(如直接最小化 )属于非凸问题,会被求解器拒绝。(关于在 MILP 中线性化两个二元变量乘积 的方法,请参见 建模模式与优化技巧。)
求解后求值表达式 (.value)
LinExpr 与 QuadExpr 均提供 .value 访问器(number | null)。当模型完成求解且表达式引用的所有变量均具有有限原问题解时,可以直接读取子表达式的值:
import { Model, expr, maximize } from "@ohtaman/washi";
const m = new Model();
const x = m.integer("x", { lb: 0 });
const y = m.integer("y", { lb: 0 });
const revenue = expr`30 * ${x} + 50 * ${y}`;
const laborUsed = expr`2 * ${x} + ${y}`;
m.objective(maximize(revenue));
m.constraint(expr`${x} + 2 * ${y} <= 40`, "wood_cap");
m.constraint(expr`${laborUsed} <= 50`, "labor_cap");
await m.solve();
console.log("Total Revenue:", revenue.value); // 1100 (x = 20, y = 10)
console.log("Labor Used:", laborUsed.value); // 50若模型尚未求解(或表达式中任意变量的 value === null),.value 将返回 null。
链式算术与关系方法
除 expr 外,Var、LinExpr 与 QuadExpr 还提供不可变的链式方法,便于动态构建表达式:
| 方法 | 适用对象 | 返回类型 | 说明 |
|---|---|---|---|
.add(other) | Var, LinExpr, QuadExpr | LinExpr 或 QuadExpr | 加上一个数值、Var、LinExpr 或 QuadExpr。 |
.sub(other) | Var, LinExpr, QuadExpr | LinExpr 或 QuadExpr | 减去一个数值、Var、LinExpr 或 QuadExpr。 |
.mul(other) | Var, LinExpr, QuadExpr | LinExpr 或 QuadExpr | 乘以标量 number,或将两个一次项相乘得到 QuadExpr。 |
.div(divisor) | LinExpr, QuadExpr | LinExpr 或 QuadExpr | 除以非零有限标量 number。 |
.neg() | Var, LinExpr, QuadExpr | LinExpr 或 QuadExpr | 对所有项取反()。 |
.le(rhs) | Var, LinExpr | Constraint | 构建 约束(lhs <= rhs)。 |
.ge(rhs) | Var, LinExpr | Constraint | 构建 约束(lhs >= rhs)。 |
.eq(rhs) | Var, LinExpr | Constraint | 构建 等式约束(lhs == rhs)。 |