@guinetik/gcanvas 1.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 (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,1009 @@
1
+ /**
2
+ * Rank-2 Tensor class for general relativity calculations.
3
+ * Provides immutable tensor operations following the Complex class pattern.
4
+ *
5
+ * @example
6
+ * // Create a Schwarzschild metric at r = 10, rs = 2
7
+ * const g = Tensor.schwarzschild(10, 2);
8
+ * console.log(g.get(0, 0)); // g_tt component
9
+ *
10
+ * @example
11
+ * // Create from components
12
+ * const metric = new Tensor([
13
+ * [-1, 0, 0, 0],
14
+ * [0, 1, 0, 0],
15
+ * [0, 0, 1, 0],
16
+ * [0, 0, 0, 1]
17
+ * ]);
18
+ */
19
+ export class Tensor {
20
+ #components;
21
+ #dimension;
22
+ #name;
23
+ #signature;
24
+ #coordinates;
25
+
26
+ /**
27
+ * Create a new rank-2 tensor from a 2D array of components.
28
+ * @param {number[][]} components - 2D array of tensor components
29
+ * @param {Object} [options={}] - Optional metadata
30
+ * @param {string} [options.name] - Name of the tensor (e.g., 'Schwarzschild')
31
+ * @param {number[]} [options.signature] - Metric signature (e.g., [-1, 1, 1, 1])
32
+ * @param {string[]} [options.coordinates] - Coordinate names (e.g., ['t', 'r', 'θ', 'φ'])
33
+ */
34
+ constructor(components, options = {}) {
35
+ // Deep copy to ensure immutability
36
+ this.#components = components.map((row) => [...row]);
37
+ this.#dimension = components.length;
38
+ this.#name = options.name || "";
39
+ this.#signature = options.signature || null;
40
+ this.#coordinates = options.coordinates || null;
41
+ }
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // STATIC FACTORY METHODS
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Create a Minkowski (flat spacetime) metric tensor.
49
+ * @returns {Tensor} Minkowski metric with signature (-,+,+,+)
50
+ */
51
+ static minkowski() {
52
+ return new Tensor(
53
+ [
54
+ [-1, 0, 0, 0],
55
+ [0, 1, 0, 0],
56
+ [0, 0, 1, 0],
57
+ [0, 0, 0, 1],
58
+ ],
59
+ {
60
+ name: "Minkowski",
61
+ signature: [-1, 1, 1, 1],
62
+ coordinates: ["t", "x", "y", "z"],
63
+ }
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Create a Schwarzschild metric tensor at a given radial position.
69
+ * Uses geometrized units where G = c = 1.
70
+ *
71
+ * @param {number} r - Radial coordinate (must be > rs)
72
+ * @param {number} rs - Schwarzschild radius (2GM/c²)
73
+ * @param {number} [theta=Math.PI/2] - Polar angle (default: equatorial plane)
74
+ * @returns {Tensor} Schwarzschild metric tensor
75
+ */
76
+ /**
77
+ * Create a Schwarzschild metric tensor at a given radial position.
78
+ * Uses geometrized units where G = c = 1.
79
+ *
80
+ * @param {number} r - Radial coordinate (must be > rs)
81
+ * @param {number} rs - Schwarzschild radius (2GM/c²)
82
+ * @param {number} [theta=Math.PI/2] - Polar angle (default: equatorial plane)
83
+ * @returns {Tensor} Schwarzschild metric tensor
84
+ */
85
+ static schwarzschild(r, rs, theta = Math.PI / 2) {
86
+ if (r <= rs) {
87
+ // Inside event horizon - metric components swap signature
88
+ // For visualization purposes, we clamp to a small value outside
89
+ r = rs * 1.001;
90
+ }
91
+
92
+ const factor = 1 - rs / r;
93
+ const sinTheta = Math.sin(theta);
94
+
95
+ return new Tensor(
96
+ [
97
+ [-factor, 0, 0, 0],
98
+ [0, 1 / factor, 0, 0],
99
+ [0, 0, r * r, 0],
100
+ [0, 0, 0, r * r * sinTheta * sinTheta],
101
+ ],
102
+ {
103
+ name: "Schwarzschild",
104
+ signature: [-1, 1, 1, 1],
105
+ coordinates: ["t", "r", "θ", "φ"],
106
+ }
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Create a contravariant (inverse) Schwarzschild metric tensor.
112
+ * @param {number} r - Radial coordinate
113
+ * @param {number} rs - Schwarzschild radius
114
+ * @param {number} [theta=Math.PI/2] - Polar angle
115
+ * @returns {Tensor} Inverse Schwarzschild metric
116
+ */
117
+ static schwarzschildContravariant(r, rs, theta = Math.PI / 2) {
118
+ if (r <= rs) r = rs * 1.001;
119
+
120
+ const factor = 1 - rs / r;
121
+ const sinTheta = Math.sin(theta);
122
+
123
+ return new Tensor(
124
+ [
125
+ [-1 / factor, 0, 0, 0],
126
+ [0, factor, 0, 0],
127
+ [0, 0, 1 / (r * r), 0],
128
+ [0, 0, 0, 1 / (r * r * sinTheta * sinTheta)],
129
+ ],
130
+ {
131
+ name: "Schwarzschild (Contravariant)",
132
+ signature: [-1, 1, 1, 1],
133
+ coordinates: ["t", "r", "θ", "φ"],
134
+ }
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Create a Kerr metric tensor at a given position.
140
+ * Describes spacetime around a rotating (spinning) black hole.
141
+ * Uses Boyer-Lindquist coordinates and geometrized units (G = c = 1).
142
+ *
143
+ * The Kerr metric has an OFF-DIAGONAL g_tφ term representing frame dragging.
144
+ *
145
+ * @param {number} r - Radial coordinate
146
+ * @param {number} theta - Polar angle (0 to π)
147
+ * @param {number} M - Mass parameter
148
+ * @param {number} a - Spin parameter (0 ≤ a ≤ M, a=M is extremal)
149
+ * @returns {Tensor} Kerr metric tensor (non-diagonal)
150
+ */
151
+ static kerr(r, theta, M, a) {
152
+ // Clamp spin to valid range [0, M]
153
+ a = Math.min(Math.abs(a), M);
154
+
155
+ const a2 = a * a;
156
+ const r2 = r * r;
157
+ const cosTheta = Math.cos(theta);
158
+ const sinTheta = Math.sin(theta);
159
+ const sin2Theta = sinTheta * sinTheta;
160
+ const cos2Theta = cosTheta * cosTheta;
161
+
162
+ // Fundamental Kerr quantities
163
+ const Sigma = r2 + a2 * cos2Theta;
164
+ const Delta = r2 - 2 * M * r + a2;
165
+
166
+ // Outer event horizon radius
167
+ const rPlus = M + Math.sqrt(Math.max(0, M * M - a2));
168
+
169
+ // Clamp to just outside horizon to avoid coordinate singularity
170
+ if (r <= rPlus) {
171
+ r = rPlus * 1.001;
172
+ const r2New = r * r;
173
+ const SigmaNew = r2New + a2 * cos2Theta;
174
+ const DeltaNew = r2New - 2 * M * r + a2;
175
+ return Tensor._buildKerrMetric(r, theta, M, a, SigmaNew, DeltaNew, sin2Theta);
176
+ }
177
+
178
+ return Tensor._buildKerrMetric(r, theta, M, a, Sigma, Delta, sin2Theta);
179
+ }
180
+
181
+ /**
182
+ * Create a contravariant (inverse) Kerr metric tensor.
183
+ * Uses analytical block-inversion for the t-φ coupling.
184
+ *
185
+ * @param {number} r - Radial coordinate
186
+ * @param {number} theta - Polar angle
187
+ * @param {number} M - Mass parameter
188
+ * @param {number} a - Spin parameter
189
+ * @returns {Tensor} Inverse Kerr metric
190
+ */
191
+ static kerrContravariant(r, theta, M, a) {
192
+ a = Math.min(Math.abs(a), M);
193
+ const a2 = a * a;
194
+ const r2 = r * r;
195
+ const cosTheta = Math.cos(theta);
196
+ const sinTheta = Math.sin(theta);
197
+ const sin2Theta = sinTheta * sinTheta;
198
+
199
+ const Sigma = r2 + a2 * cosTheta * cosTheta;
200
+ const Delta = r2 - 2 * M * r + a2;
201
+
202
+ const rPlus = M + Math.sqrt(Math.max(0, M * M - a2));
203
+ if (r <= rPlus) r = rPlus * 1.001;
204
+
205
+ // Direct analytical inverse components for Kerr metric
206
+ // g^tt = -( (r²+a²)² - a²Δsin²θ ) / (ΣΔ)
207
+ const g_inv_tt = -(Math.pow(r2 + a2, 2) - a2 * Delta * sin2Theta) / (Sigma * Delta);
208
+
209
+ // g^rr = Δ / Σ
210
+ const g_inv_rr = Delta / Sigma;
211
+
212
+ // g^θθ = 1 / Σ
213
+ const g_inv_thth = 1 / Sigma;
214
+
215
+ // g^φφ = (Δ - a²sin²θ) / (ΣΔsin²θ)
216
+ const g_inv_phph = (Delta - a2 * sin2Theta) / (Sigma * Delta * sin2Theta);
217
+
218
+ // g^tφ = -(2Mar) / (ΣΔ)
219
+ const g_inv_tph = -(2 * M * a * r) / (Sigma * Delta);
220
+
221
+ return new Tensor(
222
+ [
223
+ [g_inv_tt, 0, 0, g_inv_tph],
224
+ [0, g_inv_rr, 0, 0],
225
+ [0, 0, g_inv_thth, 0],
226
+ [g_inv_tph, 0, 0, g_inv_phph],
227
+ ],
228
+ {
229
+ name: "Kerr (Contravariant)",
230
+ signature: [-1, 1, 1, 1],
231
+ coordinates: ["t", "r", "θ", "φ"],
232
+ }
233
+ );
234
+ }
235
+
236
+ /**
237
+ * Internal helper to build Kerr metric components.
238
+ * @private
239
+ */
240
+ static _buildKerrMetric(r, theta, M, a, Sigma, Delta, sin2Theta) {
241
+ const a2 = a * a;
242
+ const r2 = r * r;
243
+
244
+ // Metric components in Boyer-Lindquist coordinates
245
+ // g_tt = -(1 - 2Mr/Σ)
246
+ const g_tt = -(1 - (2 * M * r) / Sigma);
247
+
248
+ // g_rr = Σ/Δ
249
+ const g_rr = Sigma / Delta;
250
+
251
+ // g_θθ = Σ
252
+ const g_thth = Sigma;
253
+
254
+ // g_φφ = (r² + a² + 2Ma²r sin²θ/Σ) sin²θ
255
+ const g_phph = (r2 + a2 + (2 * M * a2 * r * sin2Theta) / Sigma) * sin2Theta;
256
+
257
+ // OFF-DIAGONAL: g_tφ = g_φt = -2Mar sin²θ / Σ
258
+ // This is the FRAME DRAGGING term!
259
+ const g_tph = -(2 * M * a * r * sin2Theta) / Sigma;
260
+
261
+ return new Tensor(
262
+ [
263
+ [g_tt, 0, 0, g_tph],
264
+ [0, g_rr, 0, 0],
265
+ [0, 0, g_thth, 0],
266
+ [g_tph, 0, 0, g_phph],
267
+ ],
268
+ {
269
+ name: "Kerr",
270
+ signature: [-1, 1, 1, 1],
271
+ coordinates: ["t", "r", "θ", "φ"],
272
+ }
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Calculate Kerr horizon radii.
278
+ * r± = M ± √(M² - a²)
279
+ *
280
+ * @param {number} M - Mass parameter
281
+ * @param {number} a - Spin parameter
282
+ * @param {boolean} [inner=false] - Return inner (Cauchy) horizon if true
283
+ * @returns {number} Horizon radius (outer by default, NaN if a > M)
284
+ */
285
+ static kerrHorizonRadius(M, a, inner = false) {
286
+ const discriminant = M * M - a * a;
287
+ if (discriminant < 0) return NaN; // Naked singularity (a > M)
288
+ const sqrtDisc = Math.sqrt(discriminant);
289
+ return inner ? M - sqrtDisc : M + sqrtDisc;
290
+ }
291
+
292
+ /**
293
+ * Calculate ergosphere radius (static limit surface).
294
+ * r_ergo(θ) = M + √(M² - a²cos²θ)
295
+ *
296
+ * At poles (θ=0,π): r_ergo = r+ (touches horizon)
297
+ * At equator (θ=π/2): r_ergo = 2M (maximum extent)
298
+ *
299
+ * @param {number} M - Mass parameter
300
+ * @param {number} a - Spin parameter
301
+ * @param {number} theta - Polar angle
302
+ * @returns {number} Ergosphere radius at given theta
303
+ */
304
+ static kerrErgosphereRadius(M, a, theta) {
305
+ const cosTheta = Math.cos(theta);
306
+ const discriminant = M * M - a * a * cosTheta * cosTheta;
307
+ if (discriminant < 0) return NaN;
308
+ return M + Math.sqrt(discriminant);
309
+ }
310
+
311
+ /**
312
+ * Calculate ISCO radius for Kerr metric.
313
+ * Different for prograde (co-rotating) vs retrograde (counter-rotating) orbits.
314
+ *
315
+ * Prograde ISCO: approaches M as a → M (can orbit closer)
316
+ * Retrograde ISCO: approaches 9M as a → M (must orbit farther)
317
+ *
318
+ * Uses the exact Bardeen formula.
319
+ *
320
+ * @param {number} M - Mass parameter
321
+ * @param {number} a - Spin parameter
322
+ * @param {boolean} [prograde=true] - Prograde orbit if true
323
+ * @returns {number} ISCO radius
324
+ */
325
+ static kerrISCO(M, a, prograde = true) {
326
+ const aM = a / M; // Dimensionless spin
327
+ const sign = prograde ? 1 : -1;
328
+
329
+ // Bardeen's formula for Kerr ISCO
330
+ const Z1 = 1 + Math.cbrt(1 - aM * aM) * (Math.cbrt(1 + aM) + Math.cbrt(1 - aM));
331
+ const Z2 = Math.sqrt(3 * aM * aM + Z1 * Z1);
332
+
333
+ return M * (3 + Z2 - sign * Math.sqrt((3 - Z1) * (3 + Z1 + 2 * Z2)));
334
+ }
335
+
336
+ /**
337
+ * Calculate frame-dragging angular velocity (omega).
338
+ * This is the angular velocity at which spacetime itself rotates.
339
+ *
340
+ * ω = -g_tφ / g_φφ
341
+ *
342
+ * At large r: ω ≈ 2Ma/r³ (Lense-Thirring precession)
343
+ *
344
+ * @param {number} r - Radial coordinate
345
+ * @param {number} theta - Polar angle
346
+ * @param {number} M - Mass parameter
347
+ * @param {number} a - Spin parameter
348
+ * @returns {number} Frame dragging angular velocity
349
+ */
350
+ static kerrFrameDraggingOmega(r, theta, M, a) {
351
+ const metric = Tensor.kerr(r, theta, M, a);
352
+ const g_tph = metric.get(0, 3);
353
+ const g_phph = metric.get(3, 3);
354
+
355
+ if (Math.abs(g_phph) < 1e-10) return 0;
356
+ return -g_tph / g_phph;
357
+ }
358
+
359
+ /**
360
+ * Calculate effective potential for Kerr geodesics (equatorial plane).
361
+ *
362
+ * @param {number} M - Mass parameter
363
+ * @param {number} a - Spin parameter
364
+ * @param {number} E - Energy per unit mass
365
+ * @param {number} L - Angular momentum per unit mass
366
+ * @param {number} r - Radial coordinate
367
+ * @returns {number} Effective potential value
368
+ */
369
+ static kerrEffectivePotential(M, a, E, L, r) {
370
+ if (r <= 0) return Infinity;
371
+
372
+ const r2 = r * r;
373
+ const a2 = a * a;
374
+ const Delta = r2 - 2 * M * r + a2;
375
+
376
+ // Simplified effective potential for equatorial orbits
377
+ const term1 = -M / r;
378
+ const term2 = (L * L - a2 * (E * E - 1)) / (2 * r2);
379
+ const term3 = -M * Math.pow(L - a * E, 2) / (r2 * r);
380
+
381
+ return term1 + term2 + term3;
382
+ }
383
+
384
+ /**
385
+ * Create a diagonal tensor from an array of values.
386
+ * @param {number[]} values - Diagonal values
387
+ * @param {Object} [options={}] - Optional metadata
388
+ * @returns {Tensor} Diagonal tensor
389
+ */
390
+ static diagonal(values, options = {}) {
391
+ const n = values.length;
392
+ const components = [];
393
+ for (let i = 0; i < n; i++) {
394
+ components[i] = [];
395
+ for (let j = 0; j < n; j++) {
396
+ components[i][j] = i === j ? values[i] : 0;
397
+ }
398
+ }
399
+ return new Tensor(components, options);
400
+ }
401
+
402
+ /**
403
+ * Create an identity tensor of given dimension.
404
+ * @param {number} [n=4] - Dimension
405
+ * @returns {Tensor} Identity tensor
406
+ */
407
+ static identity(n = 4) {
408
+ const values = new Array(n).fill(1);
409
+ return Tensor.diagonal(values, { name: "Identity" });
410
+ }
411
+
412
+ /**
413
+ * Create a zero tensor of given dimension.
414
+ * @param {number} [n=4] - Dimension
415
+ * @returns {Tensor} Zero tensor
416
+ */
417
+ static zero(n = 4) {
418
+ const components = [];
419
+ for (let i = 0; i < n; i++) {
420
+ components[i] = new Array(n).fill(0);
421
+ }
422
+ return new Tensor(components, { name: "Zero" });
423
+ }
424
+
425
+ // ─────────────────────────────────────────────────────────────────────────────
426
+ // COMPONENT ACCESS
427
+ // ─────────────────────────────────────────────────────────────────────────────
428
+
429
+ /**
430
+ * Get a component at the specified indices.
431
+ * @param {number} i - Row index
432
+ * @param {number} j - Column index
433
+ * @returns {number} Component value
434
+ */
435
+ get(i, j) {
436
+ return this.#components[i][j];
437
+ }
438
+
439
+ /**
440
+ * Return a new tensor with the specified component changed.
441
+ * @param {number} i - Row index
442
+ * @param {number} j - Column index
443
+ * @param {number} value - New value
444
+ * @returns {Tensor} New tensor with updated component
445
+ */
446
+ set(i, j, value) {
447
+ const newComponents = this.#components.map((row) => [...row]);
448
+ newComponents[i][j] = value;
449
+ return new Tensor(newComponents, {
450
+ name: this.#name,
451
+ signature: this.#signature,
452
+ coordinates: this.#coordinates,
453
+ });
454
+ }
455
+
456
+ /**
457
+ * Get the diagonal components as an array.
458
+ * @returns {number[]} Diagonal values
459
+ */
460
+ getDiagonal() {
461
+ const diag = [];
462
+ for (let i = 0; i < this.#dimension; i++) {
463
+ diag.push(this.#components[i][i]);
464
+ }
465
+ return diag;
466
+ }
467
+
468
+ /**
469
+ * Get the dimension of the tensor.
470
+ * @returns {number} Dimension (n for n×n tensor)
471
+ */
472
+ get dimension() {
473
+ return this.#dimension;
474
+ }
475
+
476
+ /**
477
+ * Get the tensor name.
478
+ * @returns {string} Name
479
+ */
480
+ get name() {
481
+ return this.#name;
482
+ }
483
+
484
+ /**
485
+ * Get the metric signature.
486
+ * @returns {number[]|null} Signature array or null
487
+ */
488
+ get signature() {
489
+ return this.#signature;
490
+ }
491
+
492
+ /**
493
+ * Get the coordinate names.
494
+ * @returns {string[]|null} Coordinate names or null
495
+ */
496
+ get coordinates() {
497
+ return this.#coordinates;
498
+ }
499
+
500
+ // ─────────────────────────────────────────────────────────────────────────────
501
+ // TENSOR OPERATIONS
502
+ // ─────────────────────────────────────────────────────────────────────────────
503
+
504
+ /**
505
+ * Add another tensor to this one.
506
+ * @param {Tensor} other - Tensor to add
507
+ * @returns {Tensor} Sum of tensors
508
+ */
509
+ add(other) {
510
+ const result = [];
511
+ for (let i = 0; i < this.#dimension; i++) {
512
+ result[i] = [];
513
+ for (let j = 0; j < this.#dimension; j++) {
514
+ result[i][j] = this.#components[i][j] + other.get(i, j);
515
+ }
516
+ }
517
+ return new Tensor(result);
518
+ }
519
+
520
+ /**
521
+ * Subtract another tensor from this one.
522
+ * @param {Tensor} other - Tensor to subtract
523
+ * @returns {Tensor} Difference of tensors
524
+ */
525
+ subtract(other) {
526
+ const result = [];
527
+ for (let i = 0; i < this.#dimension; i++) {
528
+ result[i] = [];
529
+ for (let j = 0; j < this.#dimension; j++) {
530
+ result[i][j] = this.#components[i][j] - other.get(i, j);
531
+ }
532
+ }
533
+ return new Tensor(result);
534
+ }
535
+
536
+ /**
537
+ * Multiply this tensor by a scalar.
538
+ * @param {number} scalar - Scalar multiplier
539
+ * @returns {Tensor} Scaled tensor
540
+ */
541
+ scale(scalar) {
542
+ const result = [];
543
+ for (let i = 0; i < this.#dimension; i++) {
544
+ result[i] = [];
545
+ for (let j = 0; j < this.#dimension; j++) {
546
+ result[i][j] = this.#components[i][j] * scalar;
547
+ }
548
+ }
549
+ return new Tensor(result);
550
+ }
551
+
552
+ /**
553
+ * Matrix multiply this tensor with another.
554
+ * @param {Tensor} other - Tensor to multiply with
555
+ * @returns {Tensor} Product tensor
556
+ */
557
+ multiply(other) {
558
+ const result = [];
559
+ for (let i = 0; i < this.#dimension; i++) {
560
+ result[i] = [];
561
+ for (let j = 0; j < this.#dimension; j++) {
562
+ let sum = 0;
563
+ for (let k = 0; k < this.#dimension; k++) {
564
+ sum += this.#components[i][k] * other.get(k, j);
565
+ }
566
+ result[i][j] = sum;
567
+ }
568
+ }
569
+ return new Tensor(result);
570
+ }
571
+
572
+ /**
573
+ * Transpose this tensor (swap indices).
574
+ * @returns {Tensor} Transposed tensor
575
+ */
576
+ transpose() {
577
+ const result = [];
578
+ for (let i = 0; i < this.#dimension; i++) {
579
+ result[i] = [];
580
+ for (let j = 0; j < this.#dimension; j++) {
581
+ result[i][j] = this.#components[j][i];
582
+ }
583
+ }
584
+ return new Tensor(result, {
585
+ name: this.#name ? `${this.#name}ᵀ` : "",
586
+ signature: this.#signature,
587
+ coordinates: this.#coordinates,
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Compute the inverse of this tensor (for rank-2).
593
+ * Uses Gaussian elimination with partial pivoting.
594
+ * @returns {Tensor} Inverse tensor
595
+ */
596
+ inverse() {
597
+ const n = this.#dimension;
598
+
599
+ // Fast path for diagonal tensors: O(n)
600
+ if (this.isDiagonal()) {
601
+ const diag = this.getDiagonal();
602
+ const invDiag = diag.map((v) => {
603
+ if (Math.abs(v) < 1e-15) {
604
+ throw new Error("Diagonal matrix is singular, cannot compute inverse");
605
+ }
606
+ return 1 / v;
607
+ });
608
+ return Tensor.diagonal(invDiag, {
609
+ name: this.#name ? `${this.#name}⁻¹` : "",
610
+ signature: this.#signature,
611
+ coordinates: this.#coordinates,
612
+ });
613
+ }
614
+
615
+ // Create augmented matrix [A|I]
616
+ const aug = [];
617
+ for (let i = 0; i < n; i++) {
618
+ aug[i] = [];
619
+ for (let j = 0; j < n; j++) {
620
+ aug[i][j] = this.#components[i][j];
621
+ }
622
+ for (let j = 0; j < n; j++) {
623
+ aug[i][n + j] = i === j ? 1 : 0;
624
+ }
625
+ }
626
+
627
+ // Gaussian elimination with partial pivoting
628
+ for (let col = 0; col < n; col++) {
629
+ // Find pivot
630
+ let maxRow = col;
631
+ for (let row = col + 1; row < n; row++) {
632
+ if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col])) {
633
+ maxRow = row;
634
+ }
635
+ }
636
+
637
+ // Swap rows
638
+ [aug[col], aug[maxRow]] = [aug[maxRow], aug[col]];
639
+
640
+ // Check for singular matrix
641
+ if (Math.abs(aug[col][col]) < 1e-10) {
642
+ throw new Error("Matrix is singular, cannot compute inverse");
643
+ }
644
+
645
+ // Scale pivot row
646
+ const pivot = aug[col][col];
647
+ for (let j = 0; j < 2 * n; j++) {
648
+ aug[col][j] /= pivot;
649
+ }
650
+
651
+ // Eliminate column
652
+ for (let row = 0; row < n; row++) {
653
+ if (row !== col) {
654
+ const factor = aug[row][col];
655
+ for (let j = 0; j < 2 * n; j++) {
656
+ aug[row][j] -= factor * aug[col][j];
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ // Extract inverse from augmented matrix
663
+ const result = [];
664
+ for (let i = 0; i < n; i++) {
665
+ result[i] = [];
666
+ for (let j = 0; j < n; j++) {
667
+ result[i][j] = aug[i][n + j];
668
+ }
669
+ }
670
+
671
+ return new Tensor(result, {
672
+ name: this.#name ? `${this.#name}⁻¹` : "",
673
+ signature: this.#signature,
674
+ coordinates: this.#coordinates,
675
+ });
676
+ }
677
+
678
+ // ─────────────────────────────────────────────────────────────────────────────
679
+ // DERIVED QUANTITIES
680
+ // ─────────────────────────────────────────────────────────────────────────────
681
+
682
+ /**
683
+ * Compute the determinant of this tensor.
684
+ * Uses LU decomposition for efficiency.
685
+ * @returns {number} Determinant
686
+ */
687
+ determinant() {
688
+ const n = this.#dimension;
689
+
690
+ // Fast path for diagonal tensors: O(n)
691
+ if (this.isDiagonal()) {
692
+ return this.getDiagonal().reduce((acc, v) => acc * v, 1);
693
+ }
694
+
695
+ // For small matrices, use direct formulas
696
+ if (n === 2) {
697
+ return (
698
+ this.#components[0][0] * this.#components[1][1] -
699
+ this.#components[0][1] * this.#components[1][0]
700
+ );
701
+ }
702
+
703
+ if (n === 3) {
704
+ const m = this.#components;
705
+ return (
706
+ m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) -
707
+ m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +
708
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
709
+ );
710
+ }
711
+
712
+ // For 4x4 and larger, use LU decomposition
713
+ const lu = this.#components.map((row) => [...row]);
714
+ let det = 1;
715
+ let swaps = 0;
716
+
717
+ for (let col = 0; col < n; col++) {
718
+ // Find pivot
719
+ let maxRow = col;
720
+ for (let row = col + 1; row < n; row++) {
721
+ if (Math.abs(lu[row][col]) > Math.abs(lu[maxRow][col])) {
722
+ maxRow = row;
723
+ }
724
+ }
725
+
726
+ if (maxRow !== col) {
727
+ [lu[col], lu[maxRow]] = [lu[maxRow], lu[col]];
728
+ swaps++;
729
+ }
730
+
731
+ if (Math.abs(lu[col][col]) < 1e-10) {
732
+ return 0;
733
+ }
734
+
735
+ det *= lu[col][col];
736
+
737
+ for (let row = col + 1; row < n; row++) {
738
+ lu[row][col] /= lu[col][col];
739
+ for (let j = col + 1; j < n; j++) {
740
+ lu[row][j] -= lu[row][col] * lu[col][j];
741
+ }
742
+ }
743
+ }
744
+
745
+ return swaps % 2 === 0 ? det : -det;
746
+ }
747
+
748
+ /**
749
+ * Compute the trace (sum of diagonal elements).
750
+ * @returns {number} Trace
751
+ */
752
+ trace() {
753
+ let sum = 0;
754
+ for (let i = 0; i < this.#dimension; i++) {
755
+ sum += this.#components[i][i];
756
+ }
757
+ return sum;
758
+ }
759
+
760
+ /**
761
+ * Check if the tensor is diagonal (off-diagonal elements near zero).
762
+ * @param {number} [tolerance=1e-10] - Tolerance for zero comparison
763
+ * @returns {boolean} True if diagonal
764
+ */
765
+ isDiagonal(tolerance = 1e-10) {
766
+ for (let i = 0; i < this.#dimension; i++) {
767
+ for (let j = 0; j < this.#dimension; j++) {
768
+ if (i !== j && Math.abs(this.#components[i][j]) > tolerance) {
769
+ return false;
770
+ }
771
+ }
772
+ }
773
+ return true;
774
+ }
775
+
776
+ /**
777
+ * Check if the tensor is symmetric.
778
+ * @param {number} [tolerance=1e-10] - Tolerance for comparison
779
+ * @returns {boolean} True if symmetric
780
+ */
781
+ isSymmetric(tolerance = 1e-10) {
782
+ for (let i = 0; i < this.#dimension; i++) {
783
+ for (let j = i + 1; j < this.#dimension; j++) {
784
+ if (
785
+ Math.abs(this.#components[i][j] - this.#components[j][i]) > tolerance
786
+ ) {
787
+ return false;
788
+ }
789
+ }
790
+ }
791
+ return true;
792
+ }
793
+
794
+ // ─────────────────────────────────────────────────────────────────────────────
795
+ // GR-SPECIFIC STATIC UTILITIES
796
+ // ─────────────────────────────────────────────────────────────────────────────
797
+
798
+ /**
799
+ * Compute Christoffel symbols (connection coefficients) for a metric.
800
+ * Uses numerical differentiation by default, but switches to analytical
801
+ * forms for recognized metrics (e.g., Schwarzschild).
802
+ *
803
+ * @param {Function} metricFn - Function(position: number[]) => Tensor
804
+ * @param {number[]} position - Position [t, r, θ, φ] at which to evaluate
805
+ * @param {number} [delta=0.001] - Step size for numerical differentiation
806
+ * @returns {number[][][]} Christoffel symbols Γ[lambda][mu][nu]
807
+ */
808
+ static christoffel(metricFn, position, delta = 0.001) {
809
+ const g = metricFn(position);
810
+
811
+ // Fast path for Schwarzschild metric (analytical)
812
+ if (g.name === "Schwarzschild") {
813
+ const rs = position._rs || 2; // Expecting rs attached to position or default
814
+ return Tensor.schwarzschildChristoffel(position[1], rs, position[2]);
815
+ }
816
+
817
+ const dim = 4;
818
+ const result = [];
819
+ const gInv = g.inverse();
820
+
821
+ // Calculate partial derivatives ∂_ρ g_μν
822
+ const dg = []; // dg[rho][mu][nu] = ∂_ρ g_μν
823
+ for (let rho = 0; rho < dim; rho++) {
824
+ const posPlus = [...position];
825
+ const posMinus = [...position];
826
+ posPlus[rho] += delta;
827
+ posMinus[rho] -= delta;
828
+
829
+ const gPlus = metricFn(posPlus);
830
+ const gMinus = metricFn(posMinus);
831
+
832
+ dg[rho] = [];
833
+ for (let mu = 0; mu < dim; mu++) {
834
+ dg[rho][mu] = [];
835
+ for (let nu = 0; nu < dim; nu++) {
836
+ dg[rho][mu][nu] = (gPlus.get(mu, nu) - gMinus.get(mu, nu)) / (2 * delta);
837
+ }
838
+ }
839
+ }
840
+
841
+ // Compute Christoffel symbols using Einstein notation logic
842
+ // Γ^λ_μν = ½ g^λσ (∂_μ g_νσ + ∂_ν g_μσ - ∂_σ g_μν)
843
+ for (let lambda = 0; lambda < dim; lambda++) {
844
+ result[lambda] = [];
845
+ for (let mu = 0; mu < dim; mu++) {
846
+ result[lambda][mu] = [];
847
+ for (let nu = 0; nu < dim; nu++) {
848
+ let sum = 0;
849
+ for (let sigma = 0; sigma < dim; sigma++) {
850
+ const g_inv_ls = gInv.get(lambda, sigma);
851
+ if (Math.abs(g_inv_ls) < 1e-15) continue; // Skip zero terms
852
+
853
+ sum +=
854
+ (g_inv_ls *
855
+ (dg[mu][nu][sigma] + dg[nu][mu][sigma] - dg[sigma][mu][nu])) /
856
+ 2;
857
+ }
858
+ result[lambda][mu][nu] = sum;
859
+ }
860
+ }
861
+ }
862
+
863
+ return result;
864
+ }
865
+
866
+ /**
867
+ * Analytical Christoffel symbols for Schwarzschild metric.
868
+ * @param {number} r - Radial coordinate
869
+ * @param {number} rs - Schwarzschild radius
870
+ * @param {number} theta - Polar angle
871
+ * @returns {number[][][]} Christoffel symbols
872
+ */
873
+ static schwarzschildChristoffel(r, rs, theta) {
874
+ const dim = 4;
875
+ const G = []; // Gamma[lambda][mu][nu]
876
+ for (let i = 0; i < dim; i++) {
877
+ G[i] = [];
878
+ for (let j = 0; j < dim; j++) {
879
+ G[i][j] = new Array(dim).fill(0);
880
+ }
881
+ }
882
+
883
+ const factor = 1 - rs / r;
884
+ if (Math.abs(factor) < 1e-10) return G; // singularity
885
+
886
+ const rs_2r2 = rs / (2 * r * r);
887
+ const cotTheta = 1 / Math.tan(theta);
888
+ const sinTheta = Math.sin(theta);
889
+ const cosTheta = Math.cos(theta);
890
+
891
+ // Gamma^t_tr = Gamma^t_rt
892
+ G[0][0][1] = G[0][1][0] = rs_2r2 / factor;
893
+
894
+ // Gamma^r_tt
895
+ G[1][0][0] = (rs * factor) / (2 * r * r);
896
+
897
+ // Gamma^r_rr
898
+ G[1][1][1] = -rs_2r2 / factor;
899
+
900
+ // Gamma^r_thth
901
+ G[1][2][2] = -(r - rs);
902
+
903
+ // Gamma^r_phph
904
+ G[1][3][3] = -(r - rs) * sinTheta * sinTheta;
905
+
906
+ // Gamma^th_rth = Gamma^th_thr
907
+ G[2][1][2] = G[2][2][1] = 1 / r;
908
+
909
+ // Gamma^th_phph
910
+ G[2][3][3] = -sinTheta * cosTheta;
911
+
912
+ // Gamma^ph_rph = Gamma^ph_phr
913
+ G[3][1][3] = G[3][3][1] = 1 / r;
914
+
915
+ // Gamma^ph_thph = Gamma^ph_phth
916
+ G[3][2][3] = G[3][3][2] = cotTheta;
917
+
918
+ return G;
919
+ }
920
+
921
+ /**
922
+ * Compute the effective potential for geodesic motion in Schwarzschild spacetime.
923
+ *
924
+ * V_eff = -M/r + L²/(2r²) - ML²/r³
925
+ *
926
+ * @param {number} M - Mass (Schwarzschild radius / 2)
927
+ * @param {number} L - Angular momentum per unit mass
928
+ * @param {number} r - Radial coordinate
929
+ * @returns {number} Effective potential value
930
+ */
931
+ static effectivePotential(M, L, r) {
932
+ if (r <= 0) return Infinity;
933
+ const L2 = L * L;
934
+ return -M / r + L2 / (2 * r * r) - (M * L2) / (r * r * r);
935
+ }
936
+
937
+ /**
938
+ * Find the ISCO (Innermost Stable Circular Orbit) radius.
939
+ * For Schwarzschild: r_ISCO = 6M = 3rs
940
+ *
941
+ * @param {number} rs - Schwarzschild radius
942
+ * @returns {number} ISCO radius
943
+ */
944
+ static iscoRadius(rs) {
945
+ return 3 * rs;
946
+ }
947
+
948
+ /**
949
+ * Find the photon sphere radius.
950
+ * For Schwarzschild: r_photon = 3M = 1.5rs
951
+ *
952
+ * @param {number} rs - Schwarzschild radius
953
+ * @returns {number} Photon sphere radius
954
+ */
955
+ static photonSphereRadius(rs) {
956
+ return 1.5 * rs;
957
+ }
958
+
959
+ // ─────────────────────────────────────────────────────────────────────────────
960
+ // DISPLAY/UTILITY
961
+ // ─────────────────────────────────────────────────────────────────────────────
962
+
963
+ /**
964
+ * Get a flat array of all components (row-major order).
965
+ * @returns {number[]} Flat array
966
+ */
967
+ toArray() {
968
+ const flat = [];
969
+ for (let i = 0; i < this.#dimension; i++) {
970
+ for (let j = 0; j < this.#dimension; j++) {
971
+ flat.push(this.#components[i][j]);
972
+ }
973
+ }
974
+ return flat;
975
+ }
976
+
977
+ /**
978
+ * Get a 2D array copy of the components.
979
+ * @returns {number[][]} 2D array
980
+ */
981
+ toMatrix() {
982
+ return this.#components.map((row) => [...row]);
983
+ }
984
+
985
+ /**
986
+ * Get a string representation of the tensor.
987
+ * @param {number} [precision=3] - Decimal precision
988
+ * @returns {string} String representation
989
+ */
990
+ toString(precision = 3) {
991
+ const header = this.#name ? `${this.#name} Tensor:\n` : "";
992
+ const rows = this.#components.map(
993
+ (row) => `[ ${row.map((v) => v.toFixed(precision).padStart(10)).join(" ")} ]`
994
+ );
995
+ return header + rows.join("\n");
996
+ }
997
+
998
+ /**
999
+ * Get a LaTeX representation of the tensor.
1000
+ * @param {number} [precision=3] - Decimal precision
1001
+ * @returns {string} LaTeX string
1002
+ */
1003
+ toLatex(precision = 3) {
1004
+ const rows = this.#components.map((row) =>
1005
+ row.map((v) => v.toFixed(precision)).join(" & ")
1006
+ );
1007
+ return `\\begin{pmatrix}\n${rows.join(" \\\\\n")}\n\\end{pmatrix}`;
1008
+ }
1009
+ }