lakutata 3.1.2 → 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/README.md CHANGED
@@ -144,8 +144,9 @@ on. Each package's README lists its exceptions.
144
144
  `Entrypoint.md`, `Middlewares.md`, `HTTP.md`, `CLI.md`, `ServiceCalls.md`, `GRPC.md`, `NATS.md`, `Schedules.md`,
145
145
  `Queue.md`, `Database.md`, `DatabaseDrivers.md`, `MongoEmbedded.md`, `Entities.md`, `Migrations.md`, `Cache.md`,
146
146
  `Mutex.md`, `I18n.md`, `DockerIntegration.md`, `ContainerdIntegration.md`, `Monitor.md`, `Security.md`, `DBus.md`,
147
- `MCP.md`, `WebRTC.md`, `FFI.md`, `Command.md`, `Workspaces.md`, `NativeAddons.md`, `Packaging.md`, `Installers.md`;
148
- and the examples, in `doc/en/examples` and `doc/zh/examples`: whole applications built from the templates.
147
+ `MCP.md`, `WebRTC.md`, `FFI.md`, `Math.md`, `Command.md`, `Workspaces.md`, `NativeAddons.md`, `Packaging.md`,
148
+ `Installers.md`; and the examples, in `doc/en/examples` and `doc/zh/examples`: whole applications built from the
149
+ templates.
149
150
  - `@lakutata/cli`: creating, building and packaging applications.
150
151
  - The official Agent Skill for AI coding assistants that work on lakutata projects:
