lakutata 3.1.1 → 3.1.3

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/doc/zh/Math.md ADDED
@@ -0,0 +1,312 @@
1
+ # Lakutata Math
2
+
3
+ `@lakutata/math` 包在 Node.js 里做数学计算,符号计算和数值计算都有:公式的代数和微积分运算、方程和不等式、任意精度的十进制数、矩阵、统计和概率分布、数值解法、单位、数论。它不属于 `lakutata`,也不需要应用:在任何地方导入它的函数就能调用。它的特点是:
4
+
5
+ - **公式按纸上的写法书写**:`x^2 - 5x + 6`、`2(x + 1)`、`5!`、`sin(pi/6)`。结果也用同样的写法给出,所以能再读回来,也能转成 TeX 用于显示。
6
+ - **结果是精确的**:`0.1 + 0.2` 是 `3/10`,`sqrt(8)` 是 `2sqrt(2)`,`x^2 - 2` 的根是 `-sqrt(2)` 和 `sqrt(2)`。只有需要数值时才计算数值:可以是双精度数,也可以是指定位数的十进制数,每一位都正确。
7
+ - **符号计算和数值计算在同一个包里**:一个运算没有精确结果时(某些积分、五次多项式的根),数值方法可以直接用同一个公式。
8
+ - **由原生插件完成计算**:插件基于 SymEngine、Boost、Eigen 和 mpdecimal,是编译好的代码,所以很快。耗时的运算在插件自己的线程里执行,可以设时限、可以取消,不会占住事件循环。
9
+
10
+ `@lakutata/math` 的 README 介绍了每个函数及其选项;这篇指南按计算的种类,逐一说明这个包能算什么,每类都给出一个例子。
11
+
12
+ ```bash
13
+ npm install @lakutata/math
14
+ ```
15
+
16
+ 原生插件在安装这个包时编译,所以机器上要有 Python 3 和支持 C++17 的编译器(README 列出了各系统需要安装什么),编译要半分钟到几分钟。包管理器必须运行这个包的安装脚本:npm 会运行它,可能会提示这个脚本“not yet covered by allowScripts”(运行 `npm install-scripts approve @lakutata/math` 记录你允许它运行);Bun 和 pnpm 只为被告知可以信任的包运行安装脚本(Bun 在 package.json 的 `trustedDependencies` 里设置,pnpm 用 `pnpm approve-builds`)。没有编译好的插件时,加载这个包会抛出错误,并说明怎样编译。
17
+
18
+ ## 1. 公式和结果
19
+
20
+ `expression()` 读入一个公式。数字紧挨着名字或括号表示相乘(`2x`、`3(x + 1)`),`^` 表示乘方,常数(`pi`、`e`、`i`、`inf`)和函数(`sin`、`ln`、`sqrt`、`gamma`、`erf`、`zeta` 等)都用通常的名字;完整的列表见 README。表达式本身不会改变:每个运算都得到一个新的表达式。
21
+
22
+ ```typescript
23
+ import {createMath, expression} from '@lakutata/math'
24
+
25
+ console.log(expression('0.1 + 0.2').toString()) //3/10
26
+ console.log(expression('sqrt(8) + sin(pi/6)').toString()) //2sqrt(2) + 1/2
27
+ console.log(expression('3(x + 1)^2 - x').expand().toString()) //3x^2 + 5x + 3
28
+ console.log(expression('e^(-x^2/2)/sqrt(2pi)').toTeX()) //\frac{e^{-\frac{x^{2}}{2}} \sqrt{2}}{2 \sqrt{\pi}}
29
+ console.log(expression('cos(10^30)').toNumber(), Math.cos(1e30)) //-0.9959311944053957 -0.9999564608959665
30
+ console.log(expression('pi').toDecimal(50)) //3.1415926535897932384626433832795028841971693993751
31
+ const finance = createMath({definitions: {'rate': '0.13', 'net(x)': 'x/(1 + rate)'}})
32
+ console.log(finance.expression('net(113)').toString()) //100
33
+ console.log(finance.expression('p - net(p)').diff('p').toString()) //13/113
34
+ ```
35
+
36
+ - `toString()` 给出 `expression()` 能读回的公式,`toTeX()` 给出它的 TeX(供 KaTeX 或 MathJax 显示)。
37
+ - `toNumber()` 给出最接近的双精度数。它从精确值的十进制数字算出,所以即使双精度计算已经没有一位有效数字,结果的每一位也都是对的:作为双精度数,`1e30` 并不正好是 10^30,`Math.cos(1e30)` 算的其实是另一个数的余弦。`toDecimal(digits)` 以字符串给出指定位数的有效数字,每一位都是对的。
38
+ - `substitute({x: 2})` 把变量换成给定的值,结果仍然是精确的。
39
+ - `createMath({definitions})` 创建一个范围,其中的函数和常数用公式写出,可用的函数和包本身相同。公式会展开其中的定义:求导和解方程时都会用到它们。包直接导出的函数属于 `math`,这个范围没有任何定义。
40
+
41
+ ## 2. 代数和微积分
42
+
43
+ ```typescript
44
+ import {expression} from '@lakutata/math'
45
+
46
+ console.log(expression('x^6 - y^6').factor().toString()) //(x^2 - x*y + y^2)(x + y)(x - y)(x^2 + x*y + y^2)
47
+ console.log(expression('(x^2 - 1)/(x^2 + 2x + 1)').cancel().toString()) //(x - 1)/(x + 1)
48
+ console.log(expression('sin(x)^4 - cos(x)^4').simplify().toString()) //-cos(2x)
49
+ console.log(expression('x^2*sin(x)').diff('x').toString()) //2x*sin(x) + x^2*cos(x)
50
+ console.log(expression('x*e^x').integrate('x').toString()) //e^x*(x - 1)
51
+ console.log(expression('1/(x^2 + 1)').integrate('x', '-inf', 'inf').toString()) //pi
52
+ console.log(expression('(1 + a/x)^x').limit('x', 'inf').toString()) //e^a
53
+ console.log(expression('e^x').series('x', 0, 4).toString()) //x^3/6 + x^2/2 + x + 1
54
+ console.log(expression('sum(k^2, k, 1, n)').toString()) //n^3/3 + n^2/2 + n/6
55
+ console.log(expression('t^2*e^(-3t)').laplace('t', 's').toString()) //2/(s + 3)^3
56
+ ```
57
+
58
+ - **多项式和分式**:`expand`、`factor`(在有理数上分解,变量个数不限)、`cancel`、`together`、`apart`(部分分式)和 `completeSquare`。
59
+ - **化简**:`simplify()` 用对变量的所有取值都成立的恒等式(三角、双曲、指数、嵌套根式)寻找更短的形式;取决于变量符号的化简一律不做,所以 `sqrt(x^2)` 保持原样。
60
+ - **积分**:用部分分式、积分表、换元、分部积分,以及积分产生的特殊函数(`sin(x)/x` 的积分是 `Si(x)`)。每个原函数返回前都会求导验证。定积分在区间内的极点处分段计算;积分无限增大时结果是 `inf`。
61
+ - **极限**用 Gruntz 算法:可以取两侧或一侧的极限,点可以是有限点或无穷远,能处理各种不定式(`0/0`、`1^inf`、`inf - inf` 等)。
62
+ - **级数**(`series(variable, point, order)`)、**求和与求积**的闭式(`sum(1/k^2, k, 1, inf)` 是 `pi^2/6`)、**分段函数**(`piecewise(x^2, x < 0, x)`),以及**拉普拉斯变换**及其逆变换。
63
+
64
+ 这些运算大多有异步版本(第 11 节)。
65
+
66
+ ## 3. 方程和不等式
67
+
68
+ ```typescript
69
+ import {equation, type Inequality, parse, solve} from '@lakutata/math'
70
+
71
+ console.log(equation('x^3 - 6x^2 + 11x - 6 = 0').solve('x').map(String)) //['1', '2', '3']
72
+ console.log(equation('x^4 = 16').solve('x').map(String)) //['-2', '2', '-2i', '2i']
73
+ console.log(equation('x^4 = 16').solve('x', {domain: 'real'}).map(String)) //['-2', '2']
74
+ console.log(equation('x^2 - (a + b)x + a*b = 0').solve('x').map(String)) //['a', 'b']
75
+ const [solution] = solve(['x + y = 3', 'x - y = 1'], ['x', 'y'])
76
+ console.log(solution.x.toString(), solution.y.toString()) //2 1
77
+ console.log((parse('(x + 1)/(x - 2) >= 0') as Inequality).solve('x').set.toString()) //(-inf, -1] ∪ (2, inf)
78
+ ```
79
+
80
+ - 多项式方程先做因式分解,所以根的形式很简单(得到 1、2、3,而不是卡尔达诺公式的根式);各个因式在四次以内都精确求解,`a x^n + b` 不论几次都能精确求解。不给 `{domain: 'real'}` 时,解在复数范围内。
81
+ - `solve(equations, variables)` 解线性方程组;有无穷多解时,用自由变量表示其他变量。
82
+ - 由多项式分式构成的不等式给出它的实数解,形式是区间和值组成的集合(`MathSet`),集合可以求并、交、补,也可以判断某个值是否属于它。
83
+
84
+ ## 4. 十进制数
85
+
86
+ `decimal()` 得到一个指定精度的十进制数(默认 32 位有效数字,最多 100000 位):用于金额,或者需要很多位数字的值。每个运算都把结果正确舍入,恰好在中间时舍入到偶数。
87
+
88
+ ```typescript
89
+ import {decimal, expression} from '@lakutata/math'
90
+
91
+ console.log(decimal(0.1).add(0.2).toString()) //0.3
92
+ console.log(decimal('19.99').mul(3).toFixed(2)) //59.97
93
+ console.log(decimal(1, {precision: 50}).div(7).toString()) //0.14285714285714285714285714285714285714285714285714
94
+ console.log(decimal(2, {precision: 40}).sqrt().toString()) //1.41421356237309504880168872420969807857
95
+ console.log(expression('gamma(1/3)').toDecimal(40)) //2.678938534707747633655692940974677644129
96
+ console.log(expression('e^(pi*sqrt(163))').toDecimal(40)) //262537412640768743.9999999999992500725972
97
+ ```
98
+
99
+ - 数字按它的写法读入(`0.1` 就是十分之一,而不是双精度数的二进制值),字符串按十进制数读入(`'19.99'`),bigint 精确读入。
100
+ - 除了四则运算,十进制数还有开方、`exp` 和对数、三角和双曲函数、`gamma`、`erf`、`zeta`、`lambertw` 以及取整函数。表达式的 `toDecimal()` 能计算这些函数,还能计算更多特殊函数(`polygamma`、`beta`、`Si`、`Ei` 等)。
101
+ - `toDecimal()` 会用更多位数重新计算,直到两次结果一致,所以它给出的每一位都是对的:`e^(pi*sqrt(163))` 不是整数,尽管它小数点后的前十二位都是 9。
102
+
103
+ ## 5. 矩阵
104
+
105
+ 矩阵有两种:数值矩阵用于计算,表达式矩阵用于精确的线性代数。
106
+
107
+ ```typescript
108
+ import {expressionMatrix, linalg, matrix} from '@lakutata/math'
109
+
110
+ const a = matrix([[4, 1], [2, 3]])
111
+ console.log(linalg.solve(a, [5, 5])) //Float64Array(2) [1, 1]
112
+ console.log(linalg.inverse(a).toString(), linalg.determinant(a)) //[0.3, -0.1; -0.2, 0.4] 10
113
+ const m = expressionMatrix('[a, b; c, d]')
114
+ console.log(m.determinant().toString()) //a*d - b*c
115
+ console.log(m.inverse().toString()) //[d/(a*d - b*c), -b/(a*d - b*c); -c/(a*d - b*c), a/(a*d - b*c)]
116
+ console.log(expressionMatrix('[2, 1; 1, 2]').eigenvalues().map(({value, multiplicity}) => [value.toString(), multiplicity])) //[['1', 1], ['3', 1]]
117
+ ```
118
+
119
+ - `matrix()` 把双精度数存放在一个 `Float64Array` 里,`linalg` 用 Eigen 计算:线性方程组、最小二乘、逆矩阵、行列式、各种分解(LU、QR、SVD、Cholesky、特征值、Schur)、矩阵的指数和平方根、Sylvester 方程和 Lyapunov 方程,以及稀疏矩阵和它的直接解法、迭代解法。
120
+ - `expressionMatrix()` 的元素是表达式:行列式、逆矩阵、方程组、简化行阶梯形、零空间、特征多项式、特征值和特征向量,结果都是精确的,含变量时也一样。`jacobian()` 给出一组表达式的雅可比矩阵。
121
+
122
+ ## 6. 统计和概率
123
+
124
+ ```typescript
125
+ import {stats} from '@lakutata/math'
126
+
127
+ const values = [2, 4, 4, 4, 5, 5, 7, 9]
128
+ console.log(stats.mean(values), stats.median(values), stats.standardDeviation(values)) //5 4.5 2
129
+ console.log(stats.quantile(values, [0.25, 0.75])) //Float64Array(2) [4, 5.5]
130
+ const iq = stats.normal({mean: 100, standardDeviation: 15})
131
+ console.log(iq.cdf(130) - iq.cdf(70)) //0.9544997361036416
132
+ console.log(iq.quantile(0.975)) //129.3994597681008
133
+ console.log(stats.binomial({trials: 10, probability: 0.3}).pdf(3)) //0.2668279319999998
134
+ console.log(stats.random(42).integers(5, 1, 7)) //Float64Array(5) [1, 1, 6, 6, 5]
135
+ ```
136
+
137
+ - 一组值的统计量:和(补偿了舍入误差)、均值、中位数、众数、方差(默认按总体计算,`{normalization: 'unbiased'}` 按样本计算)、分位数、协方差、相关系数、z 分数。
138
+ - 24 种概率分布,由 Boost.Math 计算,有密度、累积分布函数、分位数、各阶矩和抽样。各个库为分布选用的参数不同,所以这里的参数都用名字给出(`{shape, scale}`);README 列出了它们在 SciPy 中对应的写法。
139
+ - `stats.random(seed)` 得到一个随机数生成器(xoshiro256**):种子相同时,在任何平台上抽出的均匀随机数和整数都相同。它不能用于密码学。
140
+
141
+ ## 7. 数值解法
142
+
143
+ 方程或积分没有精确解,或者只需要数值时,`numeric` 可以直接用同样的公式:
144
+
145
+ ```typescript
146
+ import {numeric} from '@lakutata/math'
147
+
148
+ console.log(numeric.findRoot('cos(x) = x', 'x', {bracket: [0, 1]}).root) //0.7390851332151606
149
+ console.log(numeric.findRoot('x^5 - x - 1', 'x', {bracket: [1, 2]}).root) //1.167303978261426
150
+ const {solution, converged} = numeric.solveSystem(['x^2 + y^2 = 4', 'x*y = 1'], ['x', 'y'], [2, 0])
151
+ console.log(solution, converged) //Float64Array(2) [1.9318516525781366, 0.5176380902050416] true
152
+ console.log(numeric.integrate('e^(-x^2)*ln(1 + x)', 'x', [0, Infinity]).value) //0.36623762882311717
153
+ const decay = numeric.solveODE({derivatives: '-2y', states: 'y', span: [0, 1], initial: 1}, {points: [0.5, 1]})
154
+ console.log(decay.y.data) //Float64Array(2) [0.3678795037443847, 0.13533535227865426]
155
+ ```
156
+
157
+ - `polynomialRoots` 给出多项式的全部根,包括复根;`findRoot` 在一个有根区间里或者从一个初始猜测出发求根;`solveSystem` 求非线性方程组的一个解。
158
+ - `integrate` 用 QUADPACK 的方法计算定积分(和 SciPy 的 `quad` 相同),区间可以是无穷的,端点或指定的点上可以有奇点。
159
+ - `solveODE` 解常微分方程和方程组,用显式 Runge-Kutta 方法(和 SciPy 的 RK45 相同),刚性方程可以用 Radau IIA。
160
+ - 方法不收敛时不会抛出异常,而是在结果里说明(`converged`、`completed`)。
161
+
162
+ ## 8. 单位和物理常数
163
+
164
+ ```typescript
165
+ import {constant, unit} from '@lakutata/math'
166
+
167
+ console.log(unit('5 cm + 2 inch').toString()) //10.08 cm
168
+ console.log(unit('100 km/h').to('m/s').toString()) //(250/9) m / s
169
+ console.log(unit('20 degC').to('degF').toString()) //68 degF
170
+ console.log(unit('1.5 h').split(['h', 'min']).map(String)) //['1 h', '30 min']
171
+ console.log(constant('boltzmann').mul('300 K').to('eV').toDecimal(6)) //0.025852 eV
172
+ ```
173
+
174
+ 单位表来自 mathjs(251 个单位,可以带前缀),单位的值都是精确的:`1 inch` 正好是 `2.54 cm`。量纲不同的值不能相加,也不能互相换算。`constant()` 给出 mathjs 的 52 个物理常数,取 CODATA 2022 的值;`createMath({units})` 可以添加自己的单位。
175
+
176
+ ## 9. 数论和多项式
177
+
178
+ ```typescript
179
+ import {ntheory, poly} from '@lakutata/math'
180
+
181
+ console.log(ntheory.factor(360)) //[{prime: 2n, exponent: 3}, {prime: 3n, exponent: 2}, {prime: 5n, exponent: 1}]
182
+ console.log(ntheory.isPrime(2n ** 61n - 1n)) //true
183
+ console.log(ntheory.convergents(ntheory.continuedFraction('pi', {terms: 4})).map(String)) //['3', '22/7', '333/106', '355/113']
184
+ console.log(poly.gcd('x^4 - 1', 'x^6 - 1').toString()) //x^2 - 1
185
+ console.log(poly.discriminant('a*x^2 + b*x + c', 'x').toString()) //-4a*c + b^2
186
+ ```
187
+
188
+ `ntheory` 处理整数(bigint):整除、模运算、中国剩余定理、素数和因数分解、数列和计数、连分数。`poly` 处理多项式,变量个数不限:最大公因式和最小公倍式、结式、判别式和因式分解。
189
+
190
+ ## 10. 把公式变成函数
191
+
192
+ 需要多次求值的表达式,可以编译成数值函数,或者写成代码:
193
+
194
+ ```typescript
195
+ import {expression} from '@lakutata/math'
196
+
197
+ const f = expression('x^2 + sin(y)').compile(['x', 'y'])
198
+ console.log(f(2, 0)) //4
199
+ console.log(f.batch(Float64Array.of(1, 0, 2, 0, 3, 0))) //Float64Array(3) [1, 4, 9]
200
+ console.log(expression('x^2 - 5x + 6').toJavaScript()) //Math.pow(x, 2) - 5*x + 6
201
+ console.log(expression('sqrt(x^2 + y^2)').toC()) //sqrt(pow(x, 2.0) + pow(y, 2.0))
202
+ ```
203
+
204
+ `batch()` 一次调用就算出很多行的值。`cse()` 找出几个表达式共有的部分,这样由它们写出的代码中,每个部分只计算一次。
205
+
206
+ ## 11. 耗时的运算
207
+
208
+ 运算在 JavaScript 线程里执行,其中一些可能很耗时:大的乘方展开、积分、几千位数字、大数的因数分解。运算期间事件循环只能等着。可能耗时的运算都有异步版本(`expandAsync()`、`simplifyAsync()`、`integrateAsync()`、`solveAsync()`、`toDecimalAsync()` 等,以及 `linalg`、`ntheory`、`poly` 和 `numeric` 里的异步版本),它们在原生插件的线程里执行:
209
+
210
+ ```typescript
211
+ import {createMath, MathTimeoutException} from '@lakutata/math'
212
+
213
+ const scope = createMath({timeout: 2000, concurrency: 4})
214
+ const expanded = await scope.expression('(x + y + 1)^30').expandAsync()
215
+ console.log(expanded.variables()) //['x', 'y']
216
+ try {
217
+ await scope.expression('(x + y + z + 1)^500').expandAsync({timeout: 100})
218
+ } catch (error) {
219
+ console.log(error instanceof MathTimeoutException) //true
220
+ }
221
+ const controller: AbortController = new AbortController()
222
+ setTimeout((): void => controller.abort(), 50)
223
+ await scope.expression('(x + y + z + 1)^500').expandAsync({signal: controller.signal}).catch((error: Error): void => console.log(error.name)) //MathCancelledException
224
+ ```
225
+
226
+ - 原生插件的线程数是处理器核数减一,或者环境变量 `LAKUTATA_MATH_THREADS` 给出的数。它们不是 libuv 的线程池,所以程序的文件读写和 DNS 查询不用等它们。
227
+ - `timeout`(`createMath()` 的选项,也可以在单次调用时给出;默认不限时)到时后,运算以 `MathTimeoutException` 拒绝;`signal` 取消时,以 `MathCancelledException` 拒绝。等待空闲线程的时间不计入时限。
228
+ - 被取消的运算会在下一个检查点停下,检查点在运算的各个步骤之间。单个步骤(Eigen 对矩阵的一次分解、SymEngine 的一次求导)会执行到底,线程要到那时才空出来:Promise 在时限到时就被拒绝,但计算可能还要继续一会儿。
229
+ - `concurrency`(`createMath()` 的选项)限制一个范围里同时执行的运算数,其余的运算排队等待。
230
+
231
+ ## 12. 在应用中使用
232
+
233
+ 控制器调用这个包,和其他代码调用它没有区别。处理客户端发来的公式时,请用异步运算,并传入请求的 `signal`:客户端断开时,运算会被取消。
234
+
235
+ ```typescript
236
+ import {Application, DTO} from 'lakutata'
237
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
238
+ import {POST} from 'lakutata/decorator/ctrl'
239
+ import {Expect} from 'lakutata/decorator/dto'
240
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
241
+ import {createMath, type MathScope, type Solutions} from '@lakutata/math'
242
+
243
+ const scope: MathScope = createMath({timeout: 2000})
244
+
245
+ class SolveDTO extends DTO {
246
+ @Expect(DTO.String().max(1000).required())
247
+ public equation: string
248
+ }
249
+
250
+ class SolveController extends Controller {
251
+ @POST('/solve', SolveDTO)
252
+ public async solve(input: SolveDTO): Promise<{ solutions: Solutions, set: string }> {
253
+ const solutions: Solutions = await scope.equation(input.equation).solveAsync('x', {signal: this.signal})
254
+ return {solutions: solutions, set: solutions.set.toString()}
255
+ }
256
+ }
257
+
258
+ Application.run(() => ({
259
+ id: 'solver.app',
260
+ name: 'Solver',
261
+ components: {
262
+ entrypoint: buildEntrypoints({
263
+ controllers: [SolveController],
264
+ http: expressHTTPEntrypoint({port: 8080})
265
+ })
266
+ },
267
+ bootstrap: ['entrypoint']
268
+ }))
269
+ ```
270
+
271
+ ```text
272
+ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"equation":"x^2 - 5x + 6 = 0"}'
273
+ {"solutions":["2","3"],"set":"{2, 3}"}
274
+ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"equation":"x^2 - 5x +* 6 = 0"}'
275
+ {"errno":"E_MATH_SYNTAX","errMsg":"Unexpected *"}
276
+ ```
277
+
278
+ - 这个包的异常没有 `statusCode`,所以上面写错的公式得到的响应是状态码 500 和 `{"errno": "E_MATH_SYNTAX", …}`。如果要用 400 回应客户端公式里的错误,请在动作里捕获 `MathException`,再抛出应用自己的异常(见 [Exceptions](Exceptions.md));`E_MATH_INTERNAL` 是原生插件的内部错误,保持原样。
279
+ - 公式的长度限制不了它的计算量:`(x + 1)^100000` 很短。能限制计算量的是时限。
280
+ - 这个包的值通过 `toJSON()` 转成 JSON:表达式、方程、十进制数和带单位的值转成它们的文本,解转成文本数组,矩阵转成它的各行。向量是 `Float64Array`,`JSON.stringify()` 会把它写成对象(`{"0": 1, "1": 1}`):请发送 `Array.from(vector)`。
281
+ - `lakutata package` 打出的单文件包、可执行文件和安装包都带着编译好的原生插件(`build/Release/math.node`)。请在每个目标平台上安装这个包之后再打包;原生插件的规则同样适用(见 [Packaging](Packaging.md) 和 [NativeAddons](NativeAddons.md))。
282
+ - 这个包可以在 Node.js `^22.13.0 || >=24.11.0`、Bun 和 Electron 的主进程中运行,后两者直接加载为 Node.js 编译的插件;不能在 Electron 的渲染进程和浏览器中运行。在 Bun 里,worker 线程里的插件运算还没结束就终止这个线程的话,整个进程会因为 Node-API 的致命错误退出(这是 Bun 1.4 的问题)。
283
+
284
+ ## 13. 验证方式,以及不支持的情况
285
+
286
+ 这个包的测试用大约 2500 个用例,把它的结果和 SymPy、mpmath(公式、十进制数、单位),NumPy、SciPy(矩阵、统计、概率分布、数值解法),以及 mathjs 和 nerdamer 的结果比较;测试在 macOS(arm64:Node.js 24、Bun 1.4、Electron 44)、Linux(arm64,glibc 和 musl:Node.js 24、Bun 1.4)和 Windows 11(arm64:Node.js 24)上运行。
287
+
288
+ 它不支持的情况,有的是有意为之,有的是还没做:
289
+
290
+ - 分母要借助代数数才能分解的分式的积分(`1/(x^4 + 1)`),以及原函数既不是初等函数、也不是积分产生的特殊函数的积分,会抛出 `MathNotSupportedException`;求不出的积分不会保留成未计算的积分形式。定积分可以用 `numeric.integrate()` 计算。
291
+ - `solve()` 解单个方程或线性方程组;非线性方程组请用 `numeric.solveSystem()`。有无穷多个解的方程(如 `sin(x) = 0`)不给出解的数组:它的 `set` 只描述这些解(`{2pi*n + pi | n integer} or {2pi*n | n integer}`),集合的运算也不接受它。
292
+ - 不等式两边必须是有理系数的多项式分式。
293
+ - 表达式矩阵的精确特征值只能求到四次;数值特征值用 `linalg.eigen()` 求。
294
+ - 不做任何取决于变量符号的处理:`sqrt(x^2)` 不等于 `abs(x)`,取决于参数符号的极限(`exp(a*x)` 在 `inf` 处)求不出来。
295
+ - 精确数最多约 315000 位:更大的乘方保持乘方的形式(`2^(10^7)`)。
296
+
297
+ ## 14. 异常
298
+
299
+ 所有异常都继承 `MathException`。公式里的错误会给出出错字符的位置 `position`。每种异常的全部情况见这个包的 README。
300
+
301
+ | 异常 | `errno` | 什么时候 |
302
+ |---|---|---|
303
+ | `MathSyntaxException` | `E_MATH_SYNTAX` | 公式写得不对;需要表达式的地方给了方程,或者反过来 |
304
+ | `MathUnknownFunctionException` | `E_MATH_UNKNOWN_FUNCTION` | 公式调用了一个既不是内置、也没有定义的函数 |
305
+ | `MathDefinitionException` | `E_MATH_DEFINITION` | `createMath()` 的定义或单位无效 |
306
+ | `MathUnitException` | `E_MATH_UNIT` | 量纲不同的单位相加、比较或换算;未知的单位 |
307
+ | `MathDomainException` | `E_MATH_DOMAIN` | 值超出函数的定义域:把复数值当作数取用、对奇异矩阵求逆、极限不存在 |
308
+ | `MathDivisionByZeroException` | `E_MATH_DIVISION_BY_ZERO` | 计算十进制值时除以零、模为 0 |
309
+ | `MathNotSupportedException` | `E_MATH_NOT_SUPPORTED` | 这个包对这个公式不做的运算(第 13 节) |
310
+ | `MathArgumentException` | `E_MATH_ARGUMENT` | 参数无效:需要数的地方还剩下变量、矩阵的维数不对 |
311
+ | `MathTimeoutException`、`MathCancelledException` | `E_MATH_TIMEOUT`、`E_MATH_CANCELLED` | 异步运算超过时限,或者被取消 |
312
+ | `MathException` | `E_MATH_INTERNAL` | 原生插件的内部错误 |
@@ -1,6 +1,6 @@
1
1
  # Lakutata 原生插件
