@johnhenry/math 0.0.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 (242) hide show
  1. package/README.md +165 -0
  2. package/dist/Bivector4.d.ts +52 -0
  3. package/dist/Bivector4.d.ts.map +1 -0
  4. package/dist/Bivector4.js +105 -0
  5. package/dist/Bivector4.js.map +1 -0
  6. package/dist/CellGraph.d.ts +259 -0
  7. package/dist/CellGraph.d.ts.map +1 -0
  8. package/dist/CellGraph.js +637 -0
  9. package/dist/CellGraph.js.map +1 -0
  10. package/dist/Clifford4Internal.d.ts +35 -0
  11. package/dist/Clifford4Internal.d.ts.map +1 -0
  12. package/dist/Clifford4Internal.js +86 -0
  13. package/dist/Clifford4Internal.js.map +1 -0
  14. package/dist/Combinatorics.d.ts +61 -0
  15. package/dist/Combinatorics.d.ts.map +1 -0
  16. package/dist/Combinatorics.js +186 -0
  17. package/dist/Combinatorics.js.map +1 -0
  18. package/dist/CombinatoricsGenerators.d.ts +44 -0
  19. package/dist/CombinatoricsGenerators.d.ts.map +1 -0
  20. package/dist/CombinatoricsGenerators.js +148 -0
  21. package/dist/CombinatoricsGenerators.js.map +1 -0
  22. package/dist/ComplexMath.d.ts +46 -0
  23. package/dist/ComplexMath.d.ts.map +1 -0
  24. package/dist/ComplexMath.js +98 -0
  25. package/dist/ComplexMath.js.map +1 -0
  26. package/dist/ComplexNumber.d.ts +174 -0
  27. package/dist/ComplexNumber.d.ts.map +1 -0
  28. package/dist/ComplexNumber.js +443 -0
  29. package/dist/ComplexNumber.js.map +1 -0
  30. package/dist/Cycle.d.ts +42 -0
  31. package/dist/Cycle.d.ts.map +1 -0
  32. package/dist/Cycle.js +120 -0
  33. package/dist/Cycle.js.map +1 -0
  34. package/dist/Decimal.d.ts +45 -0
  35. package/dist/Decimal.d.ts.map +1 -0
  36. package/dist/Decimal.js +135 -0
  37. package/dist/Decimal.js.map +1 -0
  38. package/dist/Distributions.d.ts +55 -0
  39. package/dist/Distributions.d.ts.map +1 -0
  40. package/dist/Distributions.js +222 -0
  41. package/dist/Distributions.js.map +1 -0
  42. package/dist/DualNumber.d.ts +42 -0
  43. package/dist/DualNumber.d.ts.map +1 -0
  44. package/dist/DualNumber.js +91 -0
  45. package/dist/DualNumber.js.map +1 -0
  46. package/dist/Environment.d.ts +44 -0
  47. package/dist/Environment.d.ts.map +1 -0
  48. package/dist/Environment.js +129 -0
  49. package/dist/Environment.js.map +1 -0
  50. package/dist/Expression.d.ts +19 -0
  51. package/dist/Expression.d.ts.map +1 -0
  52. package/dist/Expression.js +43 -0
  53. package/dist/Expression.js.map +1 -0
  54. package/dist/FFT.d.ts +21 -0
  55. package/dist/FFT.d.ts.map +1 -0
  56. package/dist/FFT.js +136 -0
  57. package/dist/FFT.js.map +1 -0
  58. package/dist/GeometricAlgebra4.d.ts +73 -0
  59. package/dist/GeometricAlgebra4.d.ts.map +1 -0
  60. package/dist/GeometricAlgebra4.js +115 -0
  61. package/dist/GeometricAlgebra4.js.map +1 -0
  62. package/dist/Geometry.d.ts +41 -0
  63. package/dist/Geometry.d.ts.map +1 -0
  64. package/dist/Geometry.js +167 -0
  65. package/dist/Geometry.js.map +1 -0
  66. package/dist/Graph.d.ts +72 -0
  67. package/dist/Graph.d.ts.map +1 -0
  68. package/dist/Graph.js +337 -0
  69. package/dist/Graph.js.map +1 -0
  70. package/dist/Graph3DUtils.d.ts +80 -0
  71. package/dist/Graph3DUtils.d.ts.map +1 -0
  72. package/dist/Graph3DUtils.js +156 -0
  73. package/dist/Graph3DUtils.js.map +1 -0
  74. package/dist/GraphUtils.d.ts +67 -0
  75. package/dist/GraphUtils.d.ts.map +1 -0
  76. package/dist/GraphUtils.js +72 -0
  77. package/dist/GraphUtils.js.map +1 -0
  78. package/dist/GroupTheory.d.ts +73 -0
  79. package/dist/GroupTheory.d.ts.map +1 -0
  80. package/dist/GroupTheory.js +194 -0
  81. package/dist/GroupTheory.js.map +1 -0
  82. package/dist/IntUtils.d.ts +33 -0
  83. package/dist/IntUtils.d.ts.map +1 -0
  84. package/dist/IntUtils.js +185 -0
  85. package/dist/IntUtils.js.map +1 -0
  86. package/dist/Interval.d.ts +44 -0
  87. package/dist/Interval.d.ts.map +1 -0
  88. package/dist/Interval.js +165 -0
  89. package/dist/Interval.js.map +1 -0
  90. package/dist/Lattice.d.ts +48 -0
  91. package/dist/Lattice.d.ts.map +1 -0
  92. package/dist/Lattice.js +65 -0
  93. package/dist/Lattice.js.map +1 -0
  94. package/dist/Logic.d.ts +22 -0
  95. package/dist/Logic.d.ts.map +1 -0
  96. package/dist/Logic.js +34 -0
  97. package/dist/Logic.js.map +1 -0
  98. package/dist/MatrixMath.d.ts +71 -0
  99. package/dist/MatrixMath.d.ts.map +1 -0
  100. package/dist/MatrixMath.js +302 -0
  101. package/dist/MatrixMath.js.map +1 -0
  102. package/dist/NumberTheory.d.ts +49 -0
  103. package/dist/NumberTheory.d.ts.map +1 -0
  104. package/dist/NumberTheory.js +230 -0
  105. package/dist/NumberTheory.js.map +1 -0
  106. package/dist/Numerical.d.ts +85 -0
  107. package/dist/Numerical.d.ts.map +1 -0
  108. package/dist/Numerical.js +343 -0
  109. package/dist/Numerical.js.map +1 -0
  110. package/dist/Permutation.d.ts +43 -0
  111. package/dist/Permutation.d.ts.map +1 -0
  112. package/dist/Permutation.js +142 -0
  113. package/dist/Permutation.js.map +1 -0
  114. package/dist/Polygon.d.ts +64 -0
  115. package/dist/Polygon.d.ts.map +1 -0
  116. package/dist/Polygon.js +221 -0
  117. package/dist/Polygon.js.map +1 -0
  118. package/dist/PolynomialRing.d.ts +74 -0
  119. package/dist/PolynomialRing.d.ts.map +1 -0
  120. package/dist/PolynomialRing.js +231 -0
  121. package/dist/PolynomialRing.js.map +1 -0
  122. package/dist/Quaternion.d.ts +37 -0
  123. package/dist/Quaternion.d.ts.map +1 -0
  124. package/dist/Quaternion.js +106 -0
  125. package/dist/Quaternion.js.map +1 -0
  126. package/dist/Rational.d.ts +34 -0
  127. package/dist/Rational.d.ts.map +1 -0
  128. package/dist/Rational.js +136 -0
  129. package/dist/Rational.js.map +1 -0
  130. package/dist/RealMath.d.ts +112 -0
  131. package/dist/RealMath.d.ts.map +1 -0
  132. package/dist/RealMath.js +262 -0
  133. package/dist/RealMath.js.map +1 -0
  134. package/dist/Rotor4.d.ts +180 -0
  135. package/dist/Rotor4.d.ts.map +1 -0
  136. package/dist/Rotor4.js +388 -0
  137. package/dist/Rotor4.js.map +1 -0
  138. package/dist/SpecialFunctions.d.ts +32 -0
  139. package/dist/SpecialFunctions.d.ts.map +1 -0
  140. package/dist/SpecialFunctions.js +161 -0
  141. package/dist/SpecialFunctions.js.map +1 -0
  142. package/dist/SpecialOperator.d.ts +21 -0
  143. package/dist/SpecialOperator.d.ts.map +1 -0
  144. package/dist/SpecialOperator.js +26 -0
  145. package/dist/SpecialOperator.js.map +1 -0
  146. package/dist/Statistics.d.ts +50 -0
  147. package/dist/Statistics.d.ts.map +1 -0
  148. package/dist/Statistics.js +131 -0
  149. package/dist/Statistics.js.map +1 -0
  150. package/dist/StringEvaluator.d.ts +48 -0
  151. package/dist/StringEvaluator.d.ts.map +1 -0
  152. package/dist/StringEvaluator.js +190 -0
  153. package/dist/StringEvaluator.js.map +1 -0
  154. package/dist/Structure.d.ts +130 -0
  155. package/dist/Structure.d.ts.map +1 -0
  156. package/dist/Structure.js +394 -0
  157. package/dist/Structure.js.map +1 -0
  158. package/dist/Symbolic.d.ts +501 -0
  159. package/dist/Symbolic.d.ts.map +1 -0
  160. package/dist/Symbolic.js +4077 -0
  161. package/dist/Symbolic.js.map +1 -0
  162. package/dist/Type.d.ts +31 -0
  163. package/dist/Type.d.ts.map +1 -0
  164. package/dist/Type.js +47 -0
  165. package/dist/Type.js.map +1 -0
  166. package/dist/Utilities.d.ts +27 -0
  167. package/dist/Utilities.d.ts.map +1 -0
  168. package/dist/Utilities.js +47 -0
  169. package/dist/Utilities.js.map +1 -0
  170. package/dist/Vec4.d.ts +39 -0
  171. package/dist/Vec4.d.ts.map +1 -0
  172. package/dist/Vec4.js +82 -0
  173. package/dist/Vec4.js.map +1 -0
  174. package/dist/Vector.d.ts +83 -0
  175. package/dist/Vector.d.ts.map +1 -0
  176. package/dist/Vector.js +173 -0
  177. package/dist/Vector.js.map +1 -0
  178. package/dist/VectorCalculus.d.ts +42 -0
  179. package/dist/VectorCalculus.d.ts.map +1 -0
  180. package/dist/VectorCalculus.js +82 -0
  181. package/dist/VectorCalculus.js.map +1 -0
  182. package/dist/VectorUtils.d.ts +172 -0
  183. package/dist/VectorUtils.d.ts.map +1 -0
  184. package/dist/VectorUtils.js +706 -0
  185. package/dist/VectorUtils.js.map +1 -0
  186. package/dist/differentiation-rules.d.ts +21 -0
  187. package/dist/differentiation-rules.d.ts.map +1 -0
  188. package/dist/differentiation-rules.js +52 -0
  189. package/dist/differentiation-rules.js.map +1 -0
  190. package/dist/index.d.ts +52 -0
  191. package/dist/index.d.ts.map +1 -0
  192. package/dist/index.js +66 -0
  193. package/dist/index.js.map +1 -0
  194. package/package.json +68 -0
  195. package/src/Bivector4.ts +136 -0
  196. package/src/CellGraph.ts +669 -0
  197. package/src/Clifford4Internal.ts +90 -0
  198. package/src/Combinatorics.ts +183 -0
  199. package/src/CombinatoricsGenerators.ts +144 -0
  200. package/src/ComplexMath.ts +162 -0
  201. package/src/ComplexNumber.ts +491 -0
  202. package/src/Cycle.ts +126 -0
  203. package/src/Decimal.ts +145 -0
  204. package/src/Distributions.ts +269 -0
  205. package/src/DualNumber.ts +105 -0
  206. package/src/Environment.ts +127 -0
  207. package/src/Expression.ts +48 -0
  208. package/src/FFT.ts +146 -0
  209. package/src/GeometricAlgebra4.ts +157 -0
  210. package/src/Geometry.ts +173 -0
  211. package/src/Graph.ts +337 -0
  212. package/src/Graph3DUtils.ts +269 -0
  213. package/src/GraphUtils.ts +159 -0
  214. package/src/GroupTheory.ts +214 -0
  215. package/src/IntUtils.ts +200 -0
  216. package/src/Interval.ts +171 -0
  217. package/src/Lattice.ts +87 -0
  218. package/src/Logic.ts +38 -0
  219. package/src/MatrixMath.ts +326 -0
  220. package/src/NumberTheory.ts +228 -0
  221. package/src/Numerical.ts +390 -0
  222. package/src/Permutation.ts +148 -0
  223. package/src/Polygon.ts +229 -0
  224. package/src/PolynomialRing.ts +242 -0
  225. package/src/Quaternion.ts +125 -0
  226. package/src/Rational.ts +148 -0
  227. package/src/RealMath.ts +346 -0
  228. package/src/Rotor4.ts +440 -0
  229. package/src/SpecialFunctions.ts +152 -0
  230. package/src/SpecialOperator.ts +28 -0
  231. package/src/Statistics.ts +170 -0
  232. package/src/StringEvaluator.ts +208 -0
  233. package/src/Structure.ts +481 -0
  234. package/src/Symbolic.ts +4313 -0
  235. package/src/Type.ts +48 -0
  236. package/src/Utilities.ts +49 -0
  237. package/src/Vec4.ts +98 -0
  238. package/src/Vector.ts +189 -0
  239. package/src/VectorCalculus.ts +87 -0
  240. package/src/VectorUtils.ts +797 -0
  241. package/src/differentiation-rules.ts +73 -0
  242. package/src/index.ts +140 -0