151
152
  [lakutata.com/skill](https://lakutata.com/skill/).
package/README.zh-CN.md CHANGED
@@ -121,7 +121,7 @@ Application
121
121
  ## 另见
122
122
 
123
123
  - 网站 <https://lakutata.com/zh/>:指南、示例和 API 参考,中英文都有。
124
- - 指南在这个包的 `doc/zh`(中文)和 `doc/en`(英文)中:`Overview.md`、`GettingStarted.md`、`ProjectStructure.md`、`UpgradingFrom2.md`、`UpgradingFrom1.md`、`Application.md`、`Objects.md`、`Injection.md`、`DTOValidation.md`、`Exceptions.md`、`Logger.md`、`Decorator.md`、`Time.md`、`HelperFunctions.md`、`Controllers.md`、`Entrypoint.md`、`Middlewares.md`、`HTTP.md`、`CLI.md`、`ServiceCalls.md`、`GRPC.md`、`NATS.md`、`Schedules.md`、`Queue.md`、`Database.md`、`DatabaseDrivers.md`、`MongoEmbedded.md`、`Entities.md`、`Migrations.md`、`Cache.md`、`Mutex.md`、`I18n.md`、`DockerIntegration.md`、`ContainerdIntegration.md`、`Monitor.md`、`Security.md`、`DBus.md`、`MCP.md`、`WebRTC.md`、`FFI.md`、`Command.md`、`Workspaces.md`、`NativeAddons.md`、`Packaging.md`、`Installers.md`;示例在 `doc/zh/examples` 和 `doc/en/examples` 中:从模板构建的完整应用。
124
+ - 指南在这个包的 `doc/zh`(中文)和 `doc/en`(英文)中:`Overview.md`、`GettingStarted.md`、`ProjectStructure.md`、`UpgradingFrom2.md`、`UpgradingFrom1.md`、`Application.md`、`Objects.md`、`Injection.md`、`DTOValidation.md`、`Exceptions.md`、`Logger.md`、`Decorator.md`、`Time.md`、`HelperFunctions.md`、`Controllers.md`、`Entrypoint.md`、`Middlewares.md`、`HTTP.md`、`CLI.md`、`ServiceCalls.md`、`GRPC.md`、`NATS.md`、`Schedules.md`、`Queue.md`、`Database.md`、`DatabaseDrivers.md`、`MongoEmbedded.md`、`Entities.md`、`Migrations.md`、`Cache.md`、`Mutex.md`、`I18n.md`、`DockerIntegration.md`、`ContainerdIntegration.md`、`Monitor.md`、`Security.md`、`DBus.md`、`MCP.md`、`WebRTC.md`、`FFI.md`、`Math.md`、`Command.md`、`Workspaces.md`、`NativeAddons.md`、`Packaging.md`、`Installers.md`;示例在 `doc/zh/examples` 和 `doc/en/examples` 中:从模板构建的完整应用。
125
125
  - `@lakutata/cli`:创建、构建和打包应用。
126
126
  - 官方 Agent Skill(智能体技能),供 AI 编程助手在 lakutata 项目中使用:<https://lakutata.com/zh/skill/>。
127
127
 
package/doc/en/Math.md ADDED
@@ -0,0 +1,394 @@
1
+ # Lakutata Math
2
+
3
+ The package `@lakutata/math` does mathematics in Node.js, both symbolic and numeric: algebra and calculus on formulas,
4
+ equations and inequalities, decimals of any precision, matrices, statistics and probability distributions, numeric
5
+ solutions, units, number theory. It is not part of `lakutata`, and it needs no application: its functions are imported
6
+ and called anywhere. What it is like:
7
+
8
+ - **Formulas are written as on paper**: `x^2 - 5x + 6`, `2(x + 1)`, `5!`, `sin(pi/6)`. A result is written the same
9
+ way, so it can be read back, and in TeX for display.
10
+ - **Results are exact**: `0.1 + 0.2` is `3/10`, `sqrt(8)` is `2sqrt(2)`, the roots of `x^2 - 2` are `-sqrt(2)` and
11
+ `sqrt(2)`. A number is computed only when one is asked for: a double, or as many correct decimal digits as wanted.
12
+ - **Symbolic and numeric in one package**: when an operation has no exact answer (an integral, the roots of a polynomial
13
+ of degree five), the numeric methods take the same formula.
14
+ - **A native addon** does the work, built on SymEngine, Boost, Eigen and mpdecimal: it is compiled code, so it is fast.
15
+ The long operations run in its own threads, with a time limit and a cancellation, so the event loop stays free.
16
+
17
+ The README of `@lakutata/math` describes every function and its options; this guide goes through what the package
18
+ computes, with an example of each kind.
19
+
20
+ ```bash
21
+ npm install @lakutata/math
22
+ ```
23
+
24
+ The addon is compiled when the package is installed, so the machine needs Python 3 and a C++17 compiler (the README
25
+ lists them for each system); it takes from about half a minute to a few minutes. The package manager has to run the
26
+ install script of the package: npm runs it, and may warn that the script is "not yet covered by allowScripts"
27
+ (`npm install-scripts approve @lakutata/math` records that you allow it); Bun and pnpm run it only for the packages they
28
+ are told to trust (`trustedDependencies` in the package.json for Bun, `pnpm approve-builds` for pnpm). Without the
29
+ addon, loading the package throws an error that tells how to build it.
30
+
31
+ ## 1. Formulas and results
32
+
33
+ `expression()` reads a formula. A number next to a name or a parenthesis multiplies it (`2x`, `3(x + 1)`), `^` is a
34
+ power, and the constants (`pi`, `e`, `i`, `inf`) and the functions (`sin`, `ln`, `sqrt`, `gamma`, `erf`, `zeta`…) have
35
+ their usual names; the README lists them all. An expression does not change: each operation gives a new one.
36
+
37
+ ```typescript
38
+ import {createMath, expression} from '@lakutata/math'
39
+
40
+ console.log(expression('0.1 + 0.2').toString()) //3/10
41
+ console.log(expression('sqrt(8) + sin(pi/6)').toString()) //2sqrt(2) + 1/2
42
+ console.log(expression('3(x + 1)^2 - x').expand().toString()) //3x^2 + 5x + 3
43
+ console.log(expression('e^(-x^2/2)/sqrt(2pi)').toTeX()) //\frac{e^{-\frac{x^{2}}{2}} \sqrt{2}}{2 \sqrt{\pi}}
44
+ console.log(expression('cos(10^30)').toNumber(), Math.cos(1e30)) //-0.9959311944053957 -0.9999564608959665
45
+ console.log(expression('pi').toDecimal(50)) //3.1415926535897932384626433832795028841971693993751
46
+ const finance = createMath({definitions: {'rate': '0.13', 'net(x)': 'x/(1 + rate)'}})
47
+ console.log(finance.expression('net(113)').toString()) //100
48
+ console.log(finance.expression('p - net(p)').diff('p').toString()) //13/113
49
+ ```
50
+
51
+ - `toString()` gives a formula that `expression()` reads back, `toTeX()` its TeX (for KaTeX or MathJax).
52
+ - `toNumber()` gives the nearest double. It is computed from the decimals of the exact value, so all its digits are
53
+ right, even where doubles have none: as a double, `1e30` is not exactly 10^30, and `Math.cos(1e30)` is the cosine of
54
+ another number. `toDecimal(digits)` gives that many correct significant digits, as a string.
55
+ - `substitute({x: 2})` puts values in for the variables; the result stays exact.
56
+ - `createMath({definitions})` makes a scope of functions and constants written as formulas, with the same functions
57
+ as the package. A formula sees through its definitions: derivatives and equations use them. The functions the package
58
+ exports belong to `math`, a scope with no definitions.
59
+
60
+ ## 2. Algebra and calculus
61
+
62
+ ```typescript
63
+ import {expression} from '@lakutata/math'
64
+
65
+ 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)
66
+ console.log(expression('(x^2 - 1)/(x^2 + 2x + 1)').cancel().toString()) //(x - 1)/(x + 1)
67
+ console.log(expression('sin(x)^4 - cos(x)^4').simplify().toString()) //-cos(2x)
68
+ console.log(expression('x^2*sin(x)').diff('x').toString()) //2x*sin(x) + x^2*cos(x)
69
+ console.log(expression('x*e^x').integrate('x').toString()) //e^x*(x - 1)
70
+ console.log(expression('1/(x^2 + 1)').integrate('x', '-inf', 'inf').toString()) //pi
71
+ console.log(expression('(1 + a/x)^x').limit('x', 'inf').toString()) //e^a
72
+ console.log(expression('e^x').series('x', 0, 4).toString()) //x^3/6 + x^2/2 + x + 1
73
+ console.log(expression('sum(k^2, k, 1, n)').toString()) //n^3/3 + n^2/2 + n/6
74
+ console.log(expression('t^2*e^(-3t)').laplace('t', 's').toString()) //2/(s + 3)^3
75
+ ```
76
+
77
+ - **Polynomials and fractions**: `expand`, `factor` (over the rationals, in any number of variables), `cancel`,
78
+ `together`, `apart` (partial fractions) and `completeSquare`.
79
+ - **Simplification**: `simplify()` looks for a shorter form, by identities that hold for every value of the variables
80
+ (trigonometric, hyperbolic, exponential, nested roots); nothing that depends on the signs of the variables, so
81
+ `sqrt(x^2)` stays as it is.
82
+ - **Integrals**: by partial fractions, a table, substitution, integration by parts, and the special functions of
83
+ integrals (`sin(x)/x` gives `Si(x)`). Each antiderivative is checked by its derivative before it is returned. A
84
+ definite integral is split at the poles inside its interval; it is `inf` where it grows without bound.
85
+ - **Limits** by Gruntz's algorithm, from both sides or from one, at a point or at infinity, with the indeterminate
86
+ forms (`0/0`, `1^inf`, `inf - inf`…).
87
+ - **Series** (`series(variable, point, order)`), **sums and products** in closed form (`sum(1/k^2, k, 1, inf)` is
88
+ `pi^2/6`), **piecewise** expressions (`piecewise(x^2, x < 0, x)`), and the **Laplace transform** and its inverse.
89
+
90
+ Most of these operations have an asynchronous version (section 11).
91
+
92
+ ## 3. Equations and inequalities
93
+
94
+ ```typescript
95
+ import {equation, type Inequality, parse, solve} from '@lakutata/math'
96
+
97
+ console.log(equation('x^3 - 6x^2 + 11x - 6 = 0').solve('x').map(String)) //['1', '2', '3']
98
+ console.log(equation('x^4 = 16').solve('x').map(String)) //['-2', '2', '-2i', '2i']
99
+ console.log(equation('x^4 = 16').solve('x', {domain: 'real'}).map(String)) //['-2', '2']
100
+ console.log(equation('x^2 - (a + b)x + a*b = 0').solve('x').map(String)) //['a', 'b']
101
+ const [solution] = solve(['x + y = 3', 'x - y = 1'], ['x', 'y'])
102
+ console.log(solution.x.toString(), solution.y.toString()) //2 1
103
+ console.log((parse('(x + 1)/(x - 2) >= 0') as Inequality).solve('x').set.toString()) //(-inf, -1] ∪ (2, inf)
104
+ ```
105
+
106
+ - A polynomial equation is factored first, so its roots come out simple (1, 2, 3, not the radicals of Cardano's
107
+ formula); the factors are solved exactly up to degree four, and `a x^n + b` of any degree. The solutions are complex
108
+ numbers unless `{domain: 'real'}` is given.
109
+ - `solve(equations, variables)` solves a system of linear equations, in terms of the free variables when it has
110
+ infinitely many solutions.
111
+ - An inequality of fractions of polynomials gives its real solutions as a set of intervals and values (`MathSet`), with
112
+ union, intersection, complement and membership.
113
+
114
+ ## 4. Decimals
115
+
116
+ `decimal()` gives a decimal number with a chosen precision (32 significant digits by default, up to 100000): for amounts
117
+ of money, or for values that need many digits. Each operation rounds its result correctly, halves to even.
118
+
119
+ ```typescript
120
+ import {decimal, expression} from '@lakutata/math'
121
+
122
+ console.log(decimal(0.1).add(0.2).toString()) //0.3
123
+ console.log(decimal('19.99').mul(3).toFixed(2)) //59.97
124
+ console.log(decimal(1, {precision: 50}).div(7).toString()) //0.14285714285714285714285714285714285714285714285714
125
+ console.log(decimal(2, {precision: 40}).sqrt().toString()) //1.41421356237309504880168872420969807857
126
+ console.log(expression('gamma(1/3)').toDecimal(40)) //2.678938534707747633655692940974677644129
127
+ console.log(expression('e^(pi*sqrt(163))').toDecimal(40)) //262537412640768743.9999999999992500725972
128
+ ```
129
+
130
+ - A number is read as it is written (`0.1` is one tenth, not the binary value of the double), a string as a decimal
131
+ number (`'19.99'`), a bigint exactly.
132
+ - Besides the arithmetic, decimals have roots, `exp` and the logarithms, the trigonometric and hyperbolic functions,
133
+ `gamma`, `erf`, `zeta`, `lambertw` and the rounding functions. `toDecimal()` of an expression computes the same
134
+ functions and more special ones (`polygamma`, `beta`, `Si`, `Ei`…).
135
+ - `toDecimal()` computes the value again with more digits until two results agree, so every digit it gives is right:
136
+ `e^(pi*sqrt(163))` is not an integer, though its first twelve decimals are nines.
137
+
138
+ ## 5. Matrices
139
+
140
+ There are two kinds of matrices: of numbers, for computing, and of expressions, for exact linear algebra.
141
+
142
+ ```typescript
143
+ import {expressionMatrix, linalg, matrix} from '@lakutata/math'
144
+
145
+ const a = matrix([[4, 1], [2, 3]])
146
+ console.log(linalg.solve(a, [5, 5])) //Float64Array(2) [1, 1]
147
+ console.log(linalg.inverse(a).toString(), linalg.determinant(a)) //[0.3, -0.1; -0.2, 0.4] 10
148
+ const m = expressionMatrix('[a, b; c, d]')
149
+ console.log(m.determinant().toString()) //a*d - b*c
150
+ 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)]
151
+ console.log(expressionMatrix('[2, 1; 1, 2]').eigenvalues().map(({value, multiplicity}) => [value.toString(), multiplicity])) //[['1', 1], ['3', 1]]
152
+ ```
153
+
154
+ - `matrix()` holds doubles in a `Float64Array`, and `linalg` computes with Eigen: systems, least squares, inverses,
155
+ determinants, the decompositions (LU, QR, SVD, Cholesky, eigenvalues, Schur), the exponential and the square root of
156
+ a matrix, Sylvester and Lyapunov equations, and sparse matrices with their direct and iterative solvers.
157
+ - `expressionMatrix()` holds expressions: determinants, inverses, systems, reduced row echelon forms, null spaces,
158
+ characteristic polynomials, eigenvalues and eigenvectors, all exact, also with variables. `jacobian()` gives the
159
+ Jacobian matrix of expressions.
160
+
161
+ ## 6. Statistics and probability
162
+
163
+ ```typescript
164
+ import {stats} from '@lakutata/math'
165
+
166
+ const values = [2, 4, 4, 4, 5, 5, 7, 9]
167
+ console.log(stats.mean(values), stats.median(values), stats.standardDeviation(values)) //5 4.5 2
168
+ console.log(stats.quantile(values, [0.25, 0.75])) //Float64Array(2) [4, 5.5]
169
+ const iq = stats.normal({mean: 100, standardDeviation: 15})
170
+ console.log(iq.cdf(130) - iq.cdf(70)) //0.9544997361036416
171
+ console.log(iq.quantile(0.975)) //129.3994597681008
172
+ console.log(stats.binomial({trials: 10, probability: 0.3}).pdf(3)) //0.2668279319999998
173
+ console.log(stats.random(42).integers(5, 1, 7)) //Float64Array(5) [1, 1, 6, 6, 5]
174
+ ```
175
+
176
+ - The statistics of values: sums (compensated for their roundings), means, medians, modes, variances (of a population
177
+ by default, of a sample with `{normalization: 'unbiased'}`), quantiles, covariances, correlations, z-scores.
178
+ - 24 probability distributions, computed by Boost.Math, with their density, cdf, quantiles, moments and samples. Their
179
+ parameters are given by name (`{shape, scale}`), since libraries parameterize them differently; the README gives the
180
+ equivalents in SciPy.
181
+ - `stats.random(seed)` gives a generator (xoshiro256**): from the same seed, it draws the same uniform numbers and
182
+ integers, on every platform. It is not meant for cryptography.
183
+
184
+ ## 7. Numeric solutions
185
+
186
+ When an equation or an integral has no exact solution, or numbers are enough, `numeric` takes the same formulas:
187
+
188
+ ```typescript
189
+ import {numeric} from '@lakutata/math'
190
+
191
+ console.log(numeric.findRoot('cos(x) = x', 'x', {bracket: [0, 1]}).root) //0.7390851332151606
192
+ console.log(numeric.findRoot('x^5 - x - 1', 'x', {bracket: [1, 2]}).root) //1.167303978261426
193
+ const {solution, converged} = numeric.solveSystem(['x^2 + y^2 = 4', 'x*y = 1'], ['x', 'y'], [2, 0])
194
+ console.log(solution, converged) //Float64Array(2) [1.9318516525781366, 0.5176380902050416] true
195
+ console.log(numeric.integrate('e^(-x^2)*ln(1 + x)', 'x', [0, Infinity]).value) //0.36623762882311717
196
+ const decay = numeric.solveODE({derivatives: '-2y', states: 'y', span: [0, 1], initial: 1}, {points: [0.5, 1]})
197
+ console.log(decay.y.data) //Float64Array(2) [0.3678795037443847, 0.13533535227865426]
198
+ ```
199
+
200
+ - `polynomialRoots` gives all the roots of a polynomial, complex ones too; `findRoot` a root in a bracket or from a
201
+ guess; `solveSystem` a solution of nonlinear equations.
202
+ - `integrate` computes a definite integral by QUADPACK's method (as SciPy's `quad`), on infinite intervals too, and
203
+ with singularities at the ends or at given points.
204
+ - `solveODE` solves ordinary differential equations and systems of them, by an explicit Runge-Kutta method (as SciPy's
205
+ RK45) or, for stiff equations, by Radau IIA.
206
+ - When a method does not converge, its result says so (`converged`, `completed`) instead of throwing.
207
+
208
+ ## 8. Units and physical constants
209
+
210
+ ```typescript
211
+ import {constant, unit} from '@lakutata/math'
212
+
213
+ console.log(unit('5 cm + 2 inch').toString()) //10.08 cm
214
+ console.log(unit('100 km/h').to('m/s').toString()) //(250/9) m / s
215
+ console.log(unit('20 degC').to('degF').toString()) //68 degF
216
+ console.log(unit('1.5 h').split(['h', 'min']).map(String)) //['1 h', '30 min']
217
+ console.log(constant('boltzmann').mul('300 K').to('eV').toDecimal(6)) //0.025852 eV
218
+ ```
219
+
220
+ The table of units is that of mathjs (251 units, with their prefixes), with exact values: `1 inch` is exactly `2.54 cm`.
221
+ Values of different dimensions cannot be added or converted. `constant()` gives the 52 physical constants of mathjs,
222
+ with the values of CODATA 2022, and `createMath({units})` adds units of your own.
223
+
224
+ ## 9. Number theory and polynomials
225
+
226
+ ```typescript
227
+ import {ntheory, poly} from '@lakutata/math'
228
+
229
+ console.log(ntheory.factor(360)) //[{prime: 2n, exponent: 3}, {prime: 3n, exponent: 2}, {prime: 5n, exponent: 1}]
230
+ console.log(ntheory.isPrime(2n ** 61n - 1n)) //true
231
+ console.log(ntheory.convergents(ntheory.continuedFraction('pi', {terms: 4})).map(String)) //['3', '22/7', '333/106', '355/113']
232
+ console.log(poly.gcd('x^4 - 1', 'x^6 - 1').toString()) //x^2 - 1
233
+ console.log(poly.discriminant('a*x^2 + b*x + c', 'x').toString()) //-4a*c + b^2
234
+ ```
235
+
236
+ `ntheory` works on integers (bigints): divisibility, modular arithmetic, the Chinese remainder theorem, primes and
237
+ factors, sequences and counting, continued fractions. `poly` works on polynomials in any number of variables: their
238
+ greatest common divisors and least common multiples, resultants, discriminants and factors.
239
+
240
+ ## 10. Formulas as functions
241
+
242
+ An expression evaluated many times is compiled into a function of numbers, or written as code:
243
+
244
+ ```typescript
245
+ import {expression} from '@lakutata/math'
246
+
247
+ const f = expression('x^2 + sin(y)').compile(['x', 'y'])
248
+ console.log(f(2, 0)) //4
249
+ console.log(f.batch(Float64Array.of(1, 0, 2, 0, 3, 0))) //Float64Array(3) [1, 4, 9]
250
+ console.log(expression('x^2 - 5x + 6').toJavaScript()) //Math.pow(x, 2) - 5*x + 6
251
+ console.log(expression('sqrt(x^2 + y^2)').toC()) //sqrt(pow(x, 2.0) + pow(y, 2.0))
252
+ ```
253
+
254
+ `batch()` evaluates many rows of values in one call. `cse()` finds the parts that expressions share, so that the code
255
+ written from them computes each part once.
256
+
257
+ ## 11. Long operations
258
+
259
+ The operations run in the thread of JavaScript, and some of them can take long: the expansion of a large power, an
260
+ integral, thousands of digits, the factors of a large number. While one runs, the event loop waits. The operations that
261
+ can take long have an asynchronous version (`expandAsync()`, `simplifyAsync()`, `integrateAsync()`, `solveAsync()`,
262
+ `toDecimalAsync()`… and those of `linalg`, `ntheory`, `poly` and `numeric`), which runs in the threads of the addon:
263
+
264
+ ```typescript
265
+ import {createMath, MathTimeoutException} from '@lakutata/math'
266
+
267
+ const scope = createMath({timeout: 2000, concurrency: 4})
268
+ const expanded = await scope.expression('(x + y + 1)^30').expandAsync()
269
+ console.log(expanded.variables()) //['x', 'y']
270
+ try {
271
+ await scope.expression('(x + y + z + 1)^500').expandAsync({timeout: 100})
272
+ } catch (error) {
273
+ console.log(error instanceof MathTimeoutException) //true
274
+ }
275
+ const controller: AbortController = new AbortController()
276
+ setTimeout((): void => controller.abort(), 50)
277
+ await scope.expression('(x + y + z + 1)^500').expandAsync({signal: controller.signal}).catch((error: Error): void => console.log(error.name)) //MathCancelledException
278
+ ```
279
+
280
+ - The addon has as many threads as the processor has cores, minus one, or the number in the environment variable
281
+ `LAKUTATA_MATH_THREADS`. They are not the thread pool of libuv, so the files and the DNS lookups of the program do not
282
+ wait for them.
283
+ - `timeout` (an option of `createMath()`, or of one call; none by default) rejects the operation with
284
+ `MathTimeoutException`, and an aborted `signal` with `MathCancelledException`. The time spent waiting for a free
285
+ thread does not count.
286
+ - A cancelled operation stops at the next point where it checks for a cancellation, between the steps of its work. A
287
+ single step (a decomposition of a matrix by Eigen, a derivative by SymEngine) runs to its end, and the thread is free
288
+ only then: the promise is rejected at the time limit, while the work may go on for a while.
289
+ - `concurrency` (an option of `createMath()`) limits how many operations of a scope run at the same time; the others
290
+ wait their turn.
291
+
292
+ ## 12. In an application
293
+
294
+ A controller calls the package as any other code does. With a formula from a client, use an asynchronous operation and
295
+ the `signal` of the request: when the client leaves, the operation is cancelled.
296
+
297
+ ```typescript
298
+ import {Application, DTO} from 'lakutata'
299
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
300
+ import {POST} from 'lakutata/decorator/ctrl'
301
+ import {Expect} from 'lakutata/decorator/dto'
302
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
303
+ import {createMath, type MathScope, type Solutions} from '@lakutata/math'
304
+
305
+ const scope: MathScope = createMath({timeout: 2000})
306
+
307
+ class SolveDTO extends DTO {
308
+ @Expect(DTO.String().max(1000).required())
309
+ public equation: string
310
+ }
311
+
312
+ class SolveController extends Controller {
313
+ @POST('/solve', SolveDTO)
314
+ public async solve(input: SolveDTO): Promise<{ solutions: Solutions, set: string }> {
315
+ const solutions: Solutions = await scope.equation(input.equation).solveAsync('x', {signal: this.signal})
316
+ return {solutions: solutions, set: solutions.set.toString()}
317
+ }
318
+ }
319
+
320
+ Application.run(() => ({
321
+ id: 'solver.app',
322
+ name: 'Solver',
323
+ components: {
324
+ entrypoint: buildEntrypoints({
325
+ controllers: [SolveController],
326
+ http: expressHTTPEntrypoint({port: 8080})
327
+ })
328
+ },
329
+ bootstrap: ['entrypoint']
330
+ }))
331
+ ```
332
+
333
+ ```text
334
+ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"equation":"x^2 - 5x + 6 = 0"}'
335
+ {"solutions":["2","3"],"set":"{2, 3}"}
336
+ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"equation":"x^2 - 5x +* 6 = 0"}'
337
+ {"errno":"E_MATH_SYNTAX","errMsg":"Unexpected *"}
338
+ ```
339
+
340
+ - The exceptions of the package have no `statusCode`, so the wrong formula above is answered with the status 500 and
341
+ `{"errno": "E_MATH_SYNTAX", …}`. To answer the errors in the formulas of the clients with 400, catch `MathException`
342
+ in the action and throw an exception of the application (see [Exceptions](Exceptions.md)); leave `E_MATH_INTERNAL`, an
343
+ internal error of the addon, as it is.
344
+ - The length of a formula does not bound the work it takes: `(x + 1)^100000` is short. The time limit does.
345
+ - The values of the package become JSON through their `toJSON()`: an expression, an equation, a decimal and a value
346
+ with units give their text, solutions an array of texts, a matrix its rows. A vector is a `Float64Array`, which
347
+ `JSON.stringify()` writes as an object (`{"0": 1, "1": 1}`): send `Array.from(vector)`.
348
+ - The bundles, the executables and the installers made by `lakutata package` carry the compiled addon
349
+ (`build/Release/math.node`). Build them on each target platform, after installing the package there; the rules of
350
+ the native addons apply (see [Packaging](Packaging.md) and [NativeAddons](NativeAddons.md)).
351
+ - The package runs in Node.js `^22.13.0 || >=24.11.0`, in Bun and in the main process of Electron, which load the
352
+ addon compiled for Node.js; not in the renderers of Electron nor in the browsers. In Bun, terminating a worker thread
353
+ while operations of the addon still run in it ends the whole process with a fatal error of Node-API (a bug of Bun
354
+ 1.4).
355
+
356
+ ## 13. How it is checked, and what it does not do
357
+
358
+ The tests of the package compare its results on about 2500 cases with those of SymPy and mpmath (the formulas, the
359
+ decimals, the units), of NumPy and SciPy (the matrices, the statistics, the distributions, the numeric solutions), and
360
+ of mathjs and nerdamer; they run on macOS (arm64: Node.js 24, Bun 1.4, Electron 44), on Linux (arm64, glibc and musl:
361
+ Node.js 24, Bun 1.4) and on Windows 11 (arm64: Node.js 24).
362
+
363
+ What it does not do, whether by design or not yet:
364
+
365
+ - An integral whose fraction needs algebraic numbers to factor its denominator (`1/(x^4 + 1)`), or whose antiderivative
366
+ is neither elementary nor one of the special functions of integrals, throws `MathNotSupportedException`; it is not
367
+ kept as an unevaluated integral. `numeric.integrate()` computes the definite integral.
368
+ - `solve()` solves one equation, or a system of linear equations; a system of nonlinear equations is for
369
+ `numeric.solveSystem()`. An equation with infinitely many solutions, such as `sin(x) = 0`, gives no array of them: its
370
+ `set` only describes them (`{2pi*n + pi | n integer} or {2pi*n | n integer}`), and the operations of the sets do not
371
+ take it.
372
+ - An inequality must be made of fractions of polynomials with rational coefficients.
373
+ - The exact eigenvalues of a matrix of expressions are found up to degree four; `linalg.eigen()` gives them as numbers.
374
+ - Nothing that depends on the signs of the variables: `sqrt(x^2)` is not `abs(x)`, and a limit that depends on the
375
+ sign of a parameter (`exp(a*x)` at `inf`) is not found.
376
+ - Exact numbers have at most about 315000 digits: a larger power stays a power (`2^(10^7)`).
377
+
378
+ ## 14. Exceptions
379
+
380
+ All of them extend `MathException`. The errors in a formula carry the `position` of the character where they are found.
381
+ The README of the package lists every case.
382
+
383
+ | Exception | `errno` | When |
384
+ |---|---|---|
385
+ | `MathSyntaxException` | `E_MATH_SYNTAX` | A formula is not written correctly, an equation is given where an expression is expected, or the other way round |
386
+ | `MathUnknownFunctionException` | `E_MATH_UNKNOWN_FUNCTION` | A formula calls a function that is neither built in nor defined |
387
+ | `MathDefinitionException` | `E_MATH_DEFINITION` | A definition or a unit of `createMath()` is not valid |
388
+ | `MathUnitException` | `E_MATH_UNIT` | Units of different dimensions added, compared or converted; a unit that is not known |
389
+ | `MathDomainException` | `E_MATH_DOMAIN` | A value outside the domain of a function: a complex value asked as a number, a singular matrix inverted, a limit that does not exist |
390
+ | `MathDivisionByZeroException` | `E_MATH_DIVISION_BY_ZERO` | A division by zero while computing decimals, a modulus of 0 |
391
+ | `MathNotSupportedException` | `E_MATH_NOT_SUPPORTED` | An operation the package does not do for this formula (section 13) |
392
+ | `MathArgumentException` | `E_MATH_ARGUMENT` | An argument that is not valid: variables left where a number is asked, matrices of the wrong dimensions |
393
+ | `MathTimeoutException`, `MathCancelledException` | `E_MATH_TIMEOUT`, `E_MATH_CANCELLED` | An asynchronous operation past its time limit, or cancelled |
394
+ | `MathException` | `E_MATH_INTERNAL` | An internal error of the addon |
@@ -206,6 +206,11 @@ The application depends on the addon package, and imports it as any package (`im
206
206
  the directory layout ([Packaging](Packaging.md)). The addon is built for the platform of the machine that packs the
207
207
  application, so pack the application of each platform on that platform (a CI matrix, or a container of the
208
208
  architecture for Linux).
209
+ - **An addon package built on the framework** (it depends on `lakutata` or `@lakutata/core`, to provide a component,
210
+ for instance) is bundled with the application instead, so that there is one copy of the framework, and only its addon
211
+ and the files beside it go into the tree. The addon is shipped when it is loaded as the package of `lakutata add`
212
+ loads it (`node-gyp-build` given the directory of the package), by a `require` of its file, or by a path from the
213
+ module (`path.join(__dirname, 'prebuilds', ...)`); `lakutata package` warns of an addon loaded in another way.
209
214
  - **Electron**: the addons are copied as they are installed. The addons that `lakutata add` creates run in Electron
210
215
  without being rebuilt, as do all the addons built on Node-API; an addon built on the V8 API of Node.js must be
211
216
  installed in its build for Electron before packing (`electron-rebuild`).
@@ -47,8 +47,9 @@ tool, a web and a desktop application, calls between services, background jobs,
47
47
  Not part of this package: `@lakutata/cli` (the `lakutata` command for creating projects, running migrations and packing
48
48
  applications, see [Command](Command.md)), `@lakutata/grpc` (a gRPC client, server and service transport),
49
49
  `@lakutata/nats` (a NATS component and service transport), `@lakutata/dbus` (D-Bus), `@lakutata/mcp` (the Model
50
- Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a media server, peer connections) and
51
- `@lakutata/ffi` (the C functions of native libraries).
50
+ Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a media server, peer connections),
51
+ `@lakutata/ffi` (the C functions of native libraries) and `@lakutata/math` (symbolic and numeric mathematics, as
52
+ functions that need no lakutata application).
52
53
 
53
54
  ## The guides
54
55
 
@@ -93,6 +94,7 @@ Context Protocol), `@lakutata/webrtc` (WebRTC: the signaling, a TURN server, a m
93
94
  | [MCP](MCP.md) | The tools, resources and prompts of the Model Context Protocol (`@lakutata/mcp`), over stdio and HTTP |
94
95
  | [WebRTC](WebRTC.md) | Peer-to-peer connections and a media server (`@lakutata/webrtc`): the signaling, the TURN server, browsers and applications, the actions of an application called through data channels, WHIP and WHEP |
95
96
  | [FFI](FFI.md) | The C functions of native libraries (`@lakutata/ffi`): declarations copied from headers, structs, callbacks, memory, packaging |
97
+ | [Math](Math.md) | Mathematics (`@lakutata/math`): formulas and exact results, equations, decimals of any precision, matrices, statistics, numeric solutions, units |
96
98
  | [Command](Command.md) | The `lakutata` command: creating projects, migrations, translations, environment variables |
97
99
  | [Workspaces](Workspaces.md) | Projects of several packages: running their scripts, giving them a version, adding packages |
98
100
  | [NativeAddons](NativeAddons.md) | Native addons in C++ or Rust: creating them, linking libraries, shipping them |
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` | 原生插件的内部错误 |
@@ -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 服务器、媒体服务器、对等连接)和 `@lakutata/ffi`(调用动态库中的 C 函数)。
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
 
@@ -80,6 +80,7 @@
80
80
  | [MCP](MCP.md) | 模型上下文协议(`@lakutata/mcp`)的工具、资源和提示词,基于 stdio 和 HTTP |
81
81
  | [WebRTC](WebRTC.md) | 点对点连接和媒体服务器(`@lakutata/webrtc`):信令、TURN 服务器、浏览器和应用、通过数据通道调用应用的动作、WHIP 和 WHEP |
82
82
  | [FFI](FFI.md) | 调用动态库中的 C 函数(`@lakutata/ffi`):从头文件复制的声明、结构体、回调、内存、打包 |
83
+ | [Math](Math.md) | 数学计算(`@lakutata/math`):公式和精确结果、方程、任意精度的十进制数、矩阵、统计、数值解法、单位 |
83
84
  | [Command](Command.md) | `lakutata` 命令:创建项目、迁移、翻译、环境变量 |
84
85
  | [Workspaces](Workspaces.md) | 由多个包组成的项目:运行各个包的脚本、统一版本号、添加包 |
85
86
  | [NativeAddons](NativeAddons.md) | 用 C++ 或 Rust 编写的原生插件:创建、链接库、发布 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakutata",
3
- "version": "3.1.2",
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",