matterviz 0.3.7 → 0.4.1

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 (486) hide show
  1. package/dist/Icon.svelte +7 -4
  2. package/dist/MillerIndexInput.svelte +1 -1
  3. package/dist/api/optimade.js +32 -26
  4. package/dist/app.css +0 -3
  5. package/dist/brillouin/BrillouinZone.svelte +76 -148
  6. package/dist/brillouin/BrillouinZone.svelte.d.ts +6 -14
  7. package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
  10. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  11. package/dist/brillouin/BrillouinZoneScene.svelte +97 -205
  12. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +4 -23
  13. package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
  14. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  15. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  16. package/dist/brillouin/compute.d.ts +2 -0
  17. package/dist/brillouin/compute.js +89 -90
  18. package/dist/brillouin/geometry.d.ts +8 -0
  19. package/dist/brillouin/geometry.js +57 -0
  20. package/dist/brillouin/index.d.ts +2 -0
  21. package/dist/brillouin/index.js +2 -0
  22. package/dist/brillouin/types.d.ts +2 -2
  23. package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  25. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +109 -203
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  27. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +180 -470
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  29. package/dist/chempot-diagram/async-compute.svelte.js +3 -1
  30. package/dist/chempot-diagram/chempot-worker.js +2 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +4 -4
  34. package/dist/chempot-diagram/compute.js +20 -20
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.js +8 -7
  45. package/dist/composition/FormulaFilter.svelte +18 -11
  46. package/dist/composition/PieChart.svelte +11 -10
  47. package/dist/composition/chem-sys.d.ts +8 -0
  48. package/dist/composition/chem-sys.js +86 -0
  49. package/dist/composition/format.js +7 -4
  50. package/dist/composition/index.d.ts +1 -0
  51. package/dist/composition/index.js +1 -0
  52. package/dist/composition/parse.d.ts +0 -1
  53. package/dist/composition/parse.js +41 -31
  54. package/dist/controls.d.ts +1 -0
  55. package/dist/controls.js +0 -1
  56. package/dist/convex-hull/ConvexHull.svelte +8 -10
  57. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  58. package/dist/convex-hull/ConvexHull2D.svelte +106 -185
  59. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  60. package/dist/convex-hull/ConvexHull3D.svelte +179 -683
  61. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  62. package/dist/convex-hull/ConvexHull4D.svelte +183 -687
  63. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  64. package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
  65. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  66. package/dist/convex-hull/ConvexHullControls.svelte +88 -7
  67. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  68. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  69. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  70. package/dist/convex-hull/ConvexHullStats.svelte +36 -175
  71. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  72. package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
  73. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  74. package/dist/convex-hull/GasPressureControls.svelte +4 -4
  75. package/dist/convex-hull/TemperatureSlider.svelte +2 -2
  76. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  77. package/dist/convex-hull/barycentric-coords.js +6 -33
  78. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  79. package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
  80. package/dist/convex-hull/demo-temperature.d.ts +1 -1
  81. package/dist/convex-hull/demo-temperature.js +20 -22
  82. package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
  83. package/dist/convex-hull/gas-thermodynamics.js +22 -30
  84. package/dist/convex-hull/helpers.d.ts +42 -7
  85. package/dist/convex-hull/helpers.js +171 -78
  86. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  87. package/dist/convex-hull/hull-state.svelte.js +124 -0
  88. package/dist/convex-hull/index.d.ts +10 -8
  89. package/dist/convex-hull/index.js +7 -2
  90. package/dist/convex-hull/thermodynamics.js +136 -960
  91. package/dist/convex-hull/types.d.ts +13 -5
  92. package/dist/convex-hull/types.js +12 -0
  93. package/dist/coordination/CoordinationBarPlot.svelte +27 -34
  94. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  95. package/dist/element/BohrAtom.svelte +2 -1
  96. package/dist/element/index.d.ts +4 -0
  97. package/dist/element/index.js +18 -0
  98. package/dist/feedback/DragOverlay.svelte +3 -1
  99. package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
  100. package/dist/feedback/StatusMessage.svelte +13 -3
  101. package/dist/fermi-surface/FermiSlice.svelte +13 -5
  102. package/dist/fermi-surface/FermiSurface.svelte +78 -151
  103. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
  104. package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
  105. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  106. package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -221
  107. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  108. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
  109. package/dist/fermi-surface/compute.js +67 -66
  110. package/dist/fermi-surface/export.js +6 -16
  111. package/dist/fermi-surface/index.d.ts +0 -1
  112. package/dist/fermi-surface/index.js +0 -1
  113. package/dist/fermi-surface/parse.d.ts +1 -1
  114. package/dist/fermi-surface/parse.js +71 -79
  115. package/dist/fermi-surface/types.d.ts +3 -2
  116. package/dist/heatmap-matrix/HeatmapMatrix.svelte +69 -52
  117. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
  118. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
  119. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  120. package/dist/heatmap-matrix/index.d.ts +3 -2
  121. package/dist/heatmap-matrix/index.js +1 -1
  122. package/dist/index.d.ts +1 -0
  123. package/dist/index.js +1 -0
  124. package/dist/io/ExportPane.svelte +166 -0
  125. package/dist/io/ExportPane.svelte.d.ts +17 -0
  126. package/dist/io/decompress.js +5 -4
  127. package/dist/io/export.d.ts +9 -5
  128. package/dist/io/export.js +77 -51
  129. package/dist/io/fetch.d.ts +2 -1
  130. package/dist/io/fetch.js +5 -1
  131. package/dist/io/file-drop.d.ts +8 -1
  132. package/dist/io/file-drop.js +48 -36
  133. package/dist/io/index.d.ts +2 -0
  134. package/dist/io/index.js +10 -0
  135. package/dist/io/types.d.ts +13 -0
  136. package/dist/io/url-drop.js +64 -33
  137. package/dist/isosurface/parse.js +52 -51
  138. package/dist/isosurface/slice.js +5 -4
  139. package/dist/isosurface/types.js +1 -1
  140. package/dist/keyboard.d.ts +3 -0
  141. package/dist/keyboard.js +23 -0
  142. package/dist/labels.d.ts +1 -1
  143. package/dist/labels.js +9 -8
  144. package/dist/layout/FullscreenButton.svelte +33 -0
  145. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  146. package/dist/layout/FullscreenToggle.svelte +8 -14
  147. package/dist/layout/PropertyFilter.svelte +3 -2
  148. package/dist/layout/SettingsSection.svelte +1 -1
  149. package/dist/layout/ViewerChrome.svelte +116 -0
  150. package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
  151. package/dist/layout/fullscreen.d.ts +4 -0
  152. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  153. package/dist/layout/fullscreen.svelte.js +37 -0
  154. package/dist/layout/index.d.ts +3 -0
  155. package/dist/layout/index.js +3 -0
  156. package/dist/layout/json-tree/JsonNode.svelte +1 -1
  157. package/dist/layout/json-tree/JsonTree.svelte +2 -2
  158. package/dist/layout/json-tree/utils.js +5 -4
  159. package/dist/marching-cubes.js +8 -13
  160. package/dist/math.d.ts +12 -4
  161. package/dist/math.js +42 -30
  162. package/dist/overlays/DraggablePane.svelte +4 -4
  163. package/dist/overlays/index.d.ts +4 -0
  164. package/dist/periodic-table/PeriodicTable.svelte +27 -15
  165. package/dist/periodic-table/PropertySelect.svelte +1 -0
  166. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
  167. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  168. package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
  169. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  170. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  171. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  172. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
  173. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  174. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
  175. package/dist/phase-diagram/build-diagram.js +2 -2
  176. package/dist/phase-diagram/colors.js +1 -1
  177. package/dist/phase-diagram/parse.d.ts +2 -1
  178. package/dist/phase-diagram/parse.js +6 -5
  179. package/dist/phase-diagram/types.d.ts +1 -1
  180. package/dist/phase-diagram/utils.d.ts +3 -3
  181. package/dist/phase-diagram/utils.js +8 -12
  182. package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +246 -841
  183. package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +8 -16
  184. package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
  185. package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
  186. package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +8 -7
  187. package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
  188. package/dist/plot/bar/data.d.ts +40 -0
  189. package/dist/plot/bar/data.js +154 -0
  190. package/dist/plot/bar/geometry.d.ts +39 -0
  191. package/dist/plot/bar/geometry.js +60 -0
  192. package/dist/plot/bar/index.d.ts +3 -0
  193. package/dist/plot/bar/index.js +3 -0
  194. package/dist/plot/box/BoxPlot.svelte +1292 -0
  195. package/dist/plot/box/BoxPlot.svelte.d.ts +95 -0
  196. package/dist/plot/box/BoxPlotControls.svelte +109 -0
  197. package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
  198. package/dist/plot/box/Violin.svelte +14 -0
  199. package/dist/plot/box/Violin.svelte.d.ts +70 -0
  200. package/dist/plot/box/box-plot.d.ts +56 -0
  201. package/dist/plot/box/box-plot.js +129 -0
  202. package/dist/plot/box/index.d.ts +5 -0
  203. package/dist/plot/box/index.js +5 -0
  204. package/dist/plot/box/kde.d.ts +17 -0
  205. package/dist/plot/box/kde.js +160 -0
  206. package/dist/plot/box/quantile.d.ts +3 -0
  207. package/dist/plot/box/quantile.js +53 -0
  208. package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +1 -1
  209. package/dist/plot/{auto-place.js → core/auto-place.js} +6 -3
  210. package/dist/plot/core/axis-utils.d.ts +46 -0
  211. package/dist/plot/core/axis-utils.js +110 -0
  212. package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
  213. package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
  214. package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +41 -38
  215. package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +7 -6
  216. package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
  217. package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
  218. package/dist/plot/core/components/ControlPane.svelte +46 -0
  219. package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
  220. package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
  221. package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
  222. package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
  223. package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
  224. package/dist/plot/{Line.svelte → core/components/Line.svelte} +33 -15
  225. package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +3 -2
  226. package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +9 -6
  227. package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +5 -3
  228. package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
  229. package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
  230. package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
  231. package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
  232. package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
  233. package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
  234. package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
  235. package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
  236. package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
  237. package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +5 -5
  238. package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +5 -5
  239. package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +8 -8
  240. package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +5 -5
  241. package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
  242. package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
  243. package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
  244. package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
  245. package/dist/plot/core/components/index.d.ts +17 -0
  246. package/dist/plot/core/components/index.js +17 -0
  247. package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
  248. package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +21 -23
  249. package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
  250. package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
  251. package/dist/plot/core/fill-utils.d.ts +34 -0
  252. package/dist/plot/core/fill-utils.js +391 -0
  253. package/dist/plot/core/index.d.ts +10 -0
  254. package/dist/plot/core/index.js +11 -0
  255. package/dist/plot/core/interactions.d.ts +39 -0
  256. package/dist/plot/core/interactions.js +209 -0
  257. package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
  258. package/dist/plot/{layout.js → core/layout.js} +16 -8
  259. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  260. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  261. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  262. package/dist/plot/core/placed-tween.svelte.js +68 -0
  263. package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +11 -11
  264. package/dist/plot/{reference-line.js → core/reference-line.js} +29 -42
  265. package/dist/plot/core/scales.d.ts +40 -0
  266. package/dist/plot/{scales.js → core/scales.js} +94 -93
  267. package/dist/plot/core/svg.d.ts +3 -0
  268. package/dist/plot/core/svg.js +41 -0
  269. package/dist/plot/{types.d.ts → core/types.d.ts} +36 -85
  270. package/dist/plot/{types.js → core/types.js} +1 -1
  271. package/dist/plot/{utils → core/utils}/label-placement.d.ts +3 -3
  272. package/dist/plot/{utils → core/utils}/label-placement.js +3 -3
  273. package/dist/plot/core/utils/series-visibility.d.ts +26 -0
  274. package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
  275. package/dist/plot/core/utils.d.ts +12 -0
  276. package/dist/plot/core/utils.js +27 -0
  277. package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +174 -551
  278. package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
  279. package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
  280. package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
  281. package/dist/plot/histogram/index.d.ts +2 -0
  282. package/dist/plot/histogram/index.js +2 -0
  283. package/dist/plot/index.d.ts +8 -41
  284. package/dist/plot/index.js +10 -39
  285. package/dist/plot/sankey/Sankey.svelte +697 -0
  286. package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
  287. package/dist/plot/sankey/SankeyControls.svelte +98 -0
  288. package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
  289. package/dist/plot/sankey/index.d.ts +4 -0
  290. package/dist/plot/sankey/index.js +3 -0
  291. package/dist/plot/sankey/sankey-types.d.ts +42 -0
  292. package/dist/plot/sankey/sankey-types.js +4 -0
  293. package/dist/plot/sankey/sankey.d.ts +52 -0
  294. package/dist/plot/sankey/sankey.js +189 -0
  295. package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +64 -64
  296. package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +6 -6
  297. package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
  298. package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
  299. package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +297 -1008
  300. package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +10 -18
  301. package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
  302. package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +2 -2
  303. package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
  304. package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
  305. package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
  306. package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
  307. package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +5 -12
  308. package/dist/plot/scatter/index.d.ts +7 -0
  309. package/dist/plot/scatter/index.js +5 -0
  310. package/dist/plot/scatter/scatter-data.d.ts +19 -0
  311. package/dist/plot/scatter/scatter-data.js +212 -0
  312. package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +25 -34
  313. package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +9 -17
  314. package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +14 -14
  315. package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +6 -6
  316. package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +129 -128
  317. package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +6 -15
  318. package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +7 -6
  319. package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +5 -4
  320. package/dist/plot/scatter-3d/index.d.ts +4 -0
  321. package/dist/plot/scatter-3d/index.js +4 -0
  322. package/dist/plot/sunburst/Sunburst.svelte +1041 -0
  323. package/dist/plot/sunburst/Sunburst.svelte.d.ts +97 -0
  324. package/dist/plot/sunburst/SunburstControls.svelte +200 -0
  325. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
  326. package/dist/plot/sunburst/index.d.ts +4 -0
  327. package/dist/plot/sunburst/index.js +4 -0
  328. package/dist/plot/sunburst/render.d.ts +34 -0
  329. package/dist/plot/sunburst/render.js +122 -0
  330. package/dist/plot/sunburst/sunburst.d.ts +62 -0
  331. package/dist/plot/sunburst/sunburst.js +269 -0
  332. package/dist/rdf/RdfPlot.svelte +2 -1
  333. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  334. package/dist/rdf/calc-rdf.js +11 -24
  335. package/dist/sanitize.js +14 -3
  336. package/dist/scene/SceneCamera.svelte +62 -0
  337. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  338. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  339. package/dist/scene/bind-renderer.svelte.js +14 -0
  340. package/dist/scene/index.d.ts +4 -0
  341. package/dist/scene/index.js +5 -0
  342. package/dist/scene/props.js +52 -0
  343. package/dist/scene/types.d.ts +26 -0
  344. package/dist/scene/types.js +1 -0
  345. package/dist/settings.d.ts +79 -3
  346. package/dist/settings.js +321 -1
  347. package/dist/spectral/Bands.svelte +47 -36
  348. package/dist/spectral/Bands.svelte.d.ts +6 -6
  349. package/dist/spectral/BandsAndDos.svelte +23 -25
  350. package/dist/spectral/BrillouinBandsDos.svelte +42 -30
  351. package/dist/spectral/Dos.svelte +15 -23
  352. package/dist/spectral/Dos.svelte.d.ts +4 -3
  353. package/dist/spectral/helpers.d.ts +8 -6
  354. package/dist/spectral/helpers.js +137 -65
  355. package/dist/state.svelte.d.ts +0 -7
  356. package/dist/state.svelte.js +0 -6
  357. package/dist/structure/Arrow.svelte +2 -4
  358. package/dist/structure/AtomLegend.svelte +8 -9
  359. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  360. package/dist/structure/CanvasTooltip.svelte +1 -0
  361. package/dist/structure/CellSelect.svelte +12 -5
  362. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  363. package/dist/structure/Cylinder.svelte +12 -8
  364. package/dist/structure/Cylinder.svelte.d.ts +4 -1
  365. package/dist/structure/Lattice.svelte +2 -2
  366. package/dist/structure/Structure.svelte +365 -423
  367. package/dist/structure/Structure.svelte.d.ts +5 -15
  368. package/dist/structure/StructureControls.svelte +217 -2
  369. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  370. package/dist/structure/StructureExportPane.svelte +54 -156
  371. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  372. package/dist/structure/StructureInfoPane.svelte +10 -9
  373. package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
  374. package/dist/structure/StructureScene.svelte +376 -208
  375. package/dist/structure/StructureScene.svelte.d.ts +22 -20
  376. package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
  377. package/dist/structure/{label-placement.js → atom-label-placement.js} +15 -5
  378. package/dist/structure/atom-properties.d.ts +1 -1
  379. package/dist/structure/atom-properties.js +17 -22
  380. package/dist/structure/bond-order-perception.js +3 -5
  381. package/dist/structure/bonding.d.ts +4 -0
  382. package/dist/structure/bonding.js +134 -63
  383. package/dist/structure/export.d.ts +24 -4
  384. package/dist/structure/export.js +89 -143
  385. package/dist/structure/index.d.ts +4 -4
  386. package/dist/structure/index.js +3 -3
  387. package/dist/structure/measure.d.ts +3 -2
  388. package/dist/structure/measure.js +6 -5
  389. package/dist/structure/parse.d.ts +3 -2
  390. package/dist/structure/parse.js +419 -438
  391. package/dist/structure/partial-occupancy.d.ts +0 -1
  392. package/dist/structure/partial-occupancy.js +1 -1
  393. package/dist/structure/pbc.d.ts +1 -1
  394. package/dist/structure/pbc.js +190 -13
  395. package/dist/structure/polyhedra.d.ts +41 -0
  396. package/dist/structure/polyhedra.js +602 -0
  397. package/dist/structure/site.d.ts +4 -0
  398. package/dist/structure/site.js +1 -0
  399. package/dist/structure/supercell.js +3 -2
  400. package/dist/structure/validation.js +5 -6
  401. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  402. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  403. package/dist/symmetry/SymmetryElements.svelte +354 -0
  404. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  405. package/dist/symmetry/SymmetryStats.svelte +113 -8
  406. package/dist/symmetry/WyckoffTable.svelte +68 -7
  407. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  408. package/dist/symmetry/cell-transform.js +7 -14
  409. package/dist/symmetry/index.d.ts +14 -4
  410. package/dist/symmetry/index.js +291 -72
  411. package/dist/symmetry/spacegroups.d.ts +12 -1
  412. package/dist/symmetry/spacegroups.js +63 -14
  413. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  414. package/dist/symmetry/symmetry-elements.js +521 -0
  415. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  416. package/dist/symmetry/wyckoff-db.js +87 -0
  417. package/dist/table/HeatmapTable.svelte +66 -25
  418. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  419. package/dist/table/index.d.ts +1 -3
  420. package/dist/table/index.js +1 -1
  421. package/dist/theme/index.js +8 -8
  422. package/dist/tooltip/KCoords.svelte +45 -0
  423. package/dist/tooltip/KCoords.svelte.d.ts +8 -0
  424. package/dist/tooltip/index.d.ts +1 -0
  425. package/dist/tooltip/index.js +1 -0
  426. package/dist/trajectory/Trajectory.svelte +123 -100
  427. package/dist/trajectory/Trajectory.svelte.d.ts +11 -22
  428. package/dist/trajectory/TrajectoryExportPane.svelte +17 -25
  429. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  430. package/dist/trajectory/TrajectoryInfoPane.svelte +5 -3
  431. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  432. package/dist/trajectory/constants.js +6 -2
  433. package/dist/trajectory/extract.js +17 -37
  434. package/dist/trajectory/format-detect.d.ts +1 -1
  435. package/dist/trajectory/format-detect.js +27 -19
  436. package/dist/trajectory/frame-reader.d.ts +0 -1
  437. package/dist/trajectory/frame-reader.js +63 -162
  438. package/dist/trajectory/helpers.d.ts +10 -2
  439. package/dist/trajectory/helpers.js +56 -36
  440. package/dist/trajectory/index.js +1 -1
  441. package/dist/trajectory/parse/ase.d.ts +9 -1
  442. package/dist/trajectory/parse/ase.js +47 -32
  443. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  444. package/dist/trajectory/parse/diagnostics.js +14 -0
  445. package/dist/trajectory/parse/hdf5.js +1 -1
  446. package/dist/trajectory/parse/index.d.ts +1 -1
  447. package/dist/trajectory/parse/index.js +65 -105
  448. package/dist/trajectory/parse/lammps.d.ts +0 -2
  449. package/dist/trajectory/parse/lammps.js +8 -6
  450. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  451. package/dist/trajectory/parse/pymatgen.js +74 -0
  452. package/dist/trajectory/parse/vasp.js +38 -18
  453. package/dist/trajectory/parse/xyz.d.ts +13 -1
  454. package/dist/trajectory/parse/xyz.js +102 -94
  455. package/dist/trajectory/plotting.d.ts +1 -2
  456. package/dist/trajectory/plotting.js +16 -113
  457. package/dist/utils.d.ts +2 -0
  458. package/dist/utils.js +7 -5
  459. package/dist/xrd/XrdPlot.svelte +16 -30
  460. package/dist/xrd/broadening.d.ts +2 -1
  461. package/dist/xrd/calc-xrd.js +18 -20
  462. package/dist/xrd/index.d.ts +2 -2
  463. package/dist/xrd/parse.js +2 -2
  464. package/package.json +43 -26
  465. package/dist/element/data.json +0 -11864
  466. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  467. package/dist/fermi-surface/marching-cubes.js +0 -2
  468. package/dist/plot/PlotControls.svelte.d.ts +0 -4
  469. package/dist/plot/axis-utils.d.ts +0 -19
  470. package/dist/plot/axis-utils.js +0 -78
  471. package/dist/plot/defaults.d.ts +0 -19
  472. package/dist/plot/defaults.js +0 -9
  473. package/dist/plot/fill-utils.d.ts +0 -46
  474. package/dist/plot/fill-utils.js +0 -322
  475. package/dist/plot/hover-lock.svelte.d.ts +0 -14
  476. package/dist/plot/hover-lock.svelte.js +0 -46
  477. package/dist/plot/interactions.d.ts +0 -12
  478. package/dist/plot/interactions.js +0 -101
  479. package/dist/plot/scales.d.ts +0 -48
  480. package/dist/plot/svg.d.ts +0 -1
  481. package/dist/plot/svg.js +0 -11
  482. package/dist/plot/utils/series-visibility.d.ts +0 -15
  483. package/dist/plot/utils.d.ts +0 -1
  484. package/dist/plot/utils.js +0 -14
  485. /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
  486. /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