2
2
 
3
- 原生插件是用 C++ 或 Rust 编写、由 Node.js 以 `.node` 文件加载的库:用于驱动和设备、编解码、系统 API,或者必须跑得快的代码。`lakutata add --native` 在工作区中创建一个原生插件包,可以直接构建和测试;`lakutata native` 给它链接库;`lakutata package` 把它和应用一起发布。这些插件使用 Node-API,所以同一份构建能在所有受支持的 Node.js 版本和 Electron 中运行。
3
+ 原生插件是用 C++ 或 Rust 编写、由 Node.js 以 `.node` 文件加载的库:用于驱动和设备、编解码、系统 API,或者必须跑得快的代码。`lakutata add --native` 在工作区中创建一个原生插件包,可以直接构建和测试;`lakutata native` 给它链接库;`lakutata package` 把它和应用一起发布。这些插件使用 Node-API,所以同一份构建能在所有受支持的 Node.js 版本和 Electron 中运行。不写插件、直接调用库里的 C 函数,见 [FFI](FFI.md)。
4
4
 
5
5
  ## 1. 创建原生插件
6
6
 
@@ -175,6 +175,7 @@ lakutata native link libc --crate # 一个 crate(Rust:carg
175
175
  应用依赖这个插件包,并像导入其他包一样导入它(`import {greet} from '@acme/capture'`)。