package/src/Rotor4.ts ADDED
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Rotor4 — a rotation operator in 4D space: `scalar + bivector (6) + pseudoscalar (1)`,
3
+ * 8 components total. This is the 4D generalization of {@link Quaternion}
4
+ * (which is really the 3D rotor, `scalar + bivector (3)`, that happens to
5
+ * have the same component count as a 3D vector — a coincidence unique to 3D
6
+ * that doesn't hold in 4D or any other dimension).
7
+ *
8
+ * **Double rotations.** A general 3D rotor always describes rotation in a
9
+ * single 2-plane. A general 4D rotor can describe a *double rotation*:
10
+ * simultaneous, independent rotation in two mutually orthogonal 2-planes at
11
+ * once — this is the origin of the nonzero pseudoscalar component, and it
12
+ * has no 3D analogue. See {@link Rotor4.multiply} composing two
13
+ * single-plane rotors in orthogonal planes for how a double rotation arises.
14
+ *
15
+ * **Normalization caveat.** {@link Rotor4.normalize} performs the naive
16
+ * quaternion-style renormalization (divide by magnitude). This is only
17
+ * exactly correct for *simple* rotors (single rotation plane) — for a
18
+ * genuine double rotation it does not lie on the same manifold as a simple
19
+ * rotor, so naive renormalization after numerical drift can silently produce
20
+ * an invalid rotor. {@link Rotor4.factor} and {@link Rotor4.renormalize} are
21
+ * the proper fix (Perwass-style rotor factorization): decompose into two
22
+ * simple rotors in orthogonal planes, each renormalizable exactly, then
23
+ * recompose — see the `N-Dimensional Rigid Body Dynamics` (SIGGRAPH 2020)
24
+ * paper §3.3 for why this matters for repeated physics integration.
25
+ */
26
+ import { Bivector4 } from "./Bivector4.ts";
27
+ import { E1, E2, E3, E4, geometricProductMV, type Multivector4, mvZero, PSEUDOSCALAR4 } from "./Clifford4Internal.ts";
28
+ import { bivectorFromMV, bivectorToMV, leftContraction, wedgeProduct } from "./GeometricAlgebra4.ts";
29
+ import { MatrixMath } from "./MatrixMath.ts";
30
+ import { Vec4 } from "./Vec4.ts";
31
+ import { Vector } from "./Vector.ts";
32
+
33
+ export class Rotor4 {
34
+ readonly scalar: number;
35
+ readonly bivector: Bivector4;
36
+ readonly pseudoscalar: number;
37
+
38
+ static readonly Identity = new Rotor4(1, Bivector4.Zero, 0);
39
+
40
+ constructor(scalar = 1, bivector: Bivector4 = Bivector4.Zero, pseudoscalar = 0) {
41
+ this.scalar = scalar;
42
+ this.bivector = bivector;
43
+ this.pseudoscalar = pseudoscalar;
44
+ }
45
+
46
+ private toMV(): Multivector4 {
47
+ const mv = mvZero();
48
+ mv[0] = this.scalar;
49
+ const bmv = bivectorToMV(this.bivector);
50
+ for (let i = 0; i < bmv.length; i++) mv[i] += bmv[i] as number;
51
+ mv[PSEUDOSCALAR4] = this.pseudoscalar;
52
+ return mv;
53
+ }
54
+
55
+ private static fromMV(mv: Multivector4): Rotor4 {
56
+ return new Rotor4(mv[0] as number, bivectorFromMV(mv), mv[PSEUDOSCALAR4] as number);
57
+ }
58
+
59
+ add(r: Rotor4): Rotor4 {
60
+ return new Rotor4(this.scalar + r.scalar, this.bivector.add(r.bivector), this.pseudoscalar + r.pseudoscalar);
61
+ }
62
+
63
+ subtract(r: Rotor4): Rotor4 {
64
+ return new Rotor4(this.scalar - r.scalar, this.bivector.subtract(r.bivector), this.pseudoscalar - r.pseudoscalar);
65
+ }
66
+
67
+ negate(): Rotor4 {
68
+ return new Rotor4(-this.scalar, this.bivector.negate(), -this.pseudoscalar);
69
+ }
70
+
71
+ scale(s: number): Rotor4 {
72
+ return new Rotor4(this.scalar * s, this.bivector.scale(s), this.pseudoscalar * s);
73
+ }
74
+
75
+ /** Component-wise inner product over all 8 components (scalar, 6 bivector, pseudoscalar) -- treats a Rotor4 as an 8-vector, exactly {@link Quaternion.dot}'s role in that class's own `slerp`. */
76
+ dot(r: Rotor4): number {
77
+ return this.scalar * r.scalar + this.bivector.dot(r.bivector) + this.pseudoscalar * r.pseudoscalar;
78
+ }
79
+
80
+ /** The geometric product `this * r` — composes two rotations (apply `r` first, then `this`). */
81
+ multiply(r: Rotor4): Rotor4 {
82
+ return Rotor4.fromMV(geometricProductMV(this.toMV(), r.toMV()));
83
+ }
84
+
85
+ /**
86
+ * The reverse `R~`: reverses the order of vector factors, which negates the
87
+ * bivector (grade 2, picks up (-1)^1) but leaves the scalar (grade 0) and
88
+ * pseudoscalar (grade 4, picks up (-1)^6 = +1) unchanged. For a unit rotor,
89
+ * this is also the inverse — the 4D analogue of a quaternion's conjugate
90
+ * ({@link Quaternion.conjugate}). **Not** the inverse for a non-unit rotor
91
+ * (see {@link inverse}): a rotor that has drifted off the unit manifold
92
+ * during repeated composition (e.g. per-timestep physics integration,
93
+ * before renormalization runs) will silently produce a wrong,
94
+ * `|R|²`-scaled result from `reverse()` where an inverse was intended.
95
+ */
96
+ reverse(): Rotor4 {
97
+ return new Rotor4(this.scalar, this.bivector.negate(), this.pseudoscalar);
98
+ }
99
+
100
+ /**
101
+ * Multiplicative inverse `R⁻¹ = R~ / |R|²`, distinct from {@link reverse}
102
+ * for a non-unit rotor — the 4D analogue of {@link Quaternion.inverse}.
103
+ * `this.multiply(this.inverse())` is `Identity` regardless of `this`'s
104
+ * magnitude; `this.multiply(this.reverse())` only is when `this` is
105
+ * already a unit rotor.
106
+ */
107
+ inverse(): Rotor4 {
108
+ const m2 = this.magnitudeSquared;
109
+ if (m2 === 0) throw new Error("The zero rotor has no inverse.");
110
+ return this.reverse().scale(1 / m2);
111
+ }
112
+
113
+ get magnitudeSquared(): number {
114
+ return this.scalar * this.scalar + this.bivector.magnitudeSquared + this.pseudoscalar * this.pseudoscalar;
115
+ }
116
+
117
+ get magnitude(): number {
118
+ return Math.sqrt(this.magnitudeSquared);
119
+ }
120
+
121
+ /**
122
+ * Naive renormalization (divide by magnitude). Exact for simple rotors;
123
+ * see the class-level doc comment for why this is *not* generally correct
124
+ * for double rotations, and where the real fix belongs.
125
+ */
126
+ normalize(): Rotor4 {
127
+ const m = this.magnitude;
128
+ if (m === 0) throw new Error("Cannot normalize the zero rotor.");
129
+ return this.scale(1 / m);
130
+ }
131
+
132
+ /** Apply this rotor to a vector via the sandwich product `R v R~`. Rotates `v`. */
133
+ apply(v: Vec4): Vec4 {
134
+ const vmv = mvZero();
135
+ vmv[E1] = v.x;
136
+ vmv[E2] = v.y;
137
+ vmv[E3] = v.z;
138
+ vmv[E4] = v.w;
139
+ const mv = geometricProductMV(geometricProductMV(this.toMV(), vmv), this.reverse().toMV());
140
+ return new Vec4(mv[E1] as number, mv[E2] as number, mv[E3] as number, mv[E4] as number);
141
+ }
142
+
143
+ /** Apply this rotor to a bivector via the sandwich product `R B R~`. Rotates the 2-plane `B`. */
144
+ applyToBivector(b: Bivector4): Bivector4 {
145
+ const mv = geometricProductMV(geometricProductMV(this.toMV(), bivectorToMV(b)), this.reverse().toMV());
146
+ return bivectorFromMV(mv);
147
+ }
148
+
149
+ /**
150
+ * Perwass-style rotor factorization: decompose this rotor into two simple
151
+ * rotors `[R1, R2]` in mutually orthogonal planes such that `R2.multiply(R1)`
152
+ * has the same *action* as this rotor (equal up to the usual rotor
153
+ * double-cover sign, like any rotor comparison) — the proper fix for
154
+ * {@link normalize}'s documented limitation on double rotations. Works
155
+ * even when this rotor has drifted away from being a valid unit rotor
156
+ * (the intended use case: repeated physics integration).
157
+ *
158
+ * Method: find this rotor's two invariant 2-planes as eigenspaces of the
159
+ * *symmetric* part `S = M + Mᵀ` of its 4x4 matrix representation `M`
160
+ * (restricted to an invariant plane with rotation angle `θ`, `M + Mᵀ` is
161
+ * `2cos(θ)` times the identity on that plane — so same-angle-magnitude
162
+ * planes share an eigenvalue, and a repeated eigenvalue of `S` signals an
163
+ * isoclinic-type degeneracy where *any* orthonormal basis of that
164
+ * eigenspace is a genuinely valid invariant-plane choice, not just an
165
+ * approximation). `eigenSymmetric`'s eigenvector order isn't guaranteed to
166
+ * group same-plane pairs together when eigenvalues coincide (or are
167
+ * numerically indistinguishable), so the 3 possible pairings of the 4
168
+ * eigenvectors into 2 pairs are explicitly checked for genuine
169
+ * `M`-invariance (zero "leakage" into the other pair) before use, rather
170
+ * than assumed from sort order. Once a valid pair `(u, v)` spanning an
171
+ * invariant plane is known, the angle and correctly-oriented plane come
172
+ * directly from how `M` actually acts on `u` within that plane —
173
+ * `atan2(v·(Mu), u·(Mu))` — not from this rotor's own (possibly drifted)
174
+ * scalar/bivector/pseudoscalar components.
175
+ */
176
+ factor(): [Rotor4, Rotor4] {
177
+ const basis = [Vec4.Ex, Vec4.Ey, Vec4.Ez, Vec4.Ew];
178
+ const columns = basis.map((e) => this.apply(e).toArray());
179
+ const M = (i: number, j: number): number => columns[j]?.[i] as number;
180
+ const S = Vector.fromArray([0, 1, 2, 3].map((i) => Vector.fromArray([0, 1, 2, 3].map((j) => M(i, j) + M(j, i)))));
181
+ const { vectors } = MatrixMath.eigenSymmetric(S);
182
+ const eig = [0, 1, 2, 3].map(
183
+ (i) =>
184
+ new Vec4(
185
+ vectors[0]?.[i] as number,
186
+ vectors[1]?.[i] as number,
187
+ vectors[2]?.[i] as number,
188
+ vectors[3]?.[i] as number,
189
+ ),
190
+ );
191
+
192
+ const pairings: [[number, number], [number, number]][] = [
193
+ [
194
+ [0, 1],
195
+ [2, 3],
196
+ ],
197
+ [
198
+ [0, 2],
199
+ [1, 3],
200
+ ],
201
+ [
202
+ [0, 3],
203
+ [1, 2],
204
+ ],
205
+ ];
206
+ let bestLeak = Number.POSITIVE_INFINITY;
207
+ let best: [[number, number], [number, number]] = pairings[0] as [[number, number], [number, number]];
208
+ for (const [[i, j], [k, l]] of pairings) {
209
+ const ei = eig[i] as Vec4;
210
+ const ej = eig[j] as Vec4;
211
+ const ek = eig[k] as Vec4;
212
+ const el = eig[l] as Vec4;
213
+ const mi = this.apply(ei);
214
+ const mj = this.apply(ej);
215
+ const leak = Math.abs(mi.dot(ek)) + Math.abs(mi.dot(el)) + Math.abs(mj.dot(ek)) + Math.abs(mj.dot(el));
216
+ if (leak < bestLeak) {
217
+ bestLeak = leak;
218
+ best = [
219
+ [i, j],
220
+ [k, l],
221
+ ];
222
+ }
223
+ }
224
+
225
+ const planeAndAngle = (u: Vec4, v: Vec4): Rotor4 => {
226
+ const mu = this.apply(u);
227
+ const angle = Math.atan2(v.dot(mu), u.dot(mu));
228
+ const plane = wedgeProduct(v, u).normalize();
229
+ return Rotor4.fromBivectorAngle(plane, angle);
230
+ };
231
+ const [[a, b], [c, d]] = best;
232
+ return [planeAndAngle(eig[a] as Vec4, eig[b] as Vec4), planeAndAngle(eig[c] as Vec4, eig[d] as Vec4)];
233
+ }
234
+
235
+ /**
236
+ * The proper (Perwass-style) fix for numerical drift: {@link factor} this
237
+ * rotor into two simple rotors — each exactly renormalizable, unlike a
238
+ * compound rotor — and recompose. Prefer this over {@link normalize} once
239
+ * a rotor may have accumulated drift through repeated integration and
240
+ * could be a genuine double rotation; `normalize` remains correct (and
241
+ * cheaper) for rotors known to be simple.
242
+ */
243
+ renormalize(): Rotor4 {
244
+ // factor() reads this rotor's action via apply() (the sandwich product
245
+ // R v R~), which scales vectors by |R|^2 for a non-unit R -- corrupting
246
+ // the "M is a pure rotation matrix" assumption factor()'s eigenstructure
247
+ // analysis depends on. A cheap magnitude normalize first removes that
248
+ // radial drift; the exact plane/angle recovery in factor() then handles
249
+ // the part naive normalize alone gets wrong (the double-rotation
250
+ // manifold), on an already magnitude-correct input.
251
+ const [r1, r2] = this.normalize().factor();
252
+ return r2.multiply(r1);
253
+ }
254
+
255
+ /**
256
+ * Build the rotor for a rotation of `angle` radians in the 2-plane `plane`
257
+ * (analogous to {@link Quaternion.fromAxisAngle}, but a *plane* rather than
258
+ * an *axis* — axes only work as rotation generators in 3D). `plane` must be
259
+ * simple (see {@link Bivector4.isSimple}); to build a double rotation,
260
+ * construct two simple rotors in orthogonal planes and {@link multiply}
261
+ * them together.
262
+ */
263
+ static fromBivectorAngle(plane: Bivector4, angle: number): Rotor4 {
264
+ if (!plane.isSimple()) {
265
+ throw new Error(
266
+ "Rotor4.fromBivectorAngle requires a simple (single-plane) bivector. " +
267
+ "To build a double rotation, multiply() two simple rotors in orthogonal planes instead.",
268
+ );
269
+ }
270
+ const unitPlane = plane.normalize();
271
+ const half = angle / 2;
272
+ return new Rotor4(Math.cos(half), unitPlane.scale(Math.sin(half)), 0);
273
+ }
274
+
275
+ /**
276
+ * `exp(B)`: the rotor generated by the bivector `B` -- for a *simple* `B`,
277
+ * this is a rotation of `|B|` radians in `B`'s plane, matching
278
+ * {@link fromBivectorAngle}'s convention for the common physics case of an
279
+ * already-scaled generator (e.g. `angularVelocity * dt`, where the
280
+ * bivector's magnitude *is* the rotation angle and its direction *is* the
281
+ * plane). For a *compound* `B` (e.g. an angular velocity spinning
282
+ * simultaneously in two independent planes, entirely possible physically),
283
+ * the naive `cos|B| + sin|B|·B̂` formula this reduces to for simple `B` is
284
+ * wrong: `B² ≠ -|B|²` in general (it has a nonzero pseudoscalar term too),
285
+ * so this instead decomposes `B` into its two orthogonal simple
286
+ * components `m1·Π1 + m2·Π2` and returns `exp(m2·Π2)·exp(m1·Π1)` (valid
287
+ * since orthogonal-plane generators commute). Decomposition method:
288
+ * `B`'s invariant planes are the eigenspaces of `-A²`, where `A` is `B`'s
289
+ * contraction-action matrix (`A(v) = v⌋B`, antisymmetric, so `A²` is
290
+ * symmetric negative-semidefinite) -- the bivector analogue of how
291
+ * {@link factor} finds a *rotor's* invariant planes via `M + Mᵀ`, right
292
+ * down to needing the same explicit pairing-invariance check (eigenvalue
293
+ * sort order isn't reliable when eigenvalues coincide). Returns
294
+ * {@link Identity} for the zero bivector.
295
+ */
296
+ static exp(bivector: Bivector4): Rotor4 {
297
+ if (bivector.magnitude === 0) return Rotor4.Identity;
298
+
299
+ const basis = [Vec4.Ex, Vec4.Ey, Vec4.Ez, Vec4.Ew];
300
+ const columns = basis.map((e) => leftContraction(e, bivector).toArray());
301
+ const A = (i: number, j: number): number => columns[j]?.[i] as number;
302
+ const A2 = Vector.fromArray(
303
+ [0, 1, 2, 3].map((i) =>
304
+ Vector.fromArray(
305
+ [0, 1, 2, 3].map((j) => {
306
+ let sum = 0;
307
+ for (let k = 0; k < 4; k++) sum += A(i, k) * A(k, j);
308
+ return sum;
309
+ }),
310
+ ),
311
+ ),
312
+ );
313
+ const { vectors } = MatrixMath.eigenSymmetric(A2);
314
+ const eig = [0, 1, 2, 3].map(
315
+ (i) =>
316
+ new Vec4(
317
+ vectors[0]?.[i] as number,
318
+ vectors[1]?.[i] as number,
319
+ vectors[2]?.[i] as number,
320
+ vectors[3]?.[i] as number,
321
+ ),
322
+ );
323
+
324
+ const pairings: [[number, number], [number, number]][] = [
325
+ [
326
+ [0, 1],
327
+ [2, 3],
328
+ ],
329
+ [
330
+ [0, 2],
331
+ [1, 3],
332
+ ],
333
+ [
334
+ [0, 3],
335
+ [1, 2],
336
+ ],
337
+ ];
338
+ let bestLeak = Number.POSITIVE_INFINITY;
339
+ let best: [[number, number], [number, number]] = pairings[0] as [[number, number], [number, number]];
340
+ for (const [[i, j], [k, l]] of pairings) {
341
+ const ei = eig[i] as Vec4;
342
+ const ej = eig[j] as Vec4;
343
+ const ek = eig[k] as Vec4;
344
+ const el = eig[l] as Vec4;
345
+ const ai = leftContraction(ei, bivector);
346
+ const aj = leftContraction(ej, bivector);
347
+ const leak = Math.abs(ai.dot(ek)) + Math.abs(ai.dot(el)) + Math.abs(aj.dot(ek)) + Math.abs(aj.dot(el));
348
+ if (leak < bestLeak) {
349
+ bestLeak = leak;
350
+ best = [
351
+ [i, j],
352
+ [k, l],
353
+ ];
354
+ }
355
+ }
356
+
357
+ const planeAndMagnitude = (u: Vec4, v: Vec4): { plane: Bivector4; m: number } => {
358
+ const au = leftContraction(u, bivector);
359
+ return { m: -v.dot(au), plane: wedgeProduct(v, u).normalize() };
360
+ };
361
+ const [[a, b], [c, d]] = best;
362
+ const p1 = planeAndMagnitude(eig[a] as Vec4, eig[b] as Vec4);
363
+ const p2 = planeAndMagnitude(eig[c] as Vec4, eig[d] as Vec4);
364
+ return Rotor4.fromBivectorAngle(p2.plane, p2.m).multiply(Rotor4.fromBivectorAngle(p1.plane, p1.m));
365
+ }
366
+
367
+ /**
368
+ * `log()`: the bivector `B` such that `exp(B) = this`, for a *simple*
369
+ * rotor -- the inverse of {@link exp}. Throws for a compound
370
+ * (double-rotation) rotor, same as {@link toBivectorAngle} (which this is
371
+ * built on): decomposing a general rotor's log is the Perwass-factorization
372
+ * work deferred to the physics layer.
373
+ */
374
+ log(epsilon = 1e-9): Bivector4 {
375
+ const { plane, angle } = this.toBivectorAngle(epsilon);
376
+ return plane.scale(angle);
377
+ }
378
+
379
+ /**
380
+ * Recover `(plane, angle)` for a *simple* rotor (pseudoscalar ≈ 0). Throws
381
+ * for a compound (double-rotation) rotor — see the class-level doc comment;
382
+ * decomposing a general rotor's log is the Perwass-factorization work
383
+ * deferred to the physics layer.
384
+ */
385
+ toBivectorAngle(epsilon = 1e-9): { plane: Bivector4; angle: number } {
386
+ if (Math.abs(this.pseudoscalar) > epsilon) {
387
+ throw new Error(
388
+ "Rotor4.toBivectorAngle only supports simple rotors (pseudoscalar ≈ 0); " +
389
+ "this rotor is a compound double rotation.",
390
+ );
391
+ }
392
+ const bMag = this.bivector.magnitude;
393
+ const angle = 2 * Math.atan2(bMag, this.scalar);
394
+ if (bMag === 0) return { plane: Bivector4.Zero, angle: 0 };
395
+ return { plane: this.bivector.normalize(), angle };
396
+ }
397
+
398
+ /**
399
+ * Spherical linear interpolation between two rotors, `t` in `[0, 1]` --
400
+ * the 4D analogue of {@link Quaternion.slerp}, same component-wise
401
+ * treatment (an 8-vector of scalar/bivector(6)/pseudoscalar, via
402
+ * {@link dot}) and the same near-parallel fallback to a normalized
403
+ * linear interpolation to avoid dividing by a near-zero `sin(theta0)`.
404
+ *
405
+ * **Same manifold caveat as {@link normalize}**: this is only exactly
406
+ * correct for interpolating between *simple* rotors (single rotation
407
+ * plane) — a compound double rotation doesn't lie on the sphere this
408
+ * construction assumes, so slerping one is an approximation, not a
409
+ * geodesic. For a double rotation, {@link factor} each endpoint into its
410
+ * two orthogonal simple rotors and slerp each pair independently instead.
411
+ */
412
+ static slerp(a: Rotor4, b: Rotor4, t: number): Rotor4 {
413
+ const ra = a.normalize();
414
+ let rb = b.normalize();
415
+ let cos = ra.dot(rb);
416
+ if (cos < 0) {
417
+ rb = rb.scale(-1);
418
+ cos = -cos;
419
+ }
420
+ if (cos > 0.9995) return ra.add(rb.subtract(ra).scale(t)).normalize();
421
+ const theta0 = Math.acos(cos);
422
+ const theta = theta0 * t;
423
+ const sinTheta0 = Math.sin(theta0);
424
+ const s0 = Math.sin(theta0 - theta) / sinTheta0;
425
+ const s1 = Math.sin(theta) / sinTheta0;
426
+ return ra.scale(s0).add(rb.scale(s1));
427
+ }
428
+
429
+ equals(r: Rotor4, epsilon = 0): boolean {
430
+ return (
431
+ (epsilon === 0 ? this.scalar === r.scalar : Math.abs(this.scalar - r.scalar) <= epsilon) &&
432
+ this.bivector.equals(r.bivector, epsilon) &&
433
+ (epsilon === 0 ? this.pseudoscalar === r.pseudoscalar : Math.abs(this.pseudoscalar - r.pseudoscalar) <= epsilon)
434
+ );
435
+ }
436
+
437
+ toString(): string {
438
+ return `${this.scalar} + (${this.bivector.toString()}) + ${this.pseudoscalar}e1234`;
439
+ }
440
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * SpecialFunctions — the higher transcendental functions that statistics and
3
+ * analysis rely on: the gamma and log-gamma functions (Lanczos), the beta
4
+ * function, the error function `erf`/`erfc`, and the regularised incomplete
5
+ * gamma and beta functions (which give the distribution CDFs in
6
+ * {@link Distributions} exact closed forms rather than numeric integration).
7
+ */
8
+
9
+ const LANCZOS_G = 7;
10
+ const LANCZOS = [
11
+ 0.9999999999998099, 676.5203681218851, -1259.1392167224028, 771.3234287776531, -176.6150291621406, 12.507343278686905,
12
+ -0.13857109526572012, 9.984369578019572e-6, 1.5056327351493116e-7,
13
+ ];
14
+
15
+ export class SpecialFunctions {
16
+ /** The gamma function Γ(x) (Lanczos approximation). */
17
+ static gamma(x: number): number {
18
+ if (x < 0.5) return Math.PI / (Math.sin(Math.PI * x) * SpecialFunctions.gamma(1 - x));
19
+ const z = x - 1;
20
+ let a = LANCZOS[0];
21
+ const t = z + LANCZOS_G + 0.5;
22
+ for (let i = 1; i < LANCZOS.length; i++) a += LANCZOS[i] / (z + i);
23
+ return Math.sqrt(2 * Math.PI) * t ** (z + 0.5) * Math.exp(-t) * a;
24
+ }
25
+
26
+ /** The natural log of |Γ(x)| — stable for large arguments. */
27
+ static lnGamma(x: number): number {
28
+ if (x < 0.5) return Math.log(Math.abs(Math.PI / Math.sin(Math.PI * x))) - SpecialFunctions.lnGamma(1 - x);
29
+ const z = x - 1;
30
+ let a = LANCZOS[0];
31
+ const t = z + LANCZOS_G + 0.5;
32
+ for (let i = 1; i < LANCZOS.length; i++) a += LANCZOS[i] / (z + i);
33
+ return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(a);
34
+ }
35
+
36
+ /** The beta function B(a, b) = Γ(a)Γ(b)/Γ(a+b). */
37
+ static beta(a: number, b: number): number {
38
+ return Math.exp(SpecialFunctions.lnGamma(a) + SpecialFunctions.lnGamma(b) - SpecialFunctions.lnGamma(a + b));
39
+ }
40
+
41
+ /** The error function erf(x). */
42
+ static erf(x: number): number {
43
+ const sign = x < 0 ? -1 : 1;
44
+ const ax = Math.abs(x);
45
+ const t = 1 / (1 + 0.3275911 * ax);
46
+ const y =
47
+ 1 -
48
+ ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) *
49
+ t *
50
+ Math.exp(-ax * ax);
51
+ return sign * y;
52
+ }
53
+
54
+ /** The complementary error function erfc(x) = 1 − erf(x). */
55
+ static erfc(x: number): number {
56
+ return 1 - SpecialFunctions.erf(x);
57
+ }
58
+
59
+ /**
60
+ * The regularised lower incomplete gamma function P(a, x) = γ(a, x)/Γ(a).
61
+ * (This is the CDF of the gamma/chi-square distributions.)
62
+ */
63
+ static regularizedGammaP(a: number, x: number): number {
64
+ if (x < 0 || a <= 0) return Number.NaN;
65
+ if (x === 0) return 0;
66
+ if (x < a + 1) {
67
+ // series expansion
68
+ let ap = a;
69
+ let sum = 1 / a;
70
+ let del = sum;
71
+ for (let n = 0; n < 200; n++) {
72
+ ap++;
73
+ del *= x / ap;
74
+ sum += del;
75
+ if (Math.abs(del) < Math.abs(sum) * 1e-15) break;
76
+ }
77
+ return sum * Math.exp(-x + a * Math.log(x) - SpecialFunctions.lnGamma(a));
78
+ }
79
+ // continued fraction for the upper part Q, then P = 1 - Q
80
+ const tiny = 1e-300;
81
+ let b = x + 1 - a;
82
+ let c = 1 / tiny;
83
+ let d = 1 / b;
84
+ let h = d;
85
+ for (let i = 1; i < 200; i++) {
86
+ const an = -i * (i - a);
87
+ b += 2;
88
+ d = an * d + b;
89
+ if (Math.abs(d) < tiny) d = tiny;
90
+ c = b + an / c;
91
+ if (Math.abs(c) < tiny) c = tiny;
92
+ d = 1 / d;
93
+ const del = d * c;
94
+ h *= del;
95
+ if (Math.abs(del - 1) < 1e-15) break;
96
+ }
97
+ const q = Math.exp(-x + a * Math.log(x) - SpecialFunctions.lnGamma(a)) * h;
98
+ return 1 - q;
99
+ }
100
+
101
+ /** The regularised upper incomplete gamma function Q(a, x) = 1 − P(a, x). */
102
+ static regularizedGammaQ(a: number, x: number): number {
103
+ return 1 - SpecialFunctions.regularizedGammaP(a, x);
104
+ }
105
+
106
+ /**
107
+ * The regularised incomplete beta function I_x(a, b). (This is the CDF of the
108
+ * beta distribution and underlies the Student-t and F distributions.)
109
+ */
110
+ static regularizedIncompleteBeta(x: number, a: number, b: number): number {
111
+ if (x <= 0) return 0;
112
+ if (x >= 1) return 1;
113
+ const lbeta = SpecialFunctions.lnGamma(a + b) - SpecialFunctions.lnGamma(a) - SpecialFunctions.lnGamma(b);
114
+ const front = Math.exp(lbeta + a * Math.log(x) + b * Math.log(1 - x));
115
+ // Use the continued fraction that converges fastest, swapping (a,b,x) as needed.
116
+ if (x < (a + 1) / (a + b + 2)) return (front * betaContinuedFraction(a, b, x)) / a;
117
+ return 1 - (front * betaContinuedFraction(b, a, 1 - x)) / b;
118
+ }
119
+ }
120
+
121
+ /** Lentz's continued fraction for the incomplete beta (Numerical Recipes `betacf`). */
122
+ function betaContinuedFraction(a: number, b: number, x: number): number {
123
+ const tiny = 1e-300;
124
+ const qab = a + b;
125
+ const qap = a + 1;
126
+ const qam = a - 1;
127
+ let c = 1;
128
+ let d = 1 - (qab * x) / qap;
129
+ if (Math.abs(d) < tiny) d = tiny;
130
+ d = 1 / d;
131
+ let h = d;
132
+ for (let m = 1; m < 200; m++) {
133
+ const m2 = 2 * m;
134
+ let aa = (m * (b - m) * x) / ((qam + m2) * (a + m2));
135
+ d = 1 + aa * d;
136
+ if (Math.abs(d) < tiny) d = tiny;
137
+ c = 1 + aa / c;
138
+ if (Math.abs(c) < tiny) c = tiny;
139
+ d = 1 / d;
140
+ h *= d * c;
141
+ aa = (-(a + m) * (qab + m) * x) / ((a + m2) * (qap + m2));
142
+ d = 1 + aa * d;
143
+ if (Math.abs(d) < tiny) d = tiny;
144
+ c = 1 + aa / c;
145
+ if (Math.abs(c) < tiny) c = tiny;
146
+ d = 1 / d;
147
+ const del = d * c;
148
+ h *= del;
149
+ if (Math.abs(del - 1) < 1e-15) break;
150
+ }
151
+ return h;
152
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SpecialOperator — a named symbolic operator (`+`, `-`, `*`, …) pairing a
3
+ * textual representation (`rep`) with the name of the function that implements it
4
+ * (`funct`). Ported from Mallory's ActionScript `SpecialOperator`; used by the
5
+ * expression evaluator.
6
+ */
7
+ export class SpecialOperator {
8
+ readonly rep: string;
9
+ readonly funct: string | null;
10
+
11
+ constructor(rep: string, funct: string | null) {
12
+ this.rep = rep;
13
+ this.funct = funct;
14
+ }
15
+
16
+ toString(): string {
17
+ return `Operator: ${this.rep}`;
18
+ }
19
+
20
+ static readonly Plus = new SpecialOperator("+", "add");
21
+ static readonly Minus = new SpecialOperator("-", "subtract");
22
+ static readonly Times = new SpecialOperator("*", "multiply");
23
+ static readonly Divided = new SpecialOperator("/", "divide");
24
+ static readonly Power = new SpecialOperator("^", "power");
25
+ static readonly Modulous = new SpecialOperator("%", "mod");
26
+ static readonly Elipses = new SpecialOperator("...", null);
27
+ static readonly System = new SpecialOperator("#", null);
28
+ }