@@ -1,11 +1,9 @@
1
1
  import { count_atoms_in_composition, extract_formula_elements, sort_by_electronegativity, } from '../composition';
2
2
  import * as math from '../math';
3
- import { barycentric_to_ternary_xyz, barycentric_to_tetrahedral, composition_to_barycentric_3d, composition_to_barycentric_4d, composition_to_barycentric_nd, } from './barycentric-coords';
3
+ import { composition_to_barycentric_nd } from './barycentric-coords';
4
4
  import { get_arity, HULL_STABILITY_TOL, is_on_hull, is_unary_entry } from './helpers';
5
5
  // Track warned keys to avoid log spam on large datasets with repeated invalid keys
6
6
  const warned_keys = new Set();
7
- const cross_point_2d = (origin, point_a, point_b) => (point_a.x - origin.x) * (point_b.y - origin.y) -
8
- (point_a.y - origin.y) * (point_b.x - origin.x);
9
7
  // Normalize convex hull composition keys by stripping oxidation states (e.g. "V4+" -> "V")
10
8
  // and merging amounts for keys that map to the same element. Filters non-positive amounts.
11
9
  // Only extracts FIRST valid element from each key (e.g. "Fe2O3" -> "Fe", not both Fe and O).
@@ -101,6 +99,8 @@ export function find_lowest_energy_unary_refs(entries) {
101
99
  }