176
176
 
177
177
  - **单文件包和可执行文件**:`lakutata package bundle` 把插件包排除在单文件包之外,连同它们的文件(JavaScript、插件本身、它的动态库,但不包括源码和构建时产生的文件)写入 `dist/<name>.native/`,这个目录随单文件包一起发布;可执行文件则把它们嵌入其中,或者在目录布局中放在自己旁边(见 [Packaging](Packaging.md))。插件是为打包所在机器的平台构建的,所以每个平台的应用要在那个平台上打包(用 CI 的矩阵,Linux 也可以用对应架构的容器)。
178
+ - **基于框架的插件包**(它依赖 `lakutata` 或 `@lakutata/core`,比如为了提供一个组件)则和应用一起打进单文件包,这样框架只有一份,只有插件和它旁边的文件放进这个目录。插件要用下面几种方式之一加载,才会被放进去:像 `lakutata add` 生成的插件包那样,把包的目录交给 `node-gyp-build`;直接 `require` 插件文件;或者用从模块位置构造的路径(`path.join(__dirname, 'prebuilds', ...)`)。用其他方式加载的插件,`lakutata package` 会发出警告。
178
179
  - **Electron**:插件按安装时的样子复制。`lakutata add` 创建的插件不需要重新构建就能在 Electron 中运行,所有基于 Node-API 的插件都是如此;基于 Node.js V8 API 的插件,打包前要安装它为 Electron 构建的版本(`electron-rebuild`)。