102
100
  return refs;
103
101
  }
102
+ // Result key for an entry: entry_id, falling back to its serialized composition
103
+ const id_of = (entry) => entry.entry_id ?? JSON.stringify(entry.composition);
104
104
  export function calculate_e_above_hull(input, reference_entries) {
105
105
  const is_single = !Array.isArray(input);
106
106
  const entries_of_interest = is_single ? [input] : input;
@@ -135,7 +135,7 @@ export function calculate_e_above_hull(input, reference_entries) {
135
135
  if (arity === 1) {
136
136
  // Unary system
137
137
  for (const { entry, e_form } of interest_data) {
138
- const id = entry.entry_id ?? JSON.stringify(entry.composition);
138
+ const id = id_of(entry);
139
139
  // For unary, e_above_hull is simply e_form (since stable state is 0)
140
140
  // Unless we have multiple polymorphs, in which case the hull is at min(e_form) which should be 0
141
141
  // But compute_e_form_per_atom already subtracts the stable unary reference energy.
@@ -149,6 +149,8 @@ export function calculate_e_above_hull(input, reference_entries) {
149
149
  // Build hull points from references
150
150
  const hull_input_map = new Map(); // x -> min_e_form
151
151
  for (const ref of reference_entries) {
152
+ if (ref.exclude_from_hull)
153
+ continue; // Shown but not used in hull construction
152
154
  const e_form = compute_e_form(ref);
153
155
  if (typeof e_form !== `number`)
154
156
  continue;
@@ -169,7 +171,7 @@ export function calculate_e_above_hull(input, reference_entries) {
169
171
  const hull_points = Array.from(hull_input_map, ([x, y]) => ({ x, y }));
170
172
  const lower_hull = compute_lower_hull_2d(hull_points);
171
173
  for (const { entry, e_form } of interest_data) {
172
- const id = entry.entry_id ?? JSON.stringify(entry.composition);
174
+ const id = id_of(entry);
173
175
  if (typeof e_form !== `number`) {
174
176
  results[id] = NaN;
175
177
  continue;
@@ -185,113 +187,15 @@ export function calculate_e_above_hull(input, reference_entries) {
185
187
  results[id] = y_hull === null ? NaN : Math.max(0, e_form - y_hull);
186
188
  }
187
189
  }
188
- else if (arity === 3) {
189
- // Ternary system
190
- const ref_points = [];
191
- for (const ref of reference_entries) {
192
- const e_form = compute_e_form(ref);
193
- if (typeof e_form !== `number`)
194
- continue;
195
- try {
196
- const bary = composition_to_barycentric_3d(ref.composition, elements);
197
- const point = barycentric_to_ternary_xyz(bary, e_form);
198
- ref_points.push(point);
199
- }
200
- catch {
201
- // Ignore invalid compositions
202
- }
203
- }
204
- // Ensure corner points (pure elements default to e_form = 0)
205
- for (const el of elements) {
206
- const corner = barycentric_to_ternary_xyz(composition_to_barycentric_3d({ [el]: 1 }, elements), 0);
207
- if (!ref_points.some((point) => Math.hypot(point.x - corner.x, point.y - corner.y, point.z - corner.z) < 1e-9)) {
208
- ref_points.push(corner);
209
- }
210
- }
211
- const hull_triangles = compute_lower_hull_triangles(ref_points);
212
- const hull_models = build_lower_hull_model(hull_triangles);
213
- for (const { entry, e_form } of interest_data) {
214
- const id = entry.entry_id ?? JSON.stringify(entry.composition);
215
- if (typeof e_form !== `number`) {
216
- results[id] = NaN;
217
- continue;
218
- }
219
- try {
220
- const bary = composition_to_barycentric_3d(entry.composition, elements);
221
- const point = barycentric_to_ternary_xyz(bary, e_form);
222
- const z_hull = e_hull_at_xy(hull_models, point.x, point.y);
223
- results[id] = z_hull === null ? NaN : Math.max(0, point.z - z_hull);
224
- }
225
- catch {
226
- results[id] = NaN;
227
- }
228
- }
229
- }
230
- else if (arity === 4) {
231
- // Quaternary system
232
- const ref_points = [];
233
- for (const ref of reference_entries) {
234
- const e_form = compute_e_form(ref);
235
- if (typeof e_form !== `number`)
236
- continue;
237
- try {
238
- const bary = composition_to_barycentric_4d(ref.composition, elements);
239
- const tet = barycentric_to_tetrahedral(bary);
240
- ref_points.push({ ...tet, w: e_form });
241
- }
242
- catch {
243
- // Ignore invalid
244
- }
245
- }
246
- // Ensure corner points (pure elements default to e_form = 0)
247
- for (const el of elements) {
248
- const tet = barycentric_to_tetrahedral(composition_to_barycentric_4d({ [el]: 1 }, elements));
249
- const corner = { ...tet, w: 0 };
250
- const dist = (point) => Math.hypot(point.x - corner.x, point.y - corner.y, point.z - corner.z, point.w);
251
- if (!ref_points.some((point) => dist(point) < 1e-9))
252
- ref_points.push(corner);
253
- }
254
- const hull_tetrahedra = compute_lower_hull_4d(ref_points);
255
- const interest_points = [];
256
- const interest_indices = [];
257
- interest_data.forEach(({ entry, e_form }, idx) => {
258
- if (typeof e_form === `number`) {
259
- try {
260
- const bary = composition_to_barycentric_4d(entry.composition, elements);
261
- const tet = barycentric_to_tetrahedral(bary);
262
- interest_points.push({ ...tet, w: e_form });
263
- interest_indices.push(idx);
264
- }
265
- catch {
266
- // Skip
267
- }
268
- }
269
- });
270
- const distances = compute_e_above_hull_4d(interest_points, hull_tetrahedra);
271
- // Build reverse lookup for O(1) access
272
- const idx_to_point_idx = new Map();
273
- interest_indices.forEach((original_idx, point_idx) => {
274
- idx_to_point_idx.set(original_idx, point_idx);
275
- });
276
- // Map back
277
- for (let idx = 0; idx < interest_data.length; idx++) {
278
- const { entry } = interest_data[idx];
279
- const id = entry.entry_id ?? JSON.stringify(entry.composition);
280
- const point_idx = idx_to_point_idx.get(idx) ?? -1;
281
- if (point_idx !== -1) {
282
- results[id] = Math.max(0, distances[point_idx]);
283
- }
284
- else {
285
- results[id] = NaN;
286
- }
287
- }
288
- }
289
190
  else {
290
- // Arity 5+ uses generalized N-dimensional convex hull
291
- // Helper to convert entry to hull point, returns null on expected errors
191
+ // Arity 3+ uses the generalized N-dimensional convex hull in reduced barycentric coords
192
+ // Helper to convert entry to hull point, returns null on expected errors.
193
+ // Barycentric coords sum to 1, so the first is dropped: keeping all N would confine
194
+ // points to an (N-1)-dim affine subspace, leaving the hull permanently degenerate.
292
195
  const to_hull_point = (entry, e_form) => {
293
196
  try {
294
- return [...composition_to_barycentric_nd(entry.composition, elements), e_form];
197
+ const bary = composition_to_barycentric_nd(entry.composition, elements);
198
+ return [...bary.slice(1), e_form];
295
199
  }
296
200
  catch (err) {
297
201
  // Skip expected errors (missing elements), warn on unexpected
@@ -304,6 +208,8 @@ export function calculate_e_above_hull(input, reference_entries) {
304
208
  // Build reference points
305
209
  const ref_points = [];
306
210
  for (const ref of reference_entries) {
211
+ if (ref.exclude_from_hull)
212
+ continue; // Shown but not used in hull construction
307
213
  const e_form = compute_e_form(ref);
308
214
  if (typeof e_form !== `number`)
309
215
  continue;
@@ -311,20 +217,21 @@ export function calculate_e_above_hull(input, reference_entries) {
311
217
  if (point)
312
218
  ref_points.push(point);
313
219
  }
314
- // Ensure corner points (pure elements default to e_form = 0)
220
+ // Ensure corner points (pure elements default to e_form = 0). In reduced
221
+ // coordinates, element 0 is the origin; element k > 0 has (k-1)th coord = 1.
315
222
  for (let el_idx = 0; el_idx < arity; el_idx++) {
316
- const corner = Array(arity + 1).fill(0);
317
- corner[el_idx] = 1; // ith barycentric coord = 1
223
+ const corner = Array(arity).fill(0);
224
+ if (el_idx > 0)
225
+ corner[el_idx - 1] = 1;
318
226
  if (!ref_points.some((pt) => norm_nd(subtract_nd(pt, corner)) < EPS)) {
319
227
  ref_points.push(corner);
320
228
  }
321
229
  }
322
230
  const hull_facets = compute_lower_hull_nd(compute_quickhull_nd(ref_points));
323
- // Warn if hull is degenerate (all points coplanar or insufficient spread)
231
+ // Degenerate hull (all refs co-hyperplanar, e.g. all e_form = 0): tie-plane fallback is exact
324
232
  if (hull_facets.length === 0 && ref_points.length >= arity + 1) {
325
- console.warn(`N-dimensional hull for ${arity}-element system is degenerate. ` +
326
- `Falling back to tie-hyperplane at energy 0. ` +
327
- `Consider using pymatgen for complex high-dimensional phase diagrams.`);
233
+ console.warn(`N-dimensional hull for ${arity}-element system is degenerate ` +
234
+ `(all reference points co-hyperplanar). Falling back to tie-hyperplane at energy 0.`);
328
235
  }
329
236
  // Build query points with mapping back to original indices
330
237
  const interest_points = [];
@@ -343,27 +250,20 @@ export function calculate_e_above_hull(input, reference_entries) {
343
250
  const distances = hull_facets.length > 0
344
251
  ? compute_e_above_hull_nd(interest_points, hull_facets, ref_points)
345
252
  : [];
346
- // Map results back to entries
253
+ // Map results back to entries (degenerate hull → tie-hyperplane at energy 0)
347
254
  for (let idx = 0; idx < interest_data.length; idx++) {
348
255
  const { entry, e_form } = interest_data[idx];
349
- const id = entry.entry_id ?? JSON.stringify(entry.composition);
256
+ const id = id_of(entry);
350
257
  const point_idx = idx_to_point_idx.get(idx);
351
- if (point_idx === undefined) {
258
+ const on_tie_plane = hull_facets.length === 0 && typeof e_form === `number`;
259
+ if (point_idx === undefined)
352
260
  results[id] = NaN;
353
- }
354
- else if (hull_facets.length === 0 && typeof e_form === `number`) {
355
- // Degenerate case: hull is tie-hyperplane at energy 0
356
- results[id] = Math.max(0, e_form);
357
- }
358
- else {
359
- results[id] = Math.max(0, distances[point_idx]);
360
- }
261
+ else
262
+ results[id] = Math.max(0, on_tie_plane ? e_form : distances[point_idx]);
361
263
  }
362
264
  }
363
- if (is_single) {
364
- const id = entries_of_interest[0].entry_id ?? JSON.stringify(entries_of_interest[0].composition);
365
- return results[id];
366
- }
265
+ if (is_single)
266
+ return results[id_of(entries_of_interest[0])];
367
267
  return results;
368
268
  }
369
269
  export function get_convex_hull_stats(processed_entries, elements, max_arity = 4) {
@@ -437,25 +337,23 @@ export function get_convex_hull_stats(processed_entries, elements, max_arity = 4
437
337
  }
438
338
  // Convert a PhaseData entry to a ConvexHullEntry with default visual fields.
439
339
  // x/y/z default to 0 since high-dim systems aren't visually plotted.
440
- function to_hull_entry(entry) {
441
- return {
442
- ...entry,
443
- is_element: get_arity(entry) === 1,
444
- x: 0,
445
- y: 0,
446
- z: 0,
447
- };
448
- }
340
+ const to_hull_entry = (entry) => ({
341
+ ...entry,
342
+ is_element: get_arity(entry) === 1,
343
+ x: 0,
344
+ y: 0,
345
+ z: 0,
346
+ });
449
347
  // Process raw hull entries for high-dimensional systems (5+ elements) where the
450
348
  // ConvexHull visual component can't render. Computes formation energies, hull distances,
451
349
  // stable/unstable classification, and phase stats. Returns null on failure.
452
350
  // Optionally accepts `elements` to scope the chemical system; if omitted, elements
453
351
  // are derived from the entries' compositions.
454
352
  export function process_hull_for_stats(entries, elements) {
455
- if (!entries.length)
353
+ if (entries.length === 0)
456
354
  return null;
457
355
  const processed = process_hull_entries(entries);
458
- if (!processed.entries.length)
356
+ if (processed.entries.length === 0)
459
357
  return null;
460
358
  const hull_elements = elements ?? processed.elements;
461
359
  // Compute formation energies
@@ -473,7 +371,7 @@ export function process_hull_for_stats(entries, elements) {
473
371
  try {
474
372
  const hull_distances = calculate_e_above_hull(processed.entries, processed.entries);
475
373
  for (const entry of processed.entries) {
476
- const dist = hull_distances[entry.entry_id ?? JSON.stringify(entry.composition)];
374
+ const dist = hull_distances[id_of(entry)];
477
375
  if (typeof dist === `number` && Number.isFinite(dist)) {
478
376
  entry.e_above_hull = dist;
479
377
  entry.is_stable = dist < HULL_STABILITY_TOL;
@@ -498,17 +396,11 @@ export function process_hull_for_stats(entries, elements) {
498
396
  }
499
397
  // --- 2D Convex Hull (Binary Phase Diagrams) ---
500
398
  export function compute_lower_hull_2d(points) {
501
- // Andrew's monotone chain for lower hull
502
- // Sort by x then y
503
- const sorted = [...points].sort((p1, p2) => p1.x - p2.x || p1.y - p2.y);
504
- const lower = [];
505
- for (const point of sorted) {
506
- while (lower.length >= 2 &&
507
- cross_point_2d(lower[lower.length - 2], lower[lower.length - 1], point) <= 0)
508
- lower.pop();
509
- lower.push(point);
510
- }
511
- return lower;
399
+ // Andrew's monotone chain lower hull (Point2D adapter over math.monotone_chain)
400
+ const sorted = points
401
+ .map((pt) => [pt.x, pt.y])
402
+ .toSorted((a, b) => a[0] - b[0] || a[1] - b[1]);
403
+ return math.monotone_chain(sorted).map(([x, y]) => ({ x, y }));
512
404
  }
513
405
  export function interpolate_hull_2d(hull, x) {
514
406
  if (hull.length < 2)
@@ -522,255 +414,31 @@ export function interpolate_hull_2d(hull, x) {
522
414
  const p1 = hull[idx];
523
415
  const p2 = hull[idx + 1];
524
416
  if (x >= p1.x && x <= p2.x) {
525
- const t = (x - p1.x) / Math.max(1e-12, p2.x - p1.x);
526
- return p1.y * (1 - t) + p2.y * t;
417
+ const frac = (x - p1.x) / Math.max(1e-12, p2.x - p1.x);
418
+ return p1.y * (1 - frac) + p2.y * frac;
527
419
  }
528
420
  }
529
421
  return null;
530
422
  }
531
423
  // --- Convex hull geometry ---
532
424
  const EPS = 1e-9;
533
- const subtract = (pt1, pt2) => ({
534
- x: pt1.x - pt2.x,
535
- y: pt1.y - pt2.y,
536
- z: pt1.z - pt2.z,
537
- });
538
- const cross = (vec1, vec2) => ({
539
- x: vec1.y * vec2.z - vec1.z * vec2.y,
540
- y: vec1.z * vec2.x - vec1.x * vec2.z,
541
- z: vec1.x * vec2.y - vec1.y * vec2.x,
542
- });
543
- const norm = (point) => Math.sqrt(point.x * point.x + point.y * point.y + point.z * point.z);
544
- function normalize(point) {
545
- const length = norm(point);
546
- if (length < EPS)
547
- return { x: 0, y: 0, z: 0 };
548
- return { x: point.x / length, y: point.y / length, z: point.z / length };
549
- }
550
- function compute_plane(p1, p2, p3) {
551
- const edge_12 = subtract(p2, p1);
552
- const edge_13 = subtract(p3, p1);
553
- const normal = normalize(cross(edge_12, edge_13));
554
- const offset = -(normal.x * p1.x + normal.y * p1.y + normal.z * p1.z);
555
- return { normal, offset };
556
- }
557
- const point_plane_signed_distance = (plane, point) => plane.normal.x * point.x + plane.normal.y * point.y + plane.normal.z * point.z + plane.offset;
558
- const compute_centroid = (p1, p2, p3) => ({
559
- x: (p1.x + p2.x + p3.x) / 3,
560
- y: (p1.y + p2.y + p3.y) / 3,
561
- z: (p1.z + p2.z + p3.z) / 3,
425
+ // Point conversions between object-shaped public API points and ND number[] points
426
+ const p3_to_nd = (pt) => [pt.x, pt.y, pt.z];
427
+ const p4_to_nd = (pt) => [pt.x, pt.y, pt.z, pt.w];
428
+ // Map an ND facet (vertex indices into `points`) back to the public triangle shape
429
+ const facet_to_triangle = (facet, points) => ({
430
+ vertices: facet.vertex_indices.map((idx) => points[idx]),
431
+ normal: { x: facet.plane.normal[0], y: facet.plane.normal[1], z: facet.plane.normal[2] },
432
+ centroid: { x: facet.centroid[0], y: facet.centroid[1], z: facet.centroid[2] },
562
433
  });
563
- function distance_point_to_line(line_start, line_end, point) {
564
- const line_vec = subtract(line_end, line_start);
565
- const to_point = subtract(point, line_start);
566
- const cross_prod = cross(line_vec, to_point);
567
- const line_len = norm(line_vec);
568
- if (line_len < EPS)
569
- return 0;
570
- return norm(cross_prod) / line_len;
571
- }
572
- function choose_initial_tetrahedron(points) {
573
- if (points.length < 4)
574
- return null;
575
- let idx_min_x = 0;
576
- let idx_max_x = 0;
577
- for (let idx = 1; idx < points.length; idx++) {
578
- if (points[idx].x < points[idx_min_x].x)
579
- idx_min_x = idx;
580
- if (points[idx].x > points[idx_max_x].x)
581
- idx_max_x = idx;
582
- }
583
- if (idx_min_x === idx_max_x)
584
- return null;
585
- let idx_far_line = -1;
586
- let best_dist_line = -1;
587
- for (let idx = 0; idx < points.length; idx++) {
588
- if (idx === idx_min_x || idx === idx_max_x)
589
- continue;
590
- const dist = distance_point_to_line(points[idx_min_x], points[idx_max_x], points[idx]);
591
- if (dist > best_dist_line) {
592
- best_dist_line = dist;
593
- idx_far_line = idx;
594
- }
595
- }
596
- if (idx_far_line === -1 || best_dist_line < EPS)
597
- return null;
598
- const plane0 = compute_plane(points[idx_min_x], points[idx_max_x], points[idx_far_line]);
599
- let idx_far_plane = -1;
600
- let best_dist_plane = -1;
601
- for (let idx = 0; idx < points.length; idx++) {
602
- if (idx === idx_min_x || idx === idx_max_x || idx === idx_far_line)
603
- continue;
604
- const dist = Math.abs(point_plane_signed_distance(plane0, points[idx]));
605
- if (dist > best_dist_plane) {
606
- best_dist_plane = dist;
607
- idx_far_plane = idx;
608
- }
609
- }
610
- if (idx_far_plane === -1 || best_dist_plane < EPS)
611
- return null;
612
- return [idx_min_x, idx_max_x, idx_far_line, idx_far_plane];
613
- }
614
- function make_face(points, a, b, c, interior_point) {
615
- let plane = compute_plane(points[a], points[b], points[c]);
616
- let centroid = compute_centroid(points[a], points[b], points[c]);
617
- const dist_interior = point_plane_signed_distance(plane, interior_point);
618
- if (dist_interior > 0) {
619
- plane = compute_plane(points[a], points[c], points[b]);
620
- centroid = compute_centroid(points[a], points[c], points[b]);
621
- return { vertices: [a, c, b], plane, centroid, outside_points: new Set() };
622
- }
623
- return { vertices: [a, b, c], plane, centroid, outside_points: new Set() };
624
- }
625
- function assign_outside_points(face, points, candidate_indices) {
626
- face.outside_points.clear();
627
- for (const idx of candidate_indices) {
628
- const distance = point_plane_signed_distance(face.plane, points[idx]);
629
- if (distance > EPS)
630
- face.outside_points.add(idx);
631
- }
632
- }
633
- function collect_candidate_points(faces) {
634
- const set = new Set();
635
- for (const face of faces)
636
- for (const idx of face.outside_points)
637
- set.add(idx);
638
- return Array.from(set);
639
- }
640
- function farthest_point_for_face(points, face) {
641
- let best_idx = -1;
642
- let best_distance = -1;
643
- for (const idx of face.outside_points) {
644
- const distance = point_plane_signed_distance(face.plane, points[idx]);
645
- if (distance > best_distance) {
646
- best_distance = distance;
647
- best_idx = idx;
648
- }
649
- }
650
- if (best_idx === -1)
651
- return null;
652
- return { idx: best_idx, distance: best_distance };
653
- }
654
- function build_horizon(faces, visible_face_indices) {
655
- const edge_count = new Map();
656
- for (const face_idx of visible_face_indices) {
657
- const face = faces[face_idx];
658
- const [a, b, c] = face.vertices;
659
- const edges = [
660
- [a, b],
661
- [b, c],
662
- [c, a],
663
- ];
664
- for (const [u, v] of edges) {
665
- const key = u < v ? `${u}|${v}` : `${v}|${u}`;
666
- if (!edge_count.has(key))
667
- edge_count.set(key, [u, v]);
668
- else
669
- edge_count.set(key, [Number.NaN, Number.NaN]);
670
- }
671
- }
672
- const horizon = [];
673
- for (const uv of edge_count.values()) {
674
- if (Number.isNaN(uv[0]))
675
- continue;
676
- horizon.push(uv);
677
- }
678
- return horizon;
679
- }
434
+ // 3D quickhull (thin adapter over the N-dimensional implementation)
680
435
  export function compute_quickhull_triangles(points) {
681
- if (points.length < 4)
682
- return []; // hull needs at least 4 non-coplanar points, bail if not provided
683
- const initial = choose_initial_tetrahedron(points);
684
- if (!initial)
685
- return [];
686
- const [i0, i1, i2, i3] = initial;
687
- const interior_point = {
688
- x: (points[i0].x + points[i1].x + points[i2].x + points[i3].x) / 4,
689
- y: (points[i0].y + points[i1].y + points[i2].y + points[i3].y) / 4,
690
- z: (points[i0].z + points[i1].z + points[i2].z + points[i3].z) / 4,
691
- };
692
- const faces = [
693
- make_face(points, i0, i1, i2, interior_point),
694
- make_face(points, i0, i2, i3, interior_point),
695
- make_face(points, i0, i3, i1, interior_point),
696
- make_face(points, i1, i3, i2, interior_point),
697
- ];
698
- const all_indices = [];
699
- for (let idx = 0; idx < points.length; idx++) {
700
- if (idx === i0 || idx === i1 || idx === i2 || idx === i3)
701
- continue;
702
- all_indices.push(idx);
703
- }
704
- for (const face of faces)
705
- assign_outside_points(face, points, all_indices);
706
- while (true) {
707
- let chosen_face_idx = -1;
708
- let chosen_point_idx = -1;
709
- let max_distance = -1;
710
- for (let face_idx = 0; face_idx < faces.length; face_idx++) {
711
- const face = faces[face_idx];
712
- if (face.outside_points.size === 0)
713
- continue;
714
- const far = farthest_point_for_face(points, face);
715
- if (far && far.distance > max_distance) {
716
- max_distance = far.distance;
717
- chosen_face_idx = face_idx;
718
- chosen_point_idx = far.idx;
719
- }
720
- }
721
- if (chosen_face_idx === -1)
722
- break;
723
- const eye_idx = chosen_point_idx;
724
- const visible_face_indices = new Set();
725
- for (let face_idx = 0; face_idx < faces.length; face_idx++) {
726
- const face = faces[face_idx];
727
- const dist = point_plane_signed_distance(face.plane, points[eye_idx]);
728
- if (dist > EPS)
729
- visible_face_indices.add(face_idx);
730
- }
731
- const horizon_edges = build_horizon(faces, visible_face_indices);
732
- const visible_faces = Array.from(visible_face_indices).sort((a, b) => b - a);
733
- const candidate_points = collect_candidate_points(visible_faces.map((idx) => faces[idx]));
734
- for (const idx of visible_faces)
735
- faces.splice(idx, 1);
736
- const new_faces = [];
737
- for (const [u, v] of horizon_edges) {
738
- const new_face = make_face(points, u, v, eye_idx, interior_point);
739
- new_faces.push(new_face);
740
- }
741
- for (const face of new_faces)
742
- face.outside_points.clear();
743
- for (const idx of candidate_points) {
744
- if (idx === eye_idx)
745
- continue;
746
- let best_face = null;
747
- let best_distance = EPS;
748
- for (const face of new_faces) {
749
- const dist = point_plane_signed_distance(face.plane, points[idx]);
750
- if (dist > best_distance) {
751
- best_distance = dist;
752
- best_face = face;
753
- }
754
- }
755
- if (best_face)
756
- best_face.outside_points.add(idx);
757
- }
758
- faces.push(...new_faces);
759
- }
760
- return faces.map((face) => {
761
- const [a, b, c] = face.vertices;
762
- const normal = face.plane.normal;
763
- const centroid = face.centroid;
764
- return {
765
- vertices: [points[a], points[b], points[c]],
766
- normal,
767
- centroid,
768
- };
769
- });
436
+ const facets = compute_quickhull_nd(points.map(p3_to_nd));
437
+ return facets.map((facet) => facet_to_triangle(facet, points));
770
438
  }
771
439
  export function compute_lower_hull_triangles(points) {
772
- const all_faces = compute_quickhull_triangles(points);
773
- return all_faces.filter((face) => face.normal.z < 0 - EPS);
440
+ // Lower hull faces point "down" in the z (energy) direction
441
+ return compute_quickhull_triangles(points).filter((face) => face.normal.z < 0 - EPS);
774
442
  }
775
443
  export const build_lower_hull_model = (faces) => faces.map((tri) => {
776
444
  const [p1, p2, p3] = tri.vertices;
@@ -840,496 +508,37 @@ export const compute_e_above_hull_for_points = (points, models) => points.map((p
840
508
  return 0;
841
509
  return Math.max(0, point.z - z_hull);
842
510
  });
843
- const subtract_4d = (pt1, pt2) => ({
844
- x: pt1.x - pt2.x,
845
- y: pt1.y - pt2.y,
846
- z: pt1.z - pt2.z,
847
- w: pt1.w - pt2.w,
848
- });
849
- const dot_4d = (vec_a, vec_b) => vec_a.x * vec_b.x + vec_a.y * vec_b.y + vec_a.z * vec_b.z + vec_a.w * vec_b.w;
850
- const norm_4d = (point) => Math.sqrt(point.x * point.x + point.y * point.y + point.z * point.z + point.w * point.w);
851
- function normalize_4d(point) {
852
- const length = norm_4d(point);
853
- if (length < EPS)
854
- return { x: 0, y: 0, z: 0, w: 0 };
511
+ // Map an ND facet (vertex indices into `points`) back to the public tetrahedron shape
512
+ const facet_to_tetrahedron = (facet, points) => {
513
+ const [nx, ny, nz, nw] = facet.plane.normal;
514
+ const [cx, cy, cz, cw] = facet.centroid;
855
515
  return {
856
- x: point.x / length,
857
- y: point.y / length,
858
- z: point.z / length,
859
- w: point.w / length,
516
+ vertices: facet.vertex_indices.map((idx) => points[idx]),
517
+ normal: { x: nx, y: ny, z: nz, w: nw },
518
+ centroid: { x: cx, y: cy, z: cz, w: cw },
860
519
  };
861
- }
862
- // Compute normal to a 3D hyperplane in 4D space defined by 4 points
863
- //
864
- // Mathematical Background:
865
- // A 3D hyperplane (tetrahedral facet) in 4D is defined by 4 points. The normal vector
866
- // must be orthogonal to all three edge vectors spanning the hyperplane. This is the
867
- // 4D analog of computing a cross product of two vectors in 3D.
868
- //
869
- // Approach:
870
- // 1. Form three edge vectors v1, v2, v3 from point p1
871
- // 2. Find vector n such that: n · v1 = 0, n · v2 = 0, n · v3 = 0
872
- // 3. This is equivalent to finding the null space of the 3×4 matrix [v1; v2; v3]
873
- //
874
- // Implementation:
875
- // The normal components (nx, ny, nz, nw) are computed using Laplace expansion
876
- // (cofactor method) along each column of the matrix. Each component is the determinant
877
- // of the 3×3 submatrix obtained by removing that column, with alternating signs.
878
- //
879
- // References:
880
- // - Barber et al. (1996) "The Quickhull Algorithm for Convex Hulls"
881
- // - https://en.wikipedia.org/wiki/Cross_product#Multilinear_algebra
882
- // - https://mathworld.wolfram.com/Nullspace.html
883
- function compute_plane_4d(p1, p2, p3, p4) {
884
- // Three edge vectors from p1
885
- const v1 = subtract_4d(p2, p1);
886
- const v2 = subtract_4d(p3, p1);
887
- const v3 = subtract_4d(p4, p1);
888
- // Build matrix [v1; v2; v3] and compute normal via cofactor expansion
889
- const matrix = [
890
- [v1.x, v1.y, v1.z, v1.w],
891
- [v2.x, v2.y, v2.z, v2.w],
892
- [v3.x, v3.y, v3.z, v3.w],
893
- ];
894
- // Helper: extract 3×3 submatrix by removing column col_skip, then compute determinant
895
- const det_submatrix = (col_skip) => {
896
- const cols = [0, 1, 2, 3].filter((col) => col !== col_skip);
897
- const submatrix = [
898
- [matrix[0][cols[0]], matrix[0][cols[1]], matrix[0][cols[2]]],
899
- [matrix[1][cols[0]], matrix[1][cols[1]], matrix[1][cols[2]]],
900
- [matrix[2][cols[0]], matrix[2][cols[1]], matrix[2][cols[2]]],
901
- ];
902
- return math.det_3x3(submatrix);
903
- };
904
- // Compute normal components using Laplace expansion along each column
905
- // Alternating signs: +, -, +, -
906
- const signs = [1, -1, 1, -1];
907
- const normal_components = [0, 1, 2, 3].map((col_idx) => signs[col_idx] * det_submatrix(col_idx));
908
- const [x, y, z, w] = normal_components;
909
- const normal = normalize_4d({ x, y, z, w });
910
- // Guard against degenerate (nearly co-planar) points
911
- const normal_magnitude = Math.abs(normal.x) + Math.abs(normal.y) + Math.abs(normal.z) + Math.abs(normal.w);
912
- if (normal_magnitude < EPS) {
913
- return { normal: { x: 0, y: 0, z: 0, w: 0 }, offset: 0 };
914
- }
915
- const offset = -dot_4d(normal, p1);
916
- return { normal, offset };
917
- }
918
- const point_plane_signed_distance_4d = (plane, point) => dot_4d(plane.normal, point) + plane.offset;
919
- const compute_centroid_4d = (p1, p2, p3, p4) => ({
920
- x: (p1.x + p2.x + p3.x + p4.x) / 4,
921
- y: (p1.y + p2.y + p3.y + p4.y) / 4,
922
- z: (p1.z + p2.z + p3.z + p4.z) / 4,
923
- w: (p1.w + p2.w + p3.w + p4.w) / 4,
924
- });
925
- function distance_point_to_hyperplane_4d(p1, p2, p3, point) {
926
- // Distance from point to the 2D hyperplane spanned by p1, p2, p3
927
- const v1 = subtract_4d(p2, p1);
928
- const v2 = subtract_4d(p3, p1);
929
- const vp = subtract_4d(point, p1);
930
- // Project vp onto the plane spanned by v1 and v2
931
- // Use Gram-Schmidt to find orthogonal component
932
- const v1_norm_sq = dot_4d(v1, v1);
933
- const v2_norm_sq = dot_4d(v2, v2);
934
- const v1_dot_v2 = dot_4d(v1, v2);
935
- if (v1_norm_sq < EPS || v2_norm_sq < EPS)
936
- return 0;
937
- const vp_dot_v1 = dot_4d(vp, v1);
938
- const vp_dot_v2 = dot_4d(vp, v2);
939
- // Solve linear system for projection coefficients
940
- const det = v1_norm_sq * v2_norm_sq - v1_dot_v2 * v1_dot_v2;
941
- if (Math.abs(det) < EPS)
942
- return 0;
943
- const alpha = (v2_norm_sq * vp_dot_v1 - v1_dot_v2 * vp_dot_v2) / det;
944
- const beta = (v1_norm_sq * vp_dot_v2 - v1_dot_v2 * vp_dot_v1) / det;
945
- // Compute projection
946
- const proj_x = p1.x + alpha * v1.x + beta * v2.x;
947
- const proj_y = p1.y + alpha * v1.y + beta * v2.y;
948
- const proj_z = p1.z + alpha * v1.z + beta * v2.z;
949
- const proj_w = p1.w + alpha * v1.w + beta * v2.w;
950
- // Distance is the length of (point - projection)
951
- const dx = point.x - proj_x;
952
- const dy = point.y - proj_y;
953
- const dz = point.z - proj_z;
954
- const dw = point.w - proj_w;
955
- return Math.sqrt(dx * dx + dy * dy + dz * dz + dw * dw);
956
- }
957
- // Distance from point to line in 4D
958
- function distance_point_to_line_4d(a, b, p) {
959
- const ab = subtract_4d(b, a);
960
- const ap = subtract_4d(p, a);
961
- const ab_len_sq = dot_4d(ab, ab);
962
- if (ab_len_sq < EPS)
963
- return norm_4d(ap);
964
- // Project ap onto ab
965
- const t = dot_4d(ap, ab) / ab_len_sq;
966
- const projection = {
967
- x: a.x + t * ab.x,
968
- y: a.y + t * ab.y,
969
- z: a.z + t * ab.z,
970
- w: a.w + t * ab.w,
971
- };
972
- return norm_4d(subtract_4d(p, projection));
973
- }
974
- // Maximum sample size for initial simplex selection in 4D hulls (avoids O(n²) for large datasets)
975
- const INITIAL_SIMPLEX_SAMPLE_SIZE = 100;
976
- function choose_initial_4_simplex(points) {
977
- if (points.length < 5)
978
- return null;
979
- // Find two points farthest apart across all dimensions for better numerical stability
980
- // Sample a small subset if dataset is large to avoid O(n²) scaling
981
- const sample_size = Math.min(points.length, INITIAL_SIMPLEX_SAMPLE_SIZE);
982
- const sample_indices = points.length <= sample_size
983
- ? points.map((_, idx) => idx)
984
- : Array.from({ length: sample_size }, (_, idx) => Math.floor((idx * points.length) / sample_size));
985
- let idx_far_a = 0;
986
- let idx_far_b = 0;
987
- let max_dist_sq = -1;
988
- for (const idx_a of sample_indices) {
989
- for (const idx_b of sample_indices) {
990
- if (idx_a >= idx_b)
991
- continue;
992
- const pa = points[idx_a];
993
- const pb = points[idx_b];
994
- const dist_sq = (pa.x - pb.x) ** 2 + (pa.y - pb.y) ** 2 + (pa.z - pb.z) ** 2 + (pa.w - pb.w) ** 2;
995
- if (dist_sq > max_dist_sq) {
996
- max_dist_sq = dist_sq;
997
- idx_far_a = idx_a;
998
- idx_far_b = idx_b;
999
- }
1000
- }
1001
- }
1002
- if (idx_far_a === idx_far_b || max_dist_sq < EPS)
1003
- return null;
1004
- // Find point farthest from line through idx_far_a and idx_far_b
1005
- let idx_far_line = -1;
1006
- let best_dist_line = -1;
1007
- for (let idx = 0; idx < points.length; idx++) {
1008
- if (idx === idx_far_a || idx === idx_far_b)
1009
- continue;
1010
- const dist = distance_point_to_line_4d(points[idx_far_a], points[idx_far_b], points[idx]);
1011
- if (dist > best_dist_line) {
1012
- best_dist_line = dist;
1013
- idx_far_line = idx;
1014
- }
1015
- }
1016
- if (idx_far_line === -1 || best_dist_line < EPS)
1017
- return null;
1018
- // Find point farthest from 2D plane through first three points
1019
- let idx_far_plane = -1;
1020
- let best_dist_plane = -1;
1021
- for (let idx = 0; idx < points.length; idx++) {
1022
- if (idx === idx_far_a || idx === idx_far_b || idx === idx_far_line)
1023
- continue;
1024
- const dist = distance_point_to_hyperplane_4d(points[idx_far_a], points[idx_far_b], points[idx_far_line], points[idx]);
1025
- if (dist > best_dist_plane) {
1026
- best_dist_plane = dist;
1027
- idx_far_plane = idx;
1028
- }
1029
- }
1030
- if (idx_far_plane === -1 || best_dist_plane < EPS)
1031
- return null;
1032
- // Find point farthest from 3D hyperplane through first four points
1033
- const plane0 = compute_plane_4d(points[idx_far_a], points[idx_far_b], points[idx_far_line], points[idx_far_plane]);
1034
- let idx_far_hyperplane = -1;
1035
- let best_dist_hyperplane = -1;
1036
- for (let idx = 0; idx < points.length; idx++) {
1037
- if (idx === idx_far_a ||
1038
- idx === idx_far_b ||
1039
- idx === idx_far_line ||
1040
- idx === idx_far_plane)
1041
- continue;
1042
- const dist = Math.abs(point_plane_signed_distance_4d(plane0, points[idx]));
1043
- if (dist > best_dist_hyperplane) {
1044
- best_dist_hyperplane = dist;
1045
- idx_far_hyperplane = idx;
1046
- }
1047
- }
1048
- if (idx_far_hyperplane === -1 || best_dist_hyperplane < EPS)
1049
- return null;
1050
- return [idx_far_a, idx_far_b, idx_far_line, idx_far_plane, idx_far_hyperplane];
1051
- }
1052
- function make_face_4d(points, a, b, c, d, interior_point) {
1053
- let plane = compute_plane_4d(points[a], points[b], points[c], points[d]);
1054
- let centroid = compute_centroid_4d(points[a], points[b], points[c], points[d]);
1055
- const dist_interior = point_plane_signed_distance_4d(plane, interior_point);
1056
- // Ensure normal points outward (away from interior)
1057
- if (dist_interior > 0) {
1058
- // Swap two vertices to flip normal
1059
- plane = compute_plane_4d(points[a], points[c], points[b], points[d]);
1060
- centroid = compute_centroid_4d(points[a], points[c], points[b], points[d]);
1061
- return { vertices: [a, c, b, d], plane, centroid, outside_points: new Set() };
1062
- }
1063
- return { vertices: [a, b, c, d], plane, centroid, outside_points: new Set() };
1064
- }
1065
- function assign_outside_points_4d(face, points, candidate_indices) {
1066
- face.outside_points.clear();
1067
- for (const idx of candidate_indices) {
1068
- const distance = point_plane_signed_distance_4d(face.plane, points[idx]);
1069
- if (distance > EPS)
1070
- face.outside_points.add(idx);
1071
- }
1072
- }
1073
- function collect_candidate_points_4d(faces) {
1074
- const set = new Set();
1075
- for (const face of faces) {
1076
- for (const idx of face.outside_points)
1077
- set.add(idx);
1078
- }
1079
- return Array.from(set);
1080
- }
1081
- function farthest_point_for_face_4d(points, face) {
1082
- let best_idx = -1;
1083
- let best_distance = -1;
1084
- for (const idx of face.outside_points) {
1085
- const distance = point_plane_signed_distance_4d(face.plane, points[idx]);
1086
- if (distance > best_distance) {
1087
- best_distance = distance;
1088
- best_idx = idx;
1089
- }
1090
- }
1091
- if (best_idx === -1)
1092
- return null;
1093
- return { idx: best_idx, distance: best_distance };
1094
- }
1095
- function build_horizon_4d(faces, visible_face_indices) {
1096
- // In 4D, horizon "ridges" are triangles (3 vertices)
1097
- const ridge_count = new Map();
1098
- for (const face_idx of visible_face_indices) {
1099
- const face = faces[face_idx];
1100
- const [a, b, c, d] = face.vertices;
1101
- // Each tetrahedron face has 4 triangular ridges
1102
- const ridges = [
1103
- [a, b, c],
1104
- [a, b, d],
1105
- [a, c, d],
1106
- [b, c, d],
1107
- ];
1108
- for (const ridge of ridges) {
1109
- const sorted = ridge.slice().sort((x, y) => x - y);
1110
- const key = sorted.join(`|`);
1111
- if (!ridge_count.has(key)) {
1112
- ridge_count.set(key, ridge);
1113
- }
1114
- else {
1115
- // Mark as seen twice (internal ridge)
1116
- ridge_count.set(key, [Number.NaN, Number.NaN, Number.NaN]);
1117
- }
1118
- }
1119
- }
1120
- const horizon = [];
1121
- for (const ridge of ridge_count.values()) {
1122
- if (!Number.isNaN(ridge[0])) {
1123
- horizon.push(ridge);
1124
- }
1125
- }
1126
- return horizon;
1127
- }
520
+ };
521
+ // 4D quickhull (thin adapter over the N-dimensional implementation)
1128
522
  export function compute_quickhull_4d(points) {
1129
- if (points.length < 5)
1130
- return []; // Need at least 5 non-coplanar points for 4D hull
1131
- const initial = choose_initial_4_simplex(points);
1132
- if (!initial)
1133
- return [];
1134
- const [i0, i1, i2, i3, i4] = initial;
1135
- // Interior point for orientation
1136
- const interior_point = {
1137
- x: (points[i0].x + points[i1].x + points[i2].x + points[i3].x + points[i4].x) / 5,
1138
- y: (points[i0].y + points[i1].y + points[i2].y + points[i3].y + points[i4].y) / 5,
1139
- z: (points[i0].z + points[i1].z + points[i2].z + points[i3].z + points[i4].z) / 5,
1140
- w: (points[i0].w + points[i1].w + points[i2].w + points[i3].w + points[i4].w) / 5,
1141
- };
1142
- // Initial 4-simplex has 5 tetrahedral faces
1143
- const faces = [
1144
- make_face_4d(points, i0, i1, i2, i3, interior_point),
1145
- make_face_4d(points, i0, i1, i2, i4, interior_point),
1146
- make_face_4d(points, i0, i1, i3, i4, interior_point),
1147
- make_face_4d(points, i0, i2, i3, i4, interior_point),
1148
- make_face_4d(points, i1, i2, i3, i4, interior_point),
1149
- ];
1150
- const all_indices = [];
1151
- for (let idx = 0; idx < points.length; idx++) {
1152
- if (idx === i0 || idx === i1 || idx === i2 || idx === i3 || idx === i4)
1153
- continue;
1154
- all_indices.push(idx);
1155
- }
1156
- for (const face of faces) {
1157
- assign_outside_points_4d(face, points, all_indices);
1158
- }
1159
- // Main Quick Hull iteration
1160
- while (true) {
1161
- // Step 1: Find face with farthest outside point (the "eye" point)
1162
- let chosen_face_idx = -1;
1163
- let chosen_point_idx = -1;
1164
- let max_distance = -1;
1165
- for (let face_idx = 0; face_idx < faces.length; face_idx++) {
1166
- const face = faces[face_idx];
1167
- if (face.outside_points.size === 0)
1168
- continue;
1169
- const far = farthest_point_for_face_4d(points, face);
1170
- if (far && far.distance > max_distance) {
1171
- max_distance = far.distance;
1172
- chosen_face_idx = face_idx;
1173
- chosen_point_idx = far.idx;
1174
- }
1175
- }
1176
- if (chosen_face_idx === -1)
1177
- break; // All points processed
1178
- const eye_idx = chosen_point_idx;
1179
- // Step 2: Find all faces visible from the eye point
1180
- const visible_face_indices = new Set();
1181
- for (let face_idx = 0; face_idx < faces.length; face_idx++) {
1182
- const face = faces[face_idx];
1183
- const dist = point_plane_signed_distance_4d(face.plane, points[eye_idx]);
1184
- if (dist > EPS)
1185
- visible_face_indices.add(face_idx);
1186
- }
1187
- // Step 3: Build horizon ridges (boundary between visible and non-visible faces)
1188
- const horizon_ridges = build_horizon_4d(faces, visible_face_indices);
1189
- const visible_faces = Array.from(visible_face_indices).sort((a, b) => b - a);
1190
- const candidate_points = collect_candidate_points_4d(visible_faces.map((idx) => faces[idx]));
1191
- // Step 4: Remove visible faces (they'll be replaced by new ones through eye point)
1192
- for (const idx of visible_faces) {
1193
- faces.splice(idx, 1);
1194
- }
1195
- // Step 5: Create new faces connecting horizon ridges to eye point
1196
- const new_faces = [];
1197
- for (const [u, v, w] of horizon_ridges) {
1198
- const new_face = make_face_4d(points, u, v, w, eye_idx, interior_point);
1199
- new_faces.push(new_face);
1200
- }
1201
- // Step 6: Reassign outside points from removed faces to new faces
1202
- for (const face of new_faces)
1203
- face.outside_points.clear();
1204
- for (const idx of candidate_points) {
1205
- if (idx === eye_idx)
1206
- continue;
1207
- let best_face = null;
1208
- let best_distance = EPS;
1209
- for (const face of new_faces) {
1210
- const dist = point_plane_signed_distance_4d(face.plane, points[idx]);
1211
- if (dist > best_distance) {
1212
- best_distance = dist;
1213
- best_face = face;
1214
- }
1215
- }
1216
- if (best_face)
1217
- best_face.outside_points.add(idx);
1218
- }
1219
- faces.push(...new_faces);
1220
- }
1221
- return faces.map((face) => {
1222
- const [a, b, c, d] = face.vertices;
1223
- return {
1224
- vertices: [points[a], points[b], points[c], points[d]],
1225
- normal: face.plane.normal,
1226
- centroid: face.centroid,
1227
- };
1228
- });
523
+ const facets = compute_quickhull_nd(points.map(p4_to_nd));
524
+ return facets.map((facet) => facet_to_tetrahedron(facet, points));
1229
525
  }
1230
526
  export function compute_lower_hull_4d(points) {
1231
- const all_faces = compute_quickhull_4d(points);
1232
- // Filter for "lower" faces: those with normal pointing down in w direction
1233
- return all_faces.filter((face) => face.normal.w < 0 - EPS);
1234
- }
1235
- // Check if 3D point (x,y,z) is inside 3D tetrahedron using barycentric coordinates
1236
- function point_in_tetrahedron_3d(p0, p1, p2, p3, point) {
1237
- // Solve for barycentric coordinates: point = l0*p0 + l1*p1 + l2*p2 + l3*p3
1238
- // with l0 + l1 + l2 + l3 = 1
1239
- // Build the linear system
1240
- const matrix = [
1241
- [p0.x, p1.x, p2.x, p3.x],
1242
- [p0.y, p1.y, p2.y, p3.y],
1243
- [p0.z, p1.z, p2.z, p3.z],
1244
- [1, 1, 1, 1],
1245
- ];
1246
- const rhs = [point.x, point.y, point.z, 1];
1247
- // Solve using Cramer's rule with 4x4 determinants
1248
- const det_main = math.det_4x4(matrix);
1249
- if (Math.abs(det_main) < EPS) {
1250
- return { inside: false, bary: [0, 0, 0, 0] };
1251
- }
1252
- // Compute barycentric coordinates using Cramer's rule
1253
- const bary = [0, 0, 0, 0];
1254
- for (let idx = 0; idx < 4; idx++) {
1255
- const m_i = matrix.map((row) => [...row]);
1256
- for (let row = 0; row < 4; row++) {
1257
- m_i[row][idx] = rhs[row];
1258
- }
1259
- bary[idx] = math.det_4x4(m_i) / det_main;
1260
- }
1261
- // Check if inside: all barycentric coords must be >= 0 and sum to 1
1262
- const eps_bary = -1e-9;
1263
- const inside = bary.every((coord) => coord >= eps_bary) &&
1264
- Math.abs(bary.reduce((sum, coord) => sum + coord, 0) - 1) < 1e-6;
1265
- return { inside, bary };
1266
- }
1267
- const build_tetrahedron_models = (hull_tetrahedra) => hull_tetrahedra.map((tet) => {
1268
- const [p0, p1, p2, p3] = tet.vertices;
1269
- const vertices_3d = [
1270
- { x: p0.x, y: p0.y, z: p0.z },
1271
- { x: p1.x, y: p1.y, z: p1.z },
1272
- { x: p2.x, y: p2.y, z: p2.z },
1273
- { x: p3.x, y: p3.y, z: p3.z },
1274
- ];
1275
- const xs = [p0.x, p1.x, p2.x, p3.x];
1276
- const ys = [p0.y, p1.y, p2.y, p3.y];
1277
- const zs = [p0.z, p1.z, p2.z, p3.z];
1278
- return {
1279
- vertices: tet.vertices,
1280
- vertices_3d,
1281
- min_x: Math.min(...xs),
1282
- max_x: Math.max(...xs),
1283
- min_y: Math.min(...ys),
1284
- max_y: Math.max(...ys),
1285
- min_z: Math.min(...zs),
1286
- max_z: Math.max(...zs),
1287
- };
1288
- });
1289
- // Compute distance from point to lower hull in 4D
1290
- export const compute_e_above_hull_4d = (points, hull_tetrahedra) => {
1291
- // Precompute bounding boxes for fast prefiltering
1292
- const models = build_tetrahedron_models(hull_tetrahedra);
1293
- return points.map(({ x, y, z, w }) => {
1294
- let hull_w = null;
1295
- for (const model of models) {
1296
- // Fast bounding box prefilter
1297
- if (x < model.min_x - EPS ||
1298
- x > model.max_x + EPS ||
1299
- y < model.min_y - EPS ||
1300
- y > model.max_y + EPS ||
1301
- z < model.min_z - EPS ||
1302
- z > model.max_z + EPS)
1303
- continue;
1304
- // Check if point's (x,y,z) is inside the 3D projection of the tetrahedron
1305
- const { inside, bary } = point_in_tetrahedron_3d(model.vertices_3d[0], model.vertices_3d[1], model.vertices_3d[2], model.vertices_3d[3], { x, y, z });
1306
- if (inside) {
1307
- // Compute w on the hull at this (x,y,z) using barycentric interpolation
1308
- const [p0, p1, p2, p3] = model.vertices;
1309
- const w_on_hull = bary[0] * p0.w + bary[1] * p1.w + bary[2] * p2.w + bary[3] * p3.w;
1310
- hull_w = hull_w === null ? w_on_hull : Math.min(hull_w, w_on_hull);
1311
- }
1312
- }
1313
- // If no tetrahedron contains this point's spatial projection, it's outside the valid
1314
- // composition domain. Return NaN to indicate invalid input.
1315
- if (hull_w === null)
1316
- return NaN;
1317
- return w - hull_w;
1318
- });
1319
- };
1320
- // --- N-Dimensional Convex Hull (for 5+ element systems) ---
1321
- // N-dimensional vector operations with dimension validation
1322
- const subtract_nd = (vec_a, vec_b) => {
1323
- if (vec_a.length !== vec_b.length) {
1324
- throw new Error(`Vector dimension mismatch: ${vec_a.length} vs ${vec_b.length}`);
1325
- }
1326
- return vec_a.map((val, idx) => val - vec_b[idx]);
1327
- };
527
+ // Filter for "lower" faces: those with normal pointing down in w (energy) direction
528
+ return compute_quickhull_4d(points).filter((tet) => tet.normal.w < 0 - EPS);
529
+ }
530
+ // Compute distance from point to lower hull in 4D (w is the energy dimension).
531
+ // Returns raw (unclamped) distances; NaN for points outside the composition domain.
532
+ export const compute_e_above_hull_4d = (points, hull_tetrahedra) => e_above_hull_from_simplices(points.map(p4_to_nd), hull_tetrahedra.map((tet) => tet.vertices.map(p4_to_nd)));
533
+ // --- N-Dimensional Convex Hull (single quickhull core; the 3D/4D APIs above adapt to it) ---
534
+ // N-dimensional vector operations. These run in quickhull's hot loops, so dimension
535
+ // agreement is validated once per compute_quickhull_nd call instead of per operation.
536
+ const subtract_nd = (vec_a, vec_b) => vec_a.map((val, idx) => val - vec_b[idx]);
1328
537
  const dot_nd = (vec_a, vec_b) => {
1329
- if (vec_a.length !== vec_b.length) {
1330
- throw new Error(`Vector dimension mismatch: ${vec_a.length} vs ${vec_b.length}`);
1331
- }
1332
- return vec_a.reduce((sum, val, idx) => sum + val * vec_b[idx], 0);
538
+ let sum = 0;
539
+ for (let idx = 0; idx < vec_a.length; idx++)
540
+ sum += vec_a[idx] * vec_b[idx];
541
+ return sum;
1333
542
  };
1334
543
  const norm_nd = (vec) => Math.sqrt(dot_nd(vec, vec));
1335
544
  const normalize_nd = (vec) => {
@@ -1341,8 +550,8 @@ const normalize_nd = (vec) => {
1341
550
  // Compute normal to hyperplane through N points in N-dimensional space
1342
551
  // Uses null space computation via cofactor expansion
1343
552
  function compute_hyperplane_nd(points) {
1344
- const n = points.length;
1345
- if (n < 2)
553
+ const n_points = points.length;
554
+ if (n_points < 2)
1346
555
  return { normal: [], offset: 0 };
1347
556
  // Build (N-1) edge vectors from points[0]
1348
557
  const edges = points.slice(1).map((pt) => subtract_nd(pt, points[0]));
@@ -1370,16 +579,18 @@ const compute_centroid_nd = (points) => {
1370
579
  const dim = points[0].length;
1371
580
  return Array.from({ length: dim }, (_, idx) => points.reduce((sum, pt) => sum + pt[idx], 0) / points.length);
1372
581
  };
582
+ // Maximum sample size for initial simplex selection (avoids O(n²) for large datasets)
583
+ const INITIAL_SIMPLEX_SAMPLE_SIZE = 100;
1373
584
  // Find N+1 points that span N dimensions (initial simplex for quickhull)
1374
585
  function choose_initial_simplex_nd(points) {
1375
- const n = points[0]?.length;
1376
- if (!n || points.length < n + 1)
586
+ const dim = points[0]?.length;
587
+ if (!dim || points.length < dim + 1)
1377
588
  return null;
1378
589
  const chosen = [];
1379
590
  // Greedily pick points that maximize distance from current affine hull
1380
591
  // Start with two points that are farthest apart
1381
592
  let [best_i, best_j, best_dist] = [0, 1, -1];
1382
- const sample_size = Math.min(points.length, 100);
593
+ const sample_size = Math.min(points.length, INITIAL_SIMPLEX_SAMPLE_SIZE);
1383
594
  const sample_indices = points.length <= sample_size
1384
595
  ? points.map((_, idx) => idx)
1385
596
  : Array.from({ length: sample_size }, (_, idx) => Math.floor((idx * points.length) / sample_size));
@@ -1400,7 +611,7 @@ function choose_initial_simplex_nd(points) {
1400
611
  chosen.push(best_i, best_j);
1401
612
  const chosen_set = new Set(chosen);
1402
613
  // Add remaining points to span higher dimensions
1403
- while (chosen.length < n + 1) {
614
+ while (chosen.length < dim + 1) {
1404
615
  let [best_idx, best_distance] = [-1, -1];
1405
616
  // Hoist chosen_points computation outside inner loop for O(n) instead of O(n²)
1406
617
  const chosen_points = chosen.map((idx_c) => points[idx_c]);
@@ -1434,8 +645,8 @@ function distance_to_affine_hull_nd(point, hull_points) {
1434
645
  const ab_len_sq = dot_nd(ab, ab);
1435
646
  if (ab_len_sq < EPS)
1436
647
  return norm_nd(ap);
1437
- const t = dot_nd(ap, ab) / ab_len_sq;
1438
- const proj = pt_a.map((val, idx) => val + t * ab[idx]);
648
+ const proj_frac = dot_nd(ap, ab) / ab_len_sq;
649
+ const proj = pt_a.map((val, idx) => val + proj_frac * ab[idx]);
1439
650
  return norm_nd(subtract_nd(point, proj));
1440
651
  }
1441
652
  // For 3+ points, use orthogonal projection
@@ -1446,8 +657,8 @@ function distance_to_affine_hull_nd(point, hull_points) {
1446
657
  // Solve least squares using Gram matrix G[i][j] = dot(edge_i, edge_j)
1447
658
  const gram = edges.map((edge_i) => edges.map((edge_j) => dot_nd(edge_i, edge_j)));
1448
659
  const rhs = edges.map((edge) => dot_nd(edge, vp));
1449
- // Solve Gram * coeffs = rhs using simple Gaussian elimination
1450
- const coeffs = solve_linear_system(gram, rhs);
660
+ // Solve Gram * coeffs = rhs
661
+ const coeffs = math.solve_linear_system(gram, rhs);
1451
662
  if (!coeffs) {
1452
663
  // Fallback: Gram-Schmidt when Gram matrix is singular (linearly dependent edges)
1453
664
  // Build orthogonal basis and accumulate projection in single pass
@@ -1477,50 +688,6 @@ function distance_to_affine_hull_nd(point, hull_points) {
1477
688
  const proj = origin.map((val, dim) => val + coeffs.reduce((sum, coeff, idx) => sum + coeff * edges[idx][dim], 0));
1478
689
  return norm_nd(subtract_nd(point, proj));
1479
690
  }
1480
- // Solve linear system Ax = b using Gaussian elimination with partial pivoting
1481
- function solve_linear_system(matrix_a, vec_b) {
1482
- const n = matrix_a.length;
1483
- if (n === 0)
1484
- return [];
1485
- if (vec_b.length !== n)
1486
- return null; // Dimension mismatch
1487
- // Augmented matrix
1488
- const aug = matrix_a.map((row, idx) => [...row, vec_b[idx]]);
1489
- for (let col = 0; col < n; col++) {
1490
- // Find pivot
1491
- let max_row = col;
1492
- for (let row = col + 1; row < n; row++) {
1493
- if (Math.abs(aug[row][col]) > Math.abs(aug[max_row][col])) {
1494
- max_row = row;
1495
- }
1496
- }
1497
- if (Math.abs(aug[max_row][col]) < EPS)
1498
- return null; // Singular
1499
- // Swap rows if needed
1500
- if (max_row !== col) {
1501
- const temp = aug[col];
1502
- aug[col] = aug[max_row];
1503
- aug[max_row] = temp;
1504
- }
1505
- // Eliminate
1506
- for (let row = col + 1; row < n; row++) {
1507
- const factor = aug[row][col] / aug[col][col];
1508
- for (let elim_col = col; elim_col <= n; elim_col++) {
1509
- aug[row][elim_col] -= factor * aug[col][elim_col];
1510
- }
1511
- }
1512
- }
1513
- // Back substitution
1514
- const result = Array(n).fill(0);
1515
- for (let row = n - 1; row >= 0; row--) {
1516
- let sum = aug[row][n];
1517
- for (let col = row + 1; col < n; col++) {
1518
- sum -= aug[row][col] * result[col];
1519
- }
1520
- result[row] = sum / aug[row][row];
1521
- }
1522
- return result;
1523
- }
1524
691
  // Create a simplex face with correct normal orientation (outward from interior)
1525
692
  function make_face_nd(points, vertex_indices, interior_point) {
1526
693
  const face_points = vertex_indices.map((idx) => points[idx]);
@@ -1558,9 +725,9 @@ function build_horizon_nd(faces, visible_indices) {
1558
725
  for (const face_idx of visible_indices) {
1559
726
  const face = faces[face_idx];
1560
727
  const verts = face.vertex_indices;
1561
- const n = verts.length;
1562
- // Each face has n ridges, each ridge omits one vertex
1563
- for (let skip = 0; skip < n; skip++) {
728
+ const n_verts = verts.length;
729
+ // Each face has n_verts ridges, each ridge omits one vertex
730
+ for (let skip = 0; skip < n_verts; skip++) {
1564
731
  const ridge = verts.filter((_, idx) => idx !== skip);
1565
732
  const sorted = [...ridge].sort((a, b) => a - b);
1566
733
  const key = sorted.join(`|`);
@@ -1580,8 +747,14 @@ function build_horizon_nd(faces, visible_indices) {
1580
747
  export function compute_quickhull_nd(points) {
1581
748
  if (points.length === 0)
1582
749
  return [];
1583
- const n = points[0].length;
1584
- if (points.length < n + 1)
750
+ const dim = points[0].length;
751
+ // Validate dimensions once up front; vector ops in the hot loops assume agreement
752
+ for (const pt of points) {
753
+ if (pt.length !== dim) {
754
+ throw new Error(`Vector dimension mismatch: ${pt.length} vs ${dim}`);
755
+ }
756
+ }
757
+ if (points.length < dim + 1)
1585
758
  return [];
1586
759
  // Find initial n-simplex
1587
760
  const initial = choose_initial_simplex_nd(points);
@@ -1589,9 +762,9 @@ export function compute_quickhull_nd(points) {
1589
762
  return [];
1590
763
  // Interior point for normal orientation
1591
764
  const interior = compute_centroid_nd(initial.map((idx) => points[idx]));
1592
- // Create initial n+1 facets (each omits one vertex from the simplex)
765
+ // Create initial dim+1 facets (each omits one vertex from the simplex)
1593
766
  const faces = [];
1594
- for (let skip = 0; skip <= n; skip++) {
767
+ for (let skip = 0; skip <= dim; skip++) {
1595
768
  const verts = initial.filter((_, idx) => idx !== skip);
1596
769
  faces.push(make_face_nd(points, verts, interior));
1597
770
  }
@@ -1672,27 +845,24 @@ export function compute_lower_hull_nd(faces) {
1672
845
  // Last dimension is energy; negative normal means "downward"
1673
846
  return faces.filter((face) => (face.plane.normal.at(-1) ?? 0) < -EPS);
1674
847
  }
1675
- function build_simplex_models_nd(faces, points) {
1676
- return faces.map((face) => {
1677
- const vertices = face.vertex_indices.map((idx) => points[idx]);
1678
- const n = vertices[0].length;
1679
- // Spatial coords are all except last (energy)
1680
- const vertices_spatial = vertices.map((pt) => pt.slice(0, n - 1));
1681
- // Compute bounding box in spatial dimensions
1682
- const spatial_dim = n - 1;
1683
- const bbox_min = Array.from({ length: spatial_dim }, (_, idx) => Math.min(...vertices_spatial.map((pt) => pt[idx])));
1684
- const bbox_max = Array.from({ length: spatial_dim }, (_, idx) => Math.max(...vertices_spatial.map((pt) => pt[idx])));
1685
- return { vertices, vertices_spatial, bbox_min, bbox_max };
1686
- });
1687
- }
848
+ const build_simplex_models_nd = (simplices) => simplices.map((vertices) => {
849
+ const dim = vertices[0].length;
850
+ // Spatial coords are all except last (energy)
851
+ const vertices_spatial = vertices.map((pt) => pt.slice(0, dim - 1));
852
+ // Compute bounding box in spatial dimensions
853
+ const spatial_dim = dim - 1;
854
+ const bbox_min = Array.from({ length: spatial_dim }, (_, idx) => Math.min(...vertices_spatial.map((pt) => pt[idx])));
855
+ const bbox_max = Array.from({ length: spatial_dim }, (_, idx) => Math.max(...vertices_spatial.map((pt) => pt[idx])));
856
+ return { vertices, vertices_spatial, bbox_min, bbox_max };
857
+ });
1688
858
  // Check if point is inside simplex and return barycentric coordinates
1689
859
  // Uses linear system solution: point = sum(bary[i] * vertex[i]) with sum(bary) = 1
1690
860
  function point_in_simplex_nd(point, simplex_vertices) {
1691
- const n = simplex_vertices.length; // Number of vertices = spatial_dim + 1
1692
- if (n === 0)
861
+ const n_verts = simplex_vertices.length; // Number of vertices = spatial_dim + 1
862
+ if (n_verts === 0)
1693
863
  return null;
1694
864
  const dim = point.length;
1695
- if (dim !== n - 1)
865
+ if (dim !== n_verts - 1)
1696
866
  return null; // Spatial dim should be one less than vertex count
1697
867
  // Build linear system: [v1-v0, v2-v0, ..., vn-v0] * [b1, b2, ..., bn] = point - v0
1698
868
  // Then b0 = 1 - sum(b1..bn)
@@ -1705,7 +875,7 @@ function point_in_simplex_nd(point, simplex_vertices) {
1705
875
  for (let row = 0; row < dim; row++) {
1706
876
  matrix.push(edges.map((edge) => edge[row]));
1707
877
  }
1708
- const coeffs = solve_linear_system(matrix, rhs);
878
+ const coeffs = math.solve_linear_system(matrix, rhs);
1709
879
  if (!coeffs)
1710
880
  return null;
1711
881
  // Compute b0 = 1 - sum(coeffs), ensuring sum(bary) = 1 by construction
@@ -1716,11 +886,17 @@ function point_in_simplex_nd(point, simplex_vertices) {
1716
886
  }
1717
887
  // Compute energy above hull for N-dimensional points
1718
888
  export function compute_e_above_hull_nd(query_points, hull_facets, all_points) {
1719
- const models = build_simplex_models_nd(hull_facets, all_points);
889
+ return e_above_hull_from_simplices(query_points, hull_facets.map((facet) => facet.vertex_indices.map((idx) => all_points[idx])));
890
+ }
891
+ // Shared e-above-hull core: query points against hull simplices given as vertex
892
+ // coordinate lists (last coordinate = energy). Returns raw (unclamped) distances;
893
+ // NaN for queries whose spatial projection lies outside all simplices.
894
+ function e_above_hull_from_simplices(query_points, simplices) {
895
+ const models = build_simplex_models_nd(simplices);
1720
896
  return query_points.map((query) => {
1721
- const n = query.length;
1722
- const spatial = query.slice(0, n - 1); // All but last coord
1723
- const energy = query[n - 1];
897
+ const dim = query.length;
898
+ const spatial = query.slice(0, dim - 1); // All but last coord
899
+ const energy = query[dim - 1];
1724
900
  let hull_energy = null;
1725
901
  for (const model of models) {
1726
902
  // Fast bounding box rejection
@@ -1739,7 +915,7 @@ export function compute_e_above_hull_nd(query_points, hull_facets, all_points) {
1739
915
  if (!bary)
1740
916
  continue;
1741
917
  // Interpolate energy using barycentric coords
1742
- const e_hull = bary.reduce((sum, coeff, idx) => sum + coeff * model.vertices[idx][n - 1], 0);
918
+ const e_hull = bary.reduce((sum, coeff, idx) => sum + coeff * model.vertices[idx][dim - 1], 0);
1743
919
  hull_energy = hull_energy === null ? e_hull : Math.min(hull_energy, e_hull);
1744
920
  }
1745
921
  // If no facet contains this point's spatial projection, it's outside the valid