179
180
  - **Bun**:Bun 能加载 Node-API 的插件;Bun 可执行文件会对基于 Node.js V8 API 的插件发出警告。
180
181
  - **Alpine(musl)**:Node.js 的可执行文件在那里不能加载原生插件,请用 Bun 制作。
@@ -35,7 +35,7 @@
35
35
  | `lakutata/security`、`lakutata/provider/passwordHash` | 哈希、加解密、签名、`PasswordHash` 提供者 |
36
36
  | `lakutata/utils` | 辅助函数 |
37
37
 
38
- 不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)和 `@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)。
38
+ 不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)、`@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)、`@lakutata/ffi`(调用动态库中的 C 函数)和 `@lakutata/math`(符号计算和数值计算,直接作为函数使用,不需要 lakutata 应用)。
39
39
 
40
40
  ## 指南
41
41
 
@@ -79,6 +79,8 @@
79
79
  | [DBus](DBus.md) | D-Bus 服务和客户端(`@lakutata/dbus`):方法、对象、属性、信号 |
80
80
  | [MCP](MCP.md) | 模型上下文协议(`@lakutata/mcp`)的工具、资源和提示词,基于 stdio 和 HTTP |
81
81
  | [WebRTC](WebRTC.md) | 点对点连接和媒体服务器(`@lakutata/webrtc`):信令、TURN 服务器、浏览器和应用、通过数据通道调用应用的动作、WHIP 和 WHEP |
82
+ | [FFI](FFI.md) | 调用动态库中的 C 函数(`@lakutata/ffi`):从头文件复制的声明、结构体、回调、内存、打包 |
83
+ | [Math](Math.md) | 数学计算(`@lakutata/math`):公式和精确结果、方程、任意精度的十进制数、矩阵、统计、数值解法、单位 |
82
84
  | [Command](Command.md) | `lakutata` 命令:创建项目、迁移、翻译、环境变量 |
83
85
  | [Workspaces](Workspaces.md) | 由多个包组成的项目:运行各个包的脚本、统一版本号、添加包 |
84
86
  | [NativeAddons](NativeAddons.md) | 用 C++ 或 Rust 编写的原生插件:创建、链接库、发布 |
@@ -22,7 +22,7 @@
22
22
 
23
23
  - **Node.js** `^22.13.0 || >=24.11.0`(22.13 或更高的 22.x,或者 24.11 及以上;不包括 23)。同时更新 `engines`、CI 的版本矩阵,以及 Dockerfile 的基础镜像。
24
24
  - **支持 `"module": "Node20"` 的 TypeScript**(模板使用 TypeScript 6):`lakutata/tsconfig.json` 现在设置了这个选项(2.x 中是 `Node16`),更旧的编译器不认识它。
25
- - **`package.json`**:`"type": "module"`,`lakutata` 为 `^3.0.0`,所有 `@lakutata/*` 包使用同一个版本(它们互相依赖对方的确切版本),`@lakutata/cli` 放在 `devDependencies` 中。
25
+ - **`package.json`**:`"type": "module"`,`lakutata` 为 `^3.0.0`,`@lakutata/*` 包也写 `^3.0.0`,`@lakutata/cli` 放在 `devDependencies` 中。不要把 `lakutata` 的版本号照抄给其他包:补丁版只发布改动过的包,各包的最新版本可能不同(比如 `lakutata` 是 3.1.4,`@lakutata/ffi` 是 3.1.2)。
26
26
  - **全新安装**:删除 `node_modules` 和锁文件,再重新安装。原地升级可能留下多份 `@lakutata/core`,它们的类和装饰器元数据互不相通(跨副本的 `instanceof` 和注入都会失败)。`npm ls @lakutata/core` 必须只显示一个版本。
27
27
  - **`tsconfig.json`** 继承 `lakutata/tsconfig.json`,并设置自己的 `rootDir`、`outDir` 和 `include`。要保留 `include`:发布的选项设置了 `allowJs`,没有它的话,项目根目录下的配置文件(`eslint.config.js`)也会被编译。保留自己选项的项目需要 `experimentalDecorators`、`emitDecoratorMetadata` 和 `useDefineForClassFields: false`。
28
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakutata",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "An IoC application framework for Node.js and TypeScript: one programming model for web services, CLI tools, desktop (Electron) and embedded applications.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -362,7 +362,7 @@
362
362
  "@lakutata/i18n": "3.1.1",
363
363
  "@lakutata/logger": "3.1.1",
364
364
  "@lakutata/monitor": "3.1.1",
365
- "@lakutata/mutex": "3.1.1",
365
+ "@lakutata/mutex": "3.1.3",
366
366
  "@lakutata/orm": "3.1.1",
367
367
  "@lakutata/queue": "3.1.1",
368
368
  "@lakutata/security": "3.1.1",