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,14 +1,43 @@
1
1
  import { COMPRESSION_EXTENSIONS_REGEX, CONFIG_DIRS_REGEX, STRUCT_KEYWORDS_REGEX, STRUCT_KEYWORDS_STRICT_REGEX, STRUCTURE_EXTENSIONS_REGEX, TRAJ_KEYWORDS_REGEX, VASP_FILES_REGEX, XYZ_EXTXYZ_REGEX, } from '../constants';
2
- import { ELEM_SYMBOLS } from '../labels';
2
+ import { FALLBACK_ELEMENTS, is_elem_symbol } from '../element';
3
+ import { strip_compression_extensions } from '../io';
3
4
  import * as math from '../math';
4
5
  import { wrap_to_unit_cell } from './pbc';
5
- import { normalize_scientific_notation } from '../utils';
6
+ import { make_site } from './site';
7
+ import { iter_xyz_frames } from '../trajectory/helpers';
8
+ import { normalize_scientific_notation, to_error } from '../utils';
6
9
  import { load as yaml_load } from 'js-yaml';
10
+ // === Parse error contract ===
11
+ // Individual format parsers (parse_poscar, parse_cif, parse_xyz, parse_phonopy_yaml,
12
+ // parse_optimade_json, ...) return `T | null` on failure and record failure reasons in a
13
+ // module-level collector (mirrored to the console). The top-level entry points
14
+ // parse_structure_file and parse_any_structure reset the collector on entry and THROW a
15
+ // descriptive Error aggregating the recorded reasons when nothing parses, so failure
16
+ // causes can reach the UI (callers surface error.message). Warnings (element-symbol
17
+ // fallbacks, skipped atoms, ...) never fail a parse and only go to the console.
18
+ let parse_errors = [];
19
+ const reset_parse_diagnostics = () => {
20
+ parse_errors = [];
21
+ };
22
+ // Record a failure reason; with `error` present, logs in `console.error('msg:', error)` form
23
+ const diag_error = (message, error) => {
24
+ const detail = error === undefined ? `` : `: ${to_error(error).message}`;
25
+ parse_errors.push(`${message}${detail}`);
26
+ if (error === undefined)
27
+ console.error(message);
28
+ else
29
+ console.error(`${message}:`, error);
30
+ };
31
+ const diag_warn = (message) => console.warn(message);
32
+ // Aggregate recorded failure reasons into the Error thrown by top-level entry points
33
+ const aggregate_parse_error = (filename) => {
34
+ const reasons = [...new Set(parse_errors)];
35
+ const detail = reasons.length ? `: ${reasons.join(`; `)}` : ``;
36
+ return new Error(`Failed to parse structure${filename ? ` from '${filename}'` : ``}${detail}`);
37
+ };
7
38
  const cif_coords_key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
8
39
  const cif_site_key = (element, abc, label) => `${element}|${label}|${cif_coords_key(abc)}`;
9
40
  const clone_structure_properties = (properties) => structuredClone(properties);
10
- const FALLBACK_ELEMENTS = [`H`, `He`, `Li`, `Be`, `B`, `C`, `N`, `O`, `F`, `Ne`];
11
- const is_known_element_symbol = (symbol) => ELEM_SYMBOLS.includes(symbol);
12
41
  const vec3_from_values = (values, context) => {
13
42
  if (values?.length !== 3) {
14
43
  throw new Error(`Invalid ${context}: expected 3 coordinates, got ${values?.length ?? 0}`);
@@ -41,9 +70,9 @@ function parse_coordinate_line(line) {
41
70
  const sanitized = line
42
71
  .trim()
43
72
  // Add space when '-' follows a digit and precedes a digit or dot
44
- .replace(/(\d)-(?=[\d.])/g, `$1 -`)
73
+ .replaceAll(/(\d)-(?=[\d.])/g, `$1 -`)
45
74
  // Revert accidental spaces after exponent markers
46
- .replace(/([eE])\s-\s/g, `$1-`);
75
+ .replaceAll(/([eE])\s-\s/g, `$1-`);
47
76
  tokens = sanitized.split(/\s+/);
48
77
  }
49
78
  if (tokens.length < 3)
@@ -54,13 +83,37 @@ function parse_coordinate_line(line) {
54
83
  function validate_element_symbol(symbol, index) {
55
84
  // Clean symbol (remove suffixes like _pv, /hash)
56
85
  const clean_symbol = symbol.split(/[_/]/)[0];
57
- if (is_known_element_symbol(clean_symbol))
86
+ if (is_elem_symbol(clean_symbol))
58
87
  return clean_symbol;
59
88
  // Fallback to default elements by atomic number
60
89
  const fallback = FALLBACK_ELEMENTS[index % FALLBACK_ELEMENTS.length] ?? `H`;
61
- console.warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
90
+ diag_warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
62
91
  return fallback;
63
92
  }
93
+ // Per OPTIMADE spec, species_at_sites holds species NAMES (e.g. 'Si1') resolved via the
94
+ // species list: highest-concentration entry in chemical_symbols wins, non-element entries
95
+ // like 'vacancy' are skipped, and unresolved names are treated as element symbols.
96
+ // Returns the chosen element plus its index into the species' chemical_symbols
97
+ // (sym_idx = -1 on fallback), so callers can read the matching mass/concentration entry.
98
+ function resolve_optimade_element(species_name, species_list, index) {
99
+ const spec = species_list?.find((entry) => entry.name === species_name);
100
+ let best;
101
+ for (const [sym_idx, symbol] of (spec?.chemical_symbols ?? []).entries()) {
102
+ if (!is_elem_symbol(symbol))
103
+ continue;
104
+ const conc = spec?.concentration?.[sym_idx] ?? 0;
105
+ if (!best || conc > best.conc)
106
+ best = { symbol, conc, sym_idx };
107
+ }
108
+ if (best)
109
+ return { symbol: best.symbol, sym_idx: best.sym_idx };
110
+ // Fallback: the name may be an element with a trailing atom index (e.g. 'O1');
111
+ // element symbols never contain digits, so stripping them is safe
112
+ const stripped = species_name.replace(/\d+$/, ``);
113
+ if (is_elem_symbol(stripped))
114
+ return { symbol: stripped, sym_idx: -1 };
115
+ return { symbol: validate_element_symbol(species_name, index), sym_idx: -1 };
116
+ }
64
117
  const try_create_cart_to_frac = (lattice_matrix) => {
65
118
  try {
66
119
  return math.create_cart_to_frac(lattice_matrix);
@@ -74,20 +127,43 @@ const approximate_cart_to_frac = (xyz, axis_lengths) => [
74
127
  Math.abs(axis_lengths[1]) > math.EPS ? xyz[1] / axis_lengths[1] : 0,
75
128
  Math.abs(axis_lengths[2]) > math.EPS ? xyz[2] / axis_lengths[2] : 0,
76
129
  ];
77
- // Parse VASP POSCAR file format
130
+ // Build a 3x3 matrix from 3 row vectors; error context is suffixed with the 1-based row index
131
+ const matrix3x3_from_rows = (rows, context) => [
132
+ vec3_from_values(rows[0], `${context} 1`),
133
+ vec3_from_values(rows[1], `${context} 2`),
134
+ vec3_from_values(rows[2], `${context} 3`),
135
+ ];
136
+ // cart→frac converter that falls back to per-axis-length division for singular lattices.
137
+ // axis_lengths defaults to the row norms of the lattice matrix.
138
+ const cart_to_frac_with_fallback = (matrix, axis_lengths) => {
139
+ const exact_converter = try_create_cart_to_frac(matrix);
140
+ if (exact_converter)
141
+ return { convert: exact_converter, exact: true };
142
+ const lengths = axis_lengths ?? [
143
+ Math.hypot(...matrix[0]),
144
+ Math.hypot(...matrix[1]),
145
+ Math.hypot(...matrix[2]),
146
+ ];
147
+ return { convert: (xyz) => approximate_cart_to_frac(xyz, lengths), exact: false };
148
+ };
149
+ // @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse VASP POSCAR.
78
150
  export function parse_poscar(content) {
79
151
  try {
80
- const lines = content.replace(/^\s+/, ``).split(/\r?\n/);
152
+ // Strip only horizontal whitespace: a blank first (comment) line is valid POSCAR
153
+ const lines = content.replace(/^[ \t]+/, ``).split(/\r?\n/);
81
154
  if (lines.length < 8) {
82
- console.error(`POSCAR file too short`);
155
+ diag_error(`POSCAR file too short`);
83
156
  return null;
84
157
  }
85
- // Parse scaling factor (line 2)
86
- let scale_factor = parseFloat(lines[1]);
158
+ // Scale line: one value (negative = target volume) or three per-axis Cartesian factors
159
+ const scale_tokens = lines[1].trim().split(/\s+/).map(parseFloat);
160
+ let scale_factor = scale_tokens[0];
87
161
  if (isNaN(scale_factor)) {
88
- console.error(`Invalid scaling factor in POSCAR`);
162
+ diag_error(`Invalid scaling factor in POSCAR`);
89
163
  return null;
90
164
  }
165
+ const scale_vec = scale_tokens.slice(0, 3);
166
+ const per_axis_scale = scale_vec.length === 3 && !scale_vec.some(isNaN) ? scale_vec : null;
91
167
  // Parse lattice vectors (lines 3-5)
92
168
  const parse_vector = (line, line_num) => {
93
169
  const coords = line.trim().split(/\s+/).map(parse_coordinate);
@@ -98,17 +174,19 @@ export function parse_poscar(content) {
98
174
  parse_vector(lines[3], 4),
99
175
  parse_vector(lines[4], 5),
100
176
  ];
101
- // Handle negative scale factor (volume-based scaling)
102
- if (scale_factor < 0) {
177
+ // Handle negative scale factor (volume-based scaling, single-factor form only)
178
+ if (!per_axis_scale && scale_factor < 0) {
103
179
  const volume = Math.abs(math.det_3x3(lattice_vecs));
104
- scale_factor = Math.pow(-scale_factor / volume, 1 / 3);
180
+ if (volume < math.EPS) {
181
+ diag_error(`POSCAR target-volume scaling requires a non-singular lattice`);
182
+ return null;
183
+ }
184
+ scale_factor = (-scale_factor / volume) ** (1 / 3);
105
185
  }
106
- // Scale lattice vectors
107
- const scaled_lattice = [
108
- math.scale(lattice_vecs[0], scale_factor),
109
- math.scale(lattice_vecs[1], scale_factor),
110
- math.scale(lattice_vecs[2], scale_factor),
111
- ];
186
+ // Scale lattice vectors (per-axis factors multiply Cartesian components)
187
+ const axis_scale = per_axis_scale ?? [scale_factor, scale_factor, scale_factor];
188
+ const apply_axis_scale = (vec) => vec.map((val, axis) => val * axis_scale[axis]);
189
+ const scaled_lattice = lattice_vecs.map(apply_axis_scale);
112
190
  // Parse element symbols and atom counts (may span multiple lines)
113
191
  let line_index = 5;
114
192
  let element_symbols = [];
@@ -116,7 +194,7 @@ export function parse_poscar(content) {
116
194
  // Detect if this is VASP 5+ format (has element symbols)
117
195
  // Try to parse the first token as a number - if it succeeds, it's VASP 4 format
118
196
  const first_token = lines[line_index].trim().split(/\s+/)[0];
119
- const first_token_as_number = parseInt(first_token);
197
+ const first_token_as_number = parseInt(first_token, 10);
120
198
  const has_element_symbols = isNaN(first_token_as_number);
121
199
  if (has_element_symbols) {
122
200
  // VASP 5+ format - parse element symbols (may span multiple lines)
@@ -126,7 +204,7 @@ export function parse_poscar(content) {
126
204
  if (line_index + lookahead_idx >= lines.length)
127
205
  break;
128
206
  const next_line_first_token = lines[line_index + lookahead_idx].trim().split(/\s+/)[0];
129
- const next_token_as_number = parseInt(next_line_first_token);
207
+ const next_token_as_number = parseInt(next_line_first_token, 10);
130
208
  if (!isNaN(next_token_as_number)) {
131
209
  symbol_lines = lookahead_idx;
132
210
  break;
@@ -157,144 +235,104 @@ export function parse_poscar(content) {
157
235
  line_index += 1;
158
236
  }
159
237
  if (element_symbols.length !== atom_counts.length) {
160
- console.error(`Mismatch between element symbols and atom counts`);
238
+ diag_error(`Mismatch between element symbols and atom counts`);
239
+ return null;
240
+ }
241
+ if (line_index >= lines.length) {
242
+ diag_error(`Missing coordinate mode line in POSCAR`);
161
243
  return null;
162
244
  }
163
245
  // Check for selective dynamics
164
246
  let has_selective_dynamics = false;
165
- if (line_index < lines.length) {
166
- let coordinate_mode = lines[line_index].trim().toUpperCase();
167
- if (coordinate_mode.startsWith(`S`)) {
168
- has_selective_dynamics = true;
169
- line_index += 1;
170
- if (line_index < lines.length) {
171
- coordinate_mode = lines[line_index].trim().toUpperCase();
172
- }
173
- else {
174
- console.error(`Missing coordinate mode after selective dynamics`);
175
- return null;
176
- }
247
+ let coordinate_mode = lines[line_index].trim().toUpperCase();
248
+ if (coordinate_mode.startsWith(`S`)) {
249
+ has_selective_dynamics = true;
250
+ line_index += 1;
251
+ if (line_index < lines.length) {
252
+ coordinate_mode = lines[line_index].trim().toUpperCase();
177
253
  }
178
- // Determine coordinate mode
179
- const is_direct = coordinate_mode.startsWith(`D`);
180
- const is_cartesian = coordinate_mode.startsWith(`C`) || coordinate_mode.startsWith(`K`);
181
- if (!is_direct && !is_cartesian) {
182
- console.error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
254
+ else {
255
+ diag_error(`Missing coordinate mode after selective dynamics`);
183
256
  return null;
184
257
  }
185
- // Parse atomic positions
186
- const poscar_axis_lengths = [
187
- Math.hypot(...scaled_lattice[0]),
188
- Math.hypot(...scaled_lattice[1]),
189
- Math.hypot(...scaled_lattice[2]),
190
- ];
191
- const poscar_frac_to_cart = math.create_frac_to_cart(scaled_lattice);
192
- const poscar_cart_to_frac = try_create_cart_to_frac(scaled_lattice);
193
- if (!is_direct && !poscar_cart_to_frac) {
194
- console.warn(`POSCAR: singular lattice, using axis-length fallback for cart→frac`);
195
- }
196
- const sites = [];
197
- let atom_index = 0;
198
- for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
199
- const element = validate_element_symbol(element_symbols[elem_idx], elem_idx);
200
- const count = atom_counts[elem_idx];
201
- for (let atom_count_idx = 0; atom_count_idx < count; atom_count_idx++) {
202
- const coord_line_idx = line_index + 1 + atom_index + atom_count_idx;
203
- if (coord_line_idx >= lines.length) {
204
- console.error(`Not enough coordinate lines in POSCAR`);
205
- return null;
206
- }
207
- const coords = vec3_from_values(parse_coordinate_line(lines[coord_line_idx]), `POSCAR atom coordinates on line ${coord_line_idx + 1}`);
208
- // Parse selective dynamics if present
209
- let selective_dynamics;
210
- if (has_selective_dynamics) {
211
- const tokens = lines[coord_line_idx].trim().split(/\s+/);
212
- if (tokens.length >= 6) {
213
- selective_dynamics = [tokens[3] === `T`, tokens[4] === `T`, tokens[5] === `T`];
214
- }
215
- }
216
- let xyz;
217
- let abc;
218
- if (is_direct) {
219
- // Store fractional coordinates, wrapping to [0, 1) range
220
- abc = wrap_to_unit_cell(coords);
221
- xyz = poscar_frac_to_cart(abc);
222
- }
223
- else {
224
- // Already Cartesian, scale if needed
225
- xyz = math.scale(coords, scale_factor);
226
- const raw_abc = poscar_cart_to_frac
227
- ? poscar_cart_to_frac(xyz)
228
- : approximate_cart_to_frac(xyz, poscar_axis_lengths);
229
- // Wrap fractional coordinates to [0, 1) range
230
- abc = wrap_to_unit_cell(raw_abc);
258
+ }
259
+ // Determine coordinate mode
260
+ const is_direct = coordinate_mode.startsWith(`D`);
261
+ const is_cartesian = coordinate_mode.startsWith(`C`) || coordinate_mode.startsWith(`K`);
262
+ if (!is_direct && !is_cartesian) {
263
+ diag_error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
264
+ return null;
265
+ }
266
+ // Parse atomic positions
267
+ const poscar_frac_to_cart = math.create_frac_to_cart(scaled_lattice);
268
+ const poscar_cart_to_frac = cart_to_frac_with_fallback(scaled_lattice);
269
+ if (!is_direct && !poscar_cart_to_frac.exact) {
270
+ diag_warn(`POSCAR: singular lattice, using axis-length fallback for cart→frac`);
271
+ }
272
+ const sites = [];
273
+ let atom_index = 0;
274
+ for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
275
+ const element = validate_element_symbol(element_symbols[elem_idx], elem_idx);
276
+ const count = atom_counts[elem_idx];
277
+ for (let atom_count_idx = 0; atom_count_idx < count; atom_count_idx++) {
278
+ const coord_line_idx = line_index + 1 + atom_index + atom_count_idx;
279
+ if (coord_line_idx >= lines.length) {
280
+ diag_error(`Not enough coordinate lines in POSCAR`);
281
+ return null;
282
+ }
283
+ const coords = vec3_from_values(parse_coordinate_line(lines[coord_line_idx]), `POSCAR atom coordinates on line ${coord_line_idx + 1}`);
284
+ // Parse selective dynamics if present
285
+ let selective_dynamics;
286
+ if (has_selective_dynamics) {
287
+ const tokens = lines[coord_line_idx].trim().split(/\s+/);
288
+ if (tokens.length >= 6) {
289
+ selective_dynamics = [tokens[3] === `T`, tokens[4] === `T`, tokens[5] === `T`];
231
290
  }
232
- const site = {
233
- species: [{ element, occu: 1, oxidation_state: 0 }],
234
- abc,
235
- xyz,
236
- label: `${element}${atom_index + atom_count_idx + 1}`,
237
- properties: selective_dynamics ? { selective_dynamics: selective_dynamics } : {},
238
- };
239
- sites.push(site);
240
291
  }
241
- atom_index += count;
292
+ // Cartesian input is scaled then converted to fractional (axis-length fallback
293
+ // for singular lattices); abc wraps to [0, 1) and xyz is recomputed from it so
294
+ // both stay consistent (singular Cartesian keeps the scaled input as xyz)
295
+ const cart = is_direct ? null : apply_axis_scale(coords);
296
+ const raw_abc = cart ? poscar_cart_to_frac.convert(cart) : coords;
297
+ const abc = wrap_to_unit_cell(raw_abc);
298
+ const xyz = cart && !poscar_cart_to_frac.exact ? cart : poscar_frac_to_cart(abc);
299
+ sites.push(make_site(element, abc, xyz, `${element}${atom_index + atom_count_idx + 1}`, selective_dynamics ? { selective_dynamics } : {}));
242
300
  }
243
- const lattice_params = math.calc_lattice_params(scaled_lattice);
244
- const structure = {
245
- sites,
246
- lattice: { matrix: scaled_lattice, ...lattice_params },
247
- };
248
- return structure;
249
- }
250
- else {
251
- console.error(`Missing coordinate mode line in POSCAR`);
252
- return null;
301
+ atom_index += count;
253
302
  }
303
+ const lattice_params = math.calc_lattice_params(scaled_lattice);
304
+ return { sites, lattice: { matrix: scaled_lattice, ...lattice_params } };
254
305
  }
255
306
  catch (error) {
256
- console.error(`Error parsing POSCAR file:`, error);
307
+ diag_error(`Error parsing POSCAR file`, error);
257
308
  return null;
258
309
  }
259
310
  }
260
- // Parse XYZ file format. Supports both standard XYZ and extended XYZ formats with multi-frame support
311
+ // @internal parser exported for tests + trajectory parser; public entry points: parse_structure_file/parse_any_structure. Parse standard/extended XYZ (multi-frame).
261
312
  export function parse_xyz(content) {
262
313
  try {
263
314
  const normalized_content = content.trim();
264
315
  if (!normalized_content) {
265
- console.error(`Empty XYZ file`);
316
+ diag_error(`Empty XYZ file`);
266
317
  return null;
267
318
  }
268
- // Split into frames by reading the atom count and slicing lines
319
+ // Walk frames by reading atom counts; multi-frame XYZ parses only the last frame
269
320
  const all_lines = normalized_content.split(/\r?\n/);
270
- const frames = [];
271
- let frame_line_idx = 0;
272
- while (frame_line_idx < all_lines.length) {
273
- const numAtoms = parseInt(all_lines[frame_line_idx].trim(), 10);
274
- if (!isNaN(numAtoms) &&
275
- numAtoms > 0 &&
276
- frame_line_idx + numAtoms + 1 < all_lines.length) {
277
- const frameLines = all_lines.slice(frame_line_idx, frame_line_idx + numAtoms + 2);
278
- frames.push(frameLines.join(`\n`));
279
- frame_line_idx += numAtoms + 2;
280
- }
281
- else
282
- frame_line_idx++;
283
- }
284
- // If no frames found, try simple parsing
285
- if (frames.length === 0)
286
- frames.push(normalized_content);
287
- // Parse the last frame (or only frame)
288
- const frame_content = frames.at(-1) ?? ``;
289
- const lines = frame_content.trim().split(/\r?\n/);
321
+ let last_frame = null;
322
+ for (const frame of iter_xyz_frames(all_lines))
323
+ last_frame = frame;
324
+ // If no complete frame found, fall back to parsing the whole content as one frame
325
+ const lines = last_frame
326
+ ? all_lines.slice(last_frame.start, last_frame.start + last_frame.num_atoms + 2)
327
+ : all_lines;
290
328
  if (lines.length < 2) {
291
- console.error(`XYZ frame too short`);
329
+ diag_error(`XYZ frame too short`);
292
330
  return null;
293
331
  }
294
332
  // Parse number of atoms (line 1)
295
- const num_atoms = parseInt(lines[0].trim());
333
+ const num_atoms = parseInt(lines[0].trim(), 10);
296
334
  if (isNaN(num_atoms) || num_atoms <= 0) {
297
- console.error(`Invalid number of atoms in XYZ file`);
335
+ diag_error(`Invalid number of atoms in XYZ file`);
298
336
  return null;
299
337
  }
300
338
  // Parse comment line (line 2) - may contain lattice info for extended XYZ
@@ -305,61 +343,54 @@ export function parse_xyz(content) {
305
343
  if (lattice_match) {
306
344
  const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
307
345
  if (lattice_values.length === 9) {
308
- const lattice_vectors = [
309
- vec3_from_values(lattice_values.slice(0, 3), `XYZ lattice vector 1`),
310
- vec3_from_values(lattice_values.slice(3, 6), `XYZ lattice vector 2`),
311
- vec3_from_values(lattice_values.slice(6, 9), `XYZ lattice vector 3`),
312
- ];
346
+ const lattice_vectors = matrix3x3_from_rows([lattice_values.slice(0, 3), lattice_values.slice(3, 6), lattice_values.slice(6, 9)], `XYZ lattice vector`);
313
347
  const lattice_params = math.calc_lattice_params(lattice_vectors);
314
348
  lattice = { matrix: lattice_vectors, ...lattice_params };
315
349
  }
316
350
  }
317
351
  // Parse atomic coordinates (starting from line 3)
318
- const xyz_axis_lengths = lattice ? [lattice.a, lattice.b, lattice.c] : null;
319
352
  let xyz_frac_to_cart = null;
320
353
  let xyz_cart_to_frac = null;
321
354
  if (lattice) {
322
355
  xyz_frac_to_cart = math.create_frac_to_cart(lattice.matrix);
323
- xyz_cart_to_frac = try_create_cart_to_frac(lattice.matrix);
356
+ xyz_cart_to_frac = cart_to_frac_with_fallback(lattice.matrix, [
357
+ lattice.a,
358
+ lattice.b,
359
+ lattice.c,
360
+ ]).convert;
324
361
  }
325
362
  const sites = [];
326
363
  for (let atom_idx = 0; atom_idx < num_atoms; atom_idx++) {
327
364
  const line_idx = atom_idx + 2;
328
365
  if (line_idx >= lines.length) {
329
- console.error(`Not enough coordinate lines in XYZ file`);
366
+ diag_error(`Not enough coordinate lines in XYZ file`);
330
367
  return null;
331
368
  }
332
369
  const parts = lines[line_idx].trim().split(/\s+/);
333
370
  if (parts.length < 4) {
334
- console.error(`Invalid coordinate line in XYZ file`);
371
+ diag_error(`Invalid coordinate line in XYZ file`);
335
372
  return null;
336
373
  }
337
374
  const element = validate_element_symbol(parts[0], atom_idx);
338
375
  const xyz = vec3_from_values(parts.slice(1, 4).map(parse_coordinate), `XYZ atom position ${atom_idx + 1}`);
339
376
  // Calculate fractional coordinates if lattice is available
340
377
  let abc = [0, 0, 0];
341
- if (lattice && xyz_frac_to_cart && xyz_axis_lengths) {
342
- abc = xyz_cart_to_frac
343
- ? xyz_cart_to_frac(xyz)
344
- : approximate_cart_to_frac(xyz, xyz_axis_lengths);
378
+ if (lattice && xyz_frac_to_cart && xyz_cart_to_frac) {
345
379
  // Ensure fractional coordinates are wrapped into [0, 1) for consistency
346
- abc = wrap_to_unit_cell(abc);
380
+ abc = wrap_to_unit_cell(xyz_cart_to_frac(xyz));
347
381
  // Keep rendered atoms inside primary unit cell by recomputing xyz
348
382
  const wrapped_xyz = xyz_frac_to_cart(abc);
349
383
  xyz[0] = wrapped_xyz[0];
350
384
  xyz[1] = wrapped_xyz[1];
351
385
  xyz[2] = wrapped_xyz[2];
352
386
  }
353
- const species = [{ element, occu: 1, oxidation_state: 0 }];
354
- const label = `${element}${atom_idx + 1}`;
355
- const site = { species, abc, xyz, label, properties: {} };
356
- sites.push(site);
387
+ sites.push(make_site(element, abc, xyz, `${element}${atom_idx + 1}`));
357
388
  }
358
389
  const structure = { sites, ...(lattice && { lattice }) };
359
390
  return structure;
360
391
  }
361
392
  catch (error) {
362
- console.error(`Error parsing XYZ file:`, error);
393
+ diag_error(`Error parsing XYZ file`, error);
363
394
  return null;
364
395
  }
365
396
  }
@@ -369,7 +400,7 @@ const parse_symmetry_expression = (expr_input) => {
369
400
  const coefficients = [0, 0, 0];
370
401
  let translation = 0;
371
402
  // Remove all whitespace
372
- const expr = expr_input.replace(/\s+/g, ``);
403
+ const expr = expr_input.replaceAll(/\s+/g, ``);
373
404
  if (!expr)
374
405
  return { coefficients, translation };
375
406
  // Tokenize: split into terms while preserving signs
@@ -480,13 +511,15 @@ const extract_cif_cell_parameters = (text, type, strict = true) => text
480
511
  .split(`\n`)
481
512
  .filter((line) => line.startsWith(`_${type}`))
482
513
  .map((line) => {
483
- const tokens = line.split(/\s+/).filter((token) => token.length > 0);
514
+ // Strip trailing comment (# after whitespace) and take the value right after the tag
515
+ const sans_comment = line.replace(/\s#.*$/, ``);
516
+ const tokens = sans_comment.split(/\s+/).filter(Boolean);
484
517
  if (tokens.length < 2) {
485
518
  if (strict)
486
519
  throw new Error(`Invalid CIF cell parameter line format: ${line}`);
487
520
  return null;
488
521
  }
489
- const value = parseFloat((tokens.at(-1) ?? ``).split(`(`)[0]);
522
+ const value = parseFloat(tokens[1].split(`(`)[0]);
490
523
  if (isNaN(value)) {
491
524
  if (strict)
492
525
  throw new Error(`Invalid CIF cell parameter in line: ${line}`);
@@ -518,6 +551,23 @@ const build_cif_atom_site_header_indices = (headers) => {
518
551
  });
519
552
  return indices;
520
553
  };
554
+ // Walk CIF loop_ blocks: yields each loop's header tags plus the index of its first data line
555
+ function* iter_cif_loops(lines) {
556
+ for (let idx = 0; idx < lines.length; idx++) {
557
+ if (lines[idx].trim() !== `loop_`)
558
+ continue;
559
+ const headers = [];
560
+ let jj = idx + 1;
561
+ while (jj < lines.length && lines[jj].trim().startsWith(`_`)) {
562
+ headers.push(lines[jj].trim());
563
+ jj++;
564
+ }
565
+ yield { headers, data_start: jj };
566
+ }
567
+ }
568
+ // Split a CIF data line into whitespace-separated tokens, keeping quoted multi-word
569
+ // values as single tokens and stripping the quotes
570
+ const split_cif_tokens = (line) => (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []).map((token) => token.replaceAll(/['"]/g, ``));
521
571
  // Parse atom data from CIF with robust error handling
522
572
  const parse_cif_atom_data = (raw_data, indices, coords_type) => {
523
573
  const { label = 0, symbol = -1, occupancy = -1 } = indices;
@@ -541,11 +591,11 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
541
591
  const occu = occupancy >= 0 && raw_data[occupancy]
542
592
  ? parseFloat(raw_data[occupancy].split(`(`)[0]) || 1.0
543
593
  : 1.0;
544
- const element_symbol = (symbol >= 0 && /^([A-Z][a-z]*)/.exec(raw_data[symbol])?.[1]) ||
545
- raw_data[label]?.match(/([A-Z][a-z]*)/g)?.[0] ||
546
- (() => {
547
- throw new Error(`Could not extract element symbol from: ${raw_data.join(` `)}`);
548
- })();
594
+ const from_symbol = symbol >= 0 ? /^([A-Z][a-z]*)/.exec(raw_data[symbol])?.[1] : undefined;
595
+ const element_symbol = from_symbol ?? raw_data[label]?.match(/([A-Z][a-z]*)/g)?.[0];
596
+ if (!element_symbol) {
597
+ throw new Error(`Could not extract element symbol from: ${raw_data.join(` `)}`);
598
+ }
549
599
  return {
550
600
  id: raw_data[label],
551
601
  element: element_symbol,
@@ -554,12 +604,12 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
554
604
  occupancy: occu,
555
605
  };
556
606
  };
557
- // Parse CIF (Crystallographic Information File) format
607
+ // @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse CIF (Crystallographic Information File).
558
608
  export function parse_cif(content, wrap_fractional_coords = true, strict = true) {
559
609
  try {
560
610
  const text = content.trim();
561
611
  if (!text) {
562
- console.error(`CIF file is empty`);
612
+ diag_error(`CIF file is empty`);
563
613
  return null;
564
614
  }
565
615
  // Find atom site loop that actually contains coordinates (fract or Cartn)
@@ -567,16 +617,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
567
617
  let atom_headers = [];
568
618
  const atom_data_lines = [];
569
619
  const symmetry_ops = [];
570
- for (let ii = 0; ii < lines.length; ii++) {
571
- if (lines[ii].trim() !== `loop_`)
572
- continue;
573
- let jj = ii + 1;
574
- const headers = [];
575
- // Collect headers for this loop
576
- while (jj < lines.length && lines[jj].trim().startsWith(`_`)) {
577
- headers.push(lines[jj].trim());
578
- jj++;
579
- }
620
+ for (const { headers, data_start } of iter_cif_loops(lines)) {
621
+ let jj = data_start;
580
622
  // Check if this is a symmetry operations loop
581
623
  if (headers.some((header) => header.includes(`_symmetry_equiv_pos_as_xyz`) ||
582
624
  header.includes(`_space_group_symop_operation_xyz`))) {
@@ -603,10 +645,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
603
645
  (indices_preview.cart_x !== undefined &&
604
646
  indices_preview.cart_y !== undefined &&
605
647
  indices_preview.cart_z !== undefined);
606
- if (!has_coords) {
607
- ii = jj - 1;
648
+ if (!has_coords)
608
649
  continue;
609
- }
610
650
  // This is the desired atom-site loop with coordinates: collect data lines
611
651
  atom_headers = headers;
612
652
  while (jj < lines.length) {
@@ -617,7 +657,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
617
657
  if (line.startsWith(`;`)) {
618
658
  let multi_line_data = ``;
619
659
  while (jj < lines.length && !lines[jj].trim().endsWith(`;`)) {
620
- multi_line_data += lines[jj] + `\n`;
660
+ multi_line_data += `${lines[jj]}\n`;
621
661
  jj++;
622
662
  }
623
663
  multi_line_data += lines[jj];
@@ -632,8 +672,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
632
672
  if (atom_data_lines.length > 0)
633
673
  break;
634
674
  }
635
- if (!atom_headers.length || !atom_data_lines.length) {
636
- console.error(`No valid atom site loop found in CIF file`);
675
+ if (atom_headers.length === 0 || atom_data_lines.length === 0) {
676
+ diag_error(`No valid atom site loop found in CIF file`);
637
677
  return null;
638
678
  }
639
679
  // Parse atom data with error handling
@@ -649,7 +689,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
649
689
  ? `cart`
650
690
  : null;
651
691
  if (!coords_type) {
652
- console.error(`CIF atom site loop missing coordinates (fract or Cartn)`);
692
+ diag_error(`CIF atom site loop missing coordinates (fract or Cartn)`);
653
693
  return null;
654
694
  }
655
695
  // Collect required coordinate indices
@@ -657,12 +697,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
657
697
  ? [header_indices.x, header_indices.y, header_indices.z]
658
698
  : [header_indices.cart_x, header_indices.cart_y, header_indices.cart_z];
659
699
  const atoms = atom_data_lines
660
- .map((line) => {
661
- // Handle quoted multi-word values by splitting only on whitespace
662
- // that is not inside quotes.
663
- const tokens = line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
664
- return tokens.map((token) => token.replace(/['"]/g, ``));
665
- })
700
+ .map(split_cif_tokens)
666
701
  .filter((tokens) => {
667
702
  const { disorder } = header_indices;
668
703
  const max_required_idx = Math.max(...required_indices);
@@ -674,20 +709,20 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
674
709
  return parse_cif_atom_data(tokens, header_indices, coords_type);
675
710
  }
676
711
  catch (error) {
677
- console.warn(`Skipping invalid atom data: ${error}`);
712
+ diag_warn(`Skipping invalid atom data: ${error}`);
678
713
  return null;
679
714
  }
680
715
  })
681
716
  .filter((atom) => atom !== null);
682
- if (!atoms.length) {
683
- console.error(`No valid atoms found in CIF file`);
717
+ if (atoms.length === 0) {
718
+ diag_error(`No valid atoms found in CIF file`);
684
719
  return null;
685
720
  }
686
721
  // Extract cell parameters and build lattice
687
722
  const lengths = extract_cif_cell_parameters(text, `cell_length`, strict);
688
723
  const angles = extract_cif_cell_parameters(text, `cell_angle`, strict);
689
724
  if (lengths.length < 3 || angles.length < 3) {
690
- console.error(`Insufficient cell parameters in CIF file`);
725
+ diag_error(`Insufficient cell parameters in CIF file`);
691
726
  return null;
692
727
  }
693
728
  // Build lattice and create sites
@@ -696,60 +731,44 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
696
731
  const lattice_matrix = math.cell_to_lattice_matrix(a, b, c, alpha, beta, gamma);
697
732
  const lattice_params = math.calc_lattice_params(lattice_matrix);
698
733
  const frac_to_cart = math.create_frac_to_cart(lattice_matrix);
699
- const cart_to_frac = try_create_cart_to_frac(lattice_matrix);
734
+ const cart_to_frac = cart_to_frac_with_fallback(lattice_matrix, [a, b, c]).convert;
700
735
  // Create sites with coordinate conversion and symmetry operations
701
- const wrap_vec3 = (v) => (wrap_fractional_coords ? wrap_to_unit_cell(v) : v);
736
+ const wrap_vec3 = (vec) => wrap_fractional_coords ? wrap_to_unit_cell(vec) : vec;
702
737
  // Apply symmetry operations to generate all equivalent positions
703
738
  const all_sites = [];
704
739
  // Normalize symmetry operations (trim/strip quotes) but preserve duplicates; we deduplicate positions later
705
740
  const normalized_ops = symmetry_ops
706
- .map((op) => /['"]([^'"]+)['"]/.exec(op)?.[1] || op.trim())
707
- .map((op) => op.replace(/\s+/g, ``));
741
+ .map((op) => /['"]([^'"]+)['"]/.exec(op)?.[1] ?? op.trim())
742
+ .map((op) => op.replaceAll(/\s+/g, ``));
708
743
  // Rely on symmetry operations list for all centering/translations to avoid double-counting
709
744
  // TODO: Support conventional cells with centering by discovering centering from space group metadata
710
745
  // when present (e.g. P, I, F, C, R centering types)
711
- const centering_vectors = [[0, 0, 0]];
712
746
  // Inspect optional _atom_type_number_in_cell loop to see if atom sites are already expanded
713
- const atom_type_counts = (() => {
714
- const map = {};
715
- const text_lines = text.split(`\n`);
716
- for (let li = 0; li < text_lines.length; li++) {
717
- if (text_lines[li].trim() !== `loop_`) {
718
- continue;
719
- }
720
- let lj = li + 1;
721
- const hdrs = [];
722
- while (lj < text_lines.length && text_lines[lj].trim().startsWith(`_`)) {
723
- hdrs.push(text_lines[lj].trim().toLowerCase());
724
- lj++;
725
- }
726
- const sym_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_symbol`));
727
- const num_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_number_in_cell`));
728
- if (sym_idx !== -1 && num_idx !== -1) {
729
- while (lj < text_lines.length) {
730
- const line = text_lines[lj].trim();
731
- if (!line || line === `loop_` || line.startsWith(`data_`))
732
- break;
733
- if (line.startsWith(`#`)) {
734
- lj++;
735
- continue;
736
- }
737
- const toks = (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).map((tok) => tok.replace(/['"]/g, ``));
738
- if (toks.length > Math.max(sym_idx, num_idx)) {
739
- // Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
740
- const match = /^([A-Z][a-z]*)/.exec(toks[sym_idx]);
741
- const sym = match ? match[1] : toks[sym_idx];
742
- const num = parseInt(toks[num_idx]);
743
- if (sym && !Number.isNaN(num))
744
- map[sym] = num;
745
- }
746
- lj++;
747
- }
747
+ const atom_type_counts = {};
748
+ for (const { headers, data_start } of iter_cif_loops(lines)) {
749
+ const hdrs = headers.map((hdr) => hdr.toLowerCase());
750
+ const sym_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_symbol`));
751
+ const num_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_number_in_cell`));
752
+ if (sym_idx === -1 || num_idx === -1)
753
+ continue;
754
+ for (let lj = data_start; lj < lines.length; lj++) {
755
+ const line = lines[lj].trim();
756
+ if (!line || line === `loop_` || line.startsWith(`data_`))
748
757
  break;
758
+ if (line.startsWith(`#`))
759
+ continue;
760
+ const toks = split_cif_tokens(line);
761
+ if (toks.length > Math.max(sym_idx, num_idx)) {
762
+ // Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
763
+ const match = /^([A-Z][a-z]*)/.exec(toks[sym_idx]);
764
+ const sym = match ? match[1] : toks[sym_idx];
765
+ const num = parseInt(toks[num_idx], 10);
766
+ if (sym && !Number.isNaN(num))
767
+ atom_type_counts[sym] = num;
749
768
  }
750
769
  }
751
- return map;
752
- })();
770
+ break;
771
+ }
753
772
  const observed_counts = {};
754
773
  for (const atom of atoms) {
755
774
  observed_counts[atom.element] = (observed_counts[atom.element] || 0) + 1;
@@ -774,41 +793,26 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
774
793
  }
775
794
  else {
776
795
  const xyz_base = [atom.coords[0], atom.coords[1], atom.coords[2]];
777
- const atom_abc = wrap_vec3(cart_to_frac
778
- ? cart_to_frac(xyz_base)
779
- : approximate_cart_to_frac(xyz_base, [a, b, c]));
796
+ const atom_abc = wrap_vec3(cart_to_frac(xyz_base));
780
797
  fractional_atom = { ...atom, coords: atom_abc, coords_type: `fract` };
781
798
  }
782
799
  // First apply symmetry operations in fractional space
783
800
  const equiv_atoms = apply_symmetry_ops(fractional_atom, ops_to_use, wrap_fractional_coords);
784
- // Then apply lattice centering shifts to each equivalent position
785
801
  for (const equiv_atom of equiv_atoms) {
786
- for (const cv of centering_vectors) {
787
- const abc = wrap_vec3([
788
- equiv_atom.coords[0] + cv[0],
789
- equiv_atom.coords[1] + cv[1],
790
- equiv_atom.coords[2] + cv[2],
791
- ]);
792
- const key = cif_site_key(element, abc, equiv_atom.id);
793
- if (seen_site_keys.has(key))
794
- continue;
795
- seen_site_keys.add(key);
796
- const xyz = frac_to_cart(abc);
797
- all_sites.push({
798
- species: [{ element, occu: equiv_atom.occupancy, oxidation_state: 0 }],
799
- abc,
800
- xyz,
801
- label: equiv_atom.id,
802
- properties: {},
803
- });
804
- }
802
+ const abc = wrap_vec3(equiv_atom.coords);
803
+ const key = cif_site_key(element, abc, equiv_atom.id);
804
+ if (seen_site_keys.has(key))
805
+ continue;
806
+ seen_site_keys.add(key);
807
+ const xyz = frac_to_cart(abc);
808
+ all_sites.push(make_site(element, abc, xyz, equiv_atom.id, {}, equiv_atom.occupancy));
805
809
  }
806
810
  }
807
811
  const sites = all_sites;
808
812
  return { sites, lattice: { matrix: lattice_matrix, ...lattice_params } };
809
813
  }
810
814
  catch (error) {
811
- console.error(`Error parsing CIF file:`, error);
815
+ diag_error(`Error parsing CIF file`, error);
812
816
  return null;
813
817
  }
814
818
  }
@@ -816,11 +820,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
816
820
  function convert_phonopy_cell(cell) {
817
821
  const sites = [];
818
822
  // Phonopy stores lattice vectors as rows, use them directly
819
- const lattice_matrix = [
820
- vec3_from_values(cell.lattice[0], `phonopy lattice vector 1`),
821
- vec3_from_values(cell.lattice[1], `phonopy lattice vector 2`),
822
- vec3_from_values(cell.lattice[2], `phonopy lattice vector 3`),
823
- ];
823
+ const lattice_matrix = matrix3x3_from_rows(cell.lattice, `phonopy lattice vector`);
824
824
  // Process each atomic site
825
825
  const phonopy_frac_to_cart = math.create_frac_to_cart(lattice_matrix);
826
826
  for (const point of cell.points) {
@@ -831,9 +831,7 @@ function convert_phonopy_cell(cell) {
831
831
  mass: point.mass,
832
832
  ...(point.reduced_to !== undefined && { reduced_to: point.reduced_to }),
833
833
  };
834
- const species = [{ element, occu: 1.0, oxidation_state: 0 }];
835
- const site = { species, abc, xyz, label: point.symbol, properties };
836
- sites.push(site);
834
+ sites.push(make_site(element, abc, xyz, point.symbol, properties));
837
835
  }
838
836
  // Calculate lattice parameters
839
837
  const calculated_lattice_params = math.calc_lattice_params(lattice_matrix);
@@ -852,7 +850,7 @@ const get_phonopy_cell = (data, cell_type) => {
852
850
  const cell = Reflect.get(data, cell_type);
853
851
  return is_phonopy_cell(cell) ? cell : undefined;
854
852
  };
855
- // Parse phonopy YAML file and return the requested cell type (or preferred single structure)
853
+ // @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse phonopy YAML, returns requested cell type (or preferred single structure).
856
854
  export function parse_phonopy_yaml(content, cell_type) {
857
855
  try {
858
856
  // Parse YAML content but exclude large phonon_displacements array for performance
@@ -879,7 +877,7 @@ export function parse_phonopy_yaml(content, cell_type) {
879
877
  const filtered_content = filtered_lines.join(`\n`);
880
878
  const data = yaml_load(filtered_content);
881
879
  if (!data) {
882
- console.error(`Failed to parse phonopy YAML`);
880
+ diag_error(`Failed to parse phonopy YAML`);
883
881
  return null;
884
882
  }
885
883
  // If specific cell type requested, parse only that one
@@ -887,10 +885,8 @@ export function parse_phonopy_yaml(content, cell_type) {
887
885
  const cell = get_phonopy_cell(data, cell_type);
888
886
  if (cell)
889
887
  return convert_phonopy_cell(cell);
890
- else {
891
- console.error(`Requested cell type '${cell_type}' not found in phonopy YAML`);
892
- return null;
893
- }
888
+ diag_error(`Requested cell type '${cell_type}' not found in phonopy YAML`);
889
+ return null;
894
890
  }
895
891
  // Auto mode: return preferred structure in order of preference
896
892
  // 1. supercell (most detailed)
@@ -905,11 +901,11 @@ export function parse_phonopy_yaml(content, cell_type) {
905
901
  get_phonopy_cell(data, `primitive_cell`);
906
902
  if (auto_cell)
907
903
  return convert_phonopy_cell(auto_cell);
908
- console.error(`No valid cells found in phonopy YAML`);
904
+ diag_error(`No valid cells found in phonopy YAML`);
909
905
  return null;
910
906
  }
911
907
  catch (error) {
912
- console.error(`Error parsing phonopy YAML:`, error);
908
+ diag_error(`Error parsing phonopy YAML`, error);
913
909
  return null;
914
910
  }
915
911
  }
@@ -943,8 +939,8 @@ function find_structure_in_json(obj, visited = new WeakSet()) {
943
939
  }
944
940
  return null;
945
941
  }
946
- // Type guard to validate structure-like objects
947
- function is_parsed_structure(obj) {
942
+ // Type guard to validate structure-like objects (sites array with species + coordinates)
943
+ export function is_parsed_structure(obj) {
948
944
  if (!obj || typeof obj !== `object`)
949
945
  return false;
950
946
  const sites = `sites` in obj ? obj.sites : undefined;
@@ -982,15 +978,25 @@ export function normalize_fractional_coords(structure) {
982
978
  });
983
979
  return { ...structure, sites: normalized_sites };
984
980
  }
985
- // Auto-detect file format and parse accordingly
986
- export function parse_structure_file(content, filename) {
981
+ // Detect a structure inside already-stringified JSON (OPTIMADE or pymatgen/nested).
982
+ // Throws if `content` isn't valid JSON; returns null if it holds no known structure.
983
+ const detect_json_structure = (content) => {
984
+ const parsed = JSON.parse(content);
985
+ if (is_optimade_raw(parsed)) {
986
+ const result = parse_optimade_from_raw(parsed);
987
+ if (result)
988
+ return result;
989
+ }
990
+ // Otherwise try parsing as pymatgen/nested structure JSON
991
+ const structure = find_structure_in_json(parsed);
992
+ return structure ? normalize_fractional_coords(structure) : null;
993
+ };
994
+ // Internal: auto-detect file format, returns null on failure after recording reasons (see parse error contract at top)
995
+ function parse_structure_file_impl(content, filename) {
987
996
  // If a filename is provided, try to detect format by file extension first
988
997
  if (filename) {
989
998
  // Handle compressed files by removing compression extensions
990
- let base_filename = filename.toLowerCase();
991
- while (COMPRESSION_EXTENSIONS_REGEX.test(base_filename)) {
992
- base_filename = base_filename.replace(COMPRESSION_EXTENSIONS_REGEX, ``);
993
- }
999
+ const base_filename = strip_compression_extensions(filename);
994
1000
  const ext = base_filename.split(`.`).pop();
995
1001
  // Try to detect format by file extension
996
1002
  if (ext === `xyz` || ext === `extxyz`)
@@ -998,27 +1004,18 @@ export function parse_structure_file(content, filename) {
998
1004
  // CIF files
999
1005
  if (ext === `cif`)
1000
1006
  return parse_cif(content);
1001
- // JSON files - try OPTIMADE JSON structure format first, then pymatgen
1007
+ // JSON files - extension is authoritative, so failures return null
1002
1008
  if (ext === `json`) {
1003
1009
  try {
1004
- // Parse once, reuse for detection and parsing
1005
- const parsed = JSON.parse(content);
1006
- if (is_optimade_raw(parsed)) {
1007
- const result = parse_optimade_from_raw(parsed);
1008
- if (result)
1009
- return result;
1010
- }
1011
- // Otherwise, try to parse as pymatgen/nested structure JSON
1012
- const structure = find_structure_in_json(parsed);
1013
- if (structure)
1014
- return normalize_fractional_coords(structure);
1015
- console.error(`JSON file does not contain a valid structure format`);
1016
- return null;
1010
+ const result = detect_json_structure(content);
1011
+ if (result)
1012
+ return result;
1013
+ diag_error(`JSON file does not contain a valid structure format`);
1017
1014
  }
1018
1015
  catch (error) {
1019
- console.error(`Error parsing JSON file:`, error);
1020
- return null;
1016
+ diag_error(`Error parsing JSON file`, error);
1021
1017
  }
1018
+ return null;
1022
1019
  }
1023
1020
  // YAML files (phonopy)
1024
1021
  if (ext === `yaml` || ext === `yml`)
@@ -1028,31 +1025,31 @@ export function parse_structure_file(content, filename) {
1028
1025
  return parse_poscar(content);
1029
1026
  }
1030
1027
  }
1031
- // Try to auto-detect based on content
1028
+ // Try to auto-detect based on content.
1029
+ // JSON detection must come before the line-count guard: minified JSON
1030
+ // (e.g. fetched via extensionless blob: object URLs) is a single line.
1031
+ const content_start = content.trimStart();
1032
+ const looks_like_json = content_start.startsWith(`{`) || content_start.startsWith(`[`);
1033
+ try {
1034
+ const result = detect_json_structure(content);
1035
+ if (result)
1036
+ return result;
1037
+ if (looks_like_json)
1038
+ diag_error(`JSON content does not contain a valid structure format`);
1039
+ }
1040
+ catch (error) {
1041
+ // Only swallow silently when content doesn't even look like JSON; otherwise the
1042
+ // syntax error is the most useful failure reason and must be surfaced
1043
+ if (looks_like_json)
1044
+ diag_error(`Invalid JSON`, error);
1045
+ }
1032
1046
  const lines = content.trim().split(/\r?\n/);
1033
1047
  if (lines.length < 2) {
1034
- console.error(`File too short to determine format`);
1048
+ diag_error(`File too short to determine format`);
1035
1049
  return null;
1036
1050
  }
1037
- // JSON format detection: try to parse as JSON first
1038
- try {
1039
- const parsed = JSON.parse(content);
1040
- if (is_optimade_raw(parsed)) {
1041
- const result = parse_optimade_from_raw(parsed);
1042
- if (result)
1043
- return result;
1044
- }
1045
- // Otherwise try parsing as regular JSON structure
1046
- const structure = find_structure_in_json(parsed);
1047
- if (structure) {
1048
- return normalize_fractional_coords(structure);
1049
- }
1050
- }
1051
- catch {
1052
- // Not JSON, continue with other format detection
1053
- }
1054
1051
  // XYZ format detection: first line should be a number, second line is comment
1055
- const first_line_number = parseInt(lines[0].trim());
1052
+ const first_line_number = parseInt(lines[0].trim(), 10);
1056
1053
  if (!isNaN(first_line_number) && first_line_number > 0) {
1057
1054
  // Check if this looks like XYZ format
1058
1055
  if (lines.length >= first_line_number + 2) {
@@ -1066,7 +1063,7 @@ export function parse_structure_file(content, filename) {
1066
1063
  const coords = parts.slice(1, 4);
1067
1064
  // Check if first token looks like an element symbol (not a number)
1068
1065
  // and the next 3 tokens look like coordinates (numbers)
1069
- const is_element_symbol = isNaN(parseInt(first_token)) && first_token.length <= 3;
1066
+ const is_element_symbol = isNaN(parseInt(first_token, 10)) && first_token.length <= 3;
1070
1067
  const are_coordinates = coords.every((coord) => !isNaN(parseFloat(coord)));
1071
1068
  if (is_element_symbol && are_coordinates) {
1072
1069
  // First token is likely an element symbol, likely XYZ
@@ -1098,11 +1095,20 @@ export function parse_structure_file(content, filename) {
1098
1095
  line.includes(`phonon_supercell:`));
1099
1096
  if (has_phonopy_keywords)
1100
1097
  return parse_phonopy_yaml(content);
1101
- console.error(`Unable to determine file format`);
1098
+ diag_error(`Unable to determine file format`);
1102
1099
  return null;
1103
1100
  }
1104
- // Universal parser that handles JSON and structure files
1101
+ // Auto-detect file format and parse; throws an Error aggregating per-format failure reasons when nothing parses
1102
+ export function parse_structure_file(content, filename) {
1103
+ reset_parse_diagnostics();
1104
+ const structure = parse_structure_file_impl(content, filename);
1105
+ if (structure)
1106
+ return structure;
1107
+ throw aggregate_parse_error(filename);
1108
+ }
1109
+ // Universal parser for JSON and structure files; throws an Error aggregating per-format failure reasons when nothing parses
1105
1110
  export function parse_any_structure(content, filename) {
1111
+ reset_parse_diagnostics();
1106
1112
  const finalize_structure = (structure) => ({
1107
1113
  sites: structure.sites,
1108
1114
  charge: 0,
@@ -1113,23 +1119,21 @@ export function parse_any_structure(content, filename) {
1113
1119
  lattice: { ...structure.lattice, pbc: [true, true, true] },
1114
1120
  }),
1115
1121
  });
1116
- // Try JSON first, but handle nested structures properly
1122
+ // Fast path: content is already a serialized structure object
1117
1123
  try {
1118
1124
  const parsed = JSON.parse(content);
1119
- // Check if it's already a valid structure using proper type guard
1120
1125
  if (is_parsed_structure(parsed)) {
1121
1126
  // Normalize coordinates (wrap fractional to [0,1) and recompute Cartesian)
1122
1127
  return finalize_structure(normalize_fractional_coords(parsed));
1123
1128
  }
1124
- // If not, use parse_structure_file to find nested structures
1125
- const structure = parse_structure_file(content, filename);
1126
- return structure ? finalize_structure(structure) : null;
1127
1129
  }
1128
1130
  catch {
1129
- // Try structure file formats
1130
- const parsed = parse_structure_file(content, filename);
1131
- return parsed ? finalize_structure(parsed) : null;
1131
+ // Not plain JSON — fall through to format detection, which records failure reasons
1132
1132
  }
1133
+ const structure = parse_structure_file_impl(content, filename);
1134
+ if (structure)
1135
+ return finalize_structure(structure);
1136
+ throw aggregate_parse_error(filename);
1133
1137
  }
1134
1138
  // Parse OPTIMADE JSON format
1135
1139
  export function parse_optimade_json(content) {
@@ -1138,16 +1142,80 @@ export function parse_optimade_json(content) {
1138
1142
  return parse_optimade_from_raw(raw);
1139
1143
  }
1140
1144
  catch (error) {
1141
- console.error(`Error parsing OPTIMADE JSON:`, error);
1145
+ diag_error(`Error parsing OPTIMADE JSON`, error);
1142
1146
  return null;
1143
1147
  }
1144
1148
  }
1149
+ // Build sites + lattice shared by parse_optimade_from_raw and optimade_to_crystal.
1150
+ // on_invalid controls whether invalid positions are skipped with a warning or throw;
1151
+ // site_props extracts per-site mass/concentration from the species list.
1152
+ function build_optimade_sites(attrs, opts) {
1153
+ const positions = attrs.cartesian_site_positions ?? [];
1154
+ const species_at_sites = attrs.species_at_sites ?? [];
1155
+ const species_list = Array.isArray(attrs.species) ? attrs.species : undefined;
1156
+ // OPTIMADE stores lattice vectors as rows, so use as-is
1157
+ const lattice_matrix = attrs.lattice_vectors
1158
+ ? matrix3x3_from_rows(attrs.lattice_vectors, `OPTIMADE lattice vector`)
1159
+ : undefined;
1160
+ const lattice_params = lattice_matrix ? math.calc_lattice_params(lattice_matrix) : null;
1161
+ let cart_to_frac = null;
1162
+ if (lattice_matrix && lattice_params) {
1163
+ const converter = cart_to_frac_with_fallback(lattice_matrix, [
1164
+ lattice_params.a,
1165
+ lattice_params.b,
1166
+ lattice_params.c,
1167
+ ]);
1168
+ if (!converter.exact) {
1169
+ diag_warn(`Failed to create exact coordinate converter for OPTIMADE structure`);
1170
+ }
1171
+ cart_to_frac = converter.convert;
1172
+ }
1173
+ const sites = [];
1174
+ for (let idx = 0; idx < positions.length; idx++) {
1175
+ const species_name = species_at_sites[idx];
1176
+ if (!species_name) {
1177
+ if (opts.on_invalid === `throw`)
1178
+ throw new Error(`Missing species for site ${idx}`);
1179
+ diag_warn(`Missing species for site ${idx}, skipping`);
1180
+ continue;
1181
+ }
1182
+ let xyz;
1183
+ try {
1184
+ xyz = vec3_from_values(positions[idx], `OPTIMADE atom position ${idx + 1}`);
1185
+ }
1186
+ catch (error) {
1187
+ if (opts.on_invalid === `throw`)
1188
+ throw error;
1189
+ diag_warn(`Invalid position data at site ${idx}: ${error}`);
1190
+ continue;
1191
+ }
1192
+ const { symbol: element, sym_idx } = resolve_optimade_element(species_name, species_list, idx);
1193
+ // Calculate fractional coordinates if lattice is available
1194
+ const abc = cart_to_frac ? cart_to_frac(xyz) : [0, 0, 0];
1195
+ const site_props = {};
1196
+ if (opts.site_props) {
1197
+ // Extract mass/concentration for the chosen element. sym_idx indexes the (parallel)
1198
+ // chemical_symbols/mass/concentration arrays; -1 (name resolved directly, no
1199
+ // chemical_symbols) falls back to index 0 — the single-element entry.
1200
+ const spec = species_list?.find((entry) => entry.name === species_name);
1201
+ const spec_idx = Math.max(sym_idx, 0);
1202
+ if (spec?.mass?.[spec_idx] !== undefined)
1203
+ site_props.mass = spec.mass[spec_idx];
1204
+ if (spec?.concentration?.[spec_idx] !== undefined &&
1205
+ spec.concentration[spec_idx] !== 1) {
1206
+ site_props.concentration = spec.concentration[spec_idx];
1207
+ }
1208
+ }
1209
+ sites.push(make_site(element, abc, xyz, `${element}${idx + 1}`, site_props));
1210
+ }
1211
+ return { sites, lattice_matrix, lattice_params };
1212
+ }
1145
1213
  // Parse OPTIMADE from already-parsed JSON
1146
1214
  export function parse_optimade_from_raw(raw) {
1147
1215
  try {
1148
1216
  const structure = extract_optimade_structure_from_raw(raw);
1149
1217
  if (!structure) {
1150
- console.error(`No valid OPTIMADE structure found in JSON`);
1218
+ diag_error(`No valid OPTIMADE structure found in JSON`);
1151
1219
  return null;
1152
1220
  }
1153
1221
  const attrs = structure.attributes;
@@ -1155,82 +1223,28 @@ export function parse_optimade_from_raw(raw) {
1155
1223
  const positions_raw = attrs.cartesian_site_positions;
1156
1224
  const species_raw = attrs.species_at_sites;
1157
1225
  if (!(Array.isArray(positions_raw) && Array.isArray(species_raw))) {
1158
- console.error(`OPTIMADE JSON missing required position or species data`);
1226
+ diag_error(`OPTIMADE JSON missing required position or species data`);
1159
1227
  return null;
1160
1228
  }
1161
1229
  if (positions_raw.length !== species_raw.length) {
1162
- console.error(`OPTIMADE JSON position/species count mismatch`);
1230
+ diag_error(`OPTIMADE JSON position/species count mismatch`);
1163
1231
  return null;
1164
1232
  }
1165
- const positions = positions_raw;
1166
- const species = species_raw;
1167
- // Optimade stores lattice vectors as rows, so use as is
1168
- const lattice_matrix = attrs.lattice_vectors
1169
- ? [
1170
- vec3_from_values(attrs.lattice_vectors[0], `OPTIMADE lattice vector 1`),
1171
- vec3_from_values(attrs.lattice_vectors[1], `OPTIMADE lattice vector 2`),
1172
- vec3_from_values(attrs.lattice_vectors[2], `OPTIMADE lattice vector 3`),
1173
- ]
1174
- : undefined;
1175
- const optimade_lattice_params = lattice_matrix
1176
- ? math.calc_lattice_params(lattice_matrix)
1177
- : null;
1178
- // Parse atomic sites
1179
- const optimade_exact_cart_to_frac = lattice_matrix
1180
- ? try_create_cart_to_frac(lattice_matrix)
1181
- : null;
1182
- const optimade_cart_to_frac = lattice_matrix && optimade_lattice_params
1183
- ? (optimade_exact_cart_to_frac ??
1184
- ((xyz) => approximate_cart_to_frac(xyz, [
1185
- optimade_lattice_params.a,
1186
- optimade_lattice_params.b,
1187
- optimade_lattice_params.c,
1188
- ])))
1189
- : null;
1190
- if (lattice_matrix && !optimade_exact_cart_to_frac) {
1191
- console.warn(`Failed to create exact coordinate converter for OPTIMADE structure`);
1192
- }
1193
- const sites = [];
1194
- for (let idx = 0; idx < positions.length; idx++) {
1195
- const pos = positions[idx];
1196
- const element_symbol = species[idx];
1197
- let xyz;
1198
- try {
1199
- xyz = vec3_from_values(pos, `OPTIMADE site ${idx} position`);
1200
- }
1201
- catch (error) {
1202
- console.warn(`Invalid position data at site ${idx}: ${error}`);
1203
- continue;
1204
- }
1205
- const element = validate_element_symbol(element_symbol, idx);
1206
- // Calculate fractional coordinates if lattice is available
1207
- const abc = optimade_cart_to_frac ? optimade_cart_to_frac(xyz) : [0, 0, 0];
1208
- const site = {
1209
- species: [{ element, occu: 1, oxidation_state: 0 }],
1210
- abc,
1211
- xyz,
1212
- label: `${element}${idx + 1}`,
1213
- properties: {},
1214
- };
1215
- sites.push(site);
1216
- }
1233
+ const { sites, lattice_matrix, lattice_params } = build_optimade_sites(attrs, {
1234
+ on_invalid: `skip`,
1235
+ });
1217
1236
  if (sites.length === 0) {
1218
- console.error(`No valid sites found in OPTIMADE JSON`);
1237
+ diag_error(`No valid sites found in OPTIMADE JSON`);
1219
1238
  return null;
1220
1239
  }
1221
- // Create structure object
1222
- let lattice;
1223
- if (lattice_matrix && optimade_lattice_params) {
1224
- lattice = { matrix: lattice_matrix, ...optimade_lattice_params };
1225
- }
1226
- const structure_result = {
1240
+ return {
1227
1241
  sites,
1228
- ...(lattice && { lattice }),
1242
+ ...(lattice_matrix &&
1243
+ lattice_params && { lattice: { matrix: lattice_matrix, ...lattice_params } }),
1229
1244
  };
1230
- return structure_result;
1231
1245
  }
1232
1246
  catch (error) {
1233
- console.error(`Error parsing OPTIMADE JSON:`, error);
1247
+ diag_error(`Error parsing OPTIMADE JSON`, error);
1234
1248
  return null;
1235
1249
  }
1236
1250
  }
@@ -1252,9 +1266,7 @@ function extract_optimade_structure_from_raw(raw) {
1252
1266
  const candidate = Array.isArray(payload) ? payload[0] : payload;
1253
1267
  return is_optimade_structure_object(candidate) ? candidate : null;
1254
1268
  }
1255
- const unwrap_data = (value) => value && typeof value === `object` && `data` in value
1256
- ? value.data
1257
- : value;
1269
+ const unwrap_data = (value) => value && typeof value === `object` && `data` in value ? value.data : value;
1258
1270
  // Type guard: verify minimal OPTIMADE structure shape
1259
1271
  function is_optimade_structure_object(value) {
1260
1272
  if (!value || typeof value !== `object`)
@@ -1270,45 +1282,18 @@ function is_optimade_structure_object(value) {
1270
1282
  }
1271
1283
  // Convert OPTIMADE structure to Crystal format
1272
1284
  export function optimade_to_crystal(optimade_structure) {
1273
- const { lattice_vectors, cartesian_site_positions, species_at_sites, species, ...properties } = optimade_structure.attributes;
1285
+ const { lattice_vectors, cartesian_site_positions, species_at_sites, species: _species, // excluded from the properties rest
1286
+ ...properties } = optimade_structure.attributes;
1274
1287
  if (!lattice_vectors || !cartesian_site_positions || !species_at_sites) {
1275
- console.error(`Missing required OPTIMADE structure data`);
1288
+ diag_error(`Missing required OPTIMADE structure data`);
1276
1289
  return null;
1277
1290
  }
1278
1291
  try {
1279
- const lattice_matrix = [
1280
- vec3_from_values(lattice_vectors[0], `OPTIMADE lattice vector 1`),
1281
- vec3_from_values(lattice_vectors[1], `OPTIMADE lattice vector 2`),
1282
- vec3_from_values(lattice_vectors[2], `OPTIMADE lattice vector 3`),
1283
- ];
1284
- const lattice_params = math.calc_lattice_params(lattice_matrix);
1285
- // Build species lookup for site properties (mass, concentration, etc.)
1286
- const species_map = new Map(species?.map((spec) => [spec.name, spec]));
1287
- const crystal_cart_to_frac = try_create_cart_to_frac(lattice_matrix) ??
1288
- ((xyz) => approximate_cart_to_frac(xyz, [lattice_params.a, lattice_params.b, lattice_params.c]));
1289
- const sites = cartesian_site_positions.map((pos, idx) => {
1290
- const element_symbol = species_at_sites[idx];
1291
- if (!element_symbol)
1292
- throw new Error(`Missing species for site ${idx}`);
1293
- const element = validate_element_symbol(element_symbol, idx);
1294
- const xyz = vec3_from_values(pos, `OPTIMADE atom position ${idx + 1}`);
1295
- const abc = crystal_cart_to_frac ? crystal_cart_to_frac(xyz) : [0, 0, 0];
1296
- // Extract mass/concentration from species data
1297
- const spec = species_map.get(element_symbol);
1298
- const site_props = {};
1299
- if (spec?.mass?.[0] !== undefined)
1300
- site_props.mass = spec.mass[0];
1301
- if (spec?.concentration?.[0] !== undefined && spec.concentration[0] !== 1) {
1302
- site_props.concentration = spec.concentration[0];
1303
- }
1304
- return {
1305
- species: [{ element, occu: 1, oxidation_state: 0 }],
1306
- abc,
1307
- xyz,
1308
- label: `${element}${idx + 1}`,
1309
- properties: site_props,
1310
- };
1311
- });
1292
+ const { sites, lattice_matrix, lattice_params } = build_optimade_sites(optimade_structure.attributes, { on_invalid: `throw`, site_props: true });
1293
+ if (!lattice_matrix || !lattice_params) {
1294
+ diag_error(`Missing required OPTIMADE structure data`);
1295
+ return null;
1296
+ }
1312
1297
  return {
1313
1298
  sites,
1314
1299
  lattice: { matrix: lattice_matrix, ...lattice_params, pbc: [true, true, true] },
@@ -1317,7 +1302,7 @@ export function optimade_to_crystal(optimade_structure) {
1317
1302
  };
1318
1303
  }
1319
1304
  catch (err) {
1320
- console.error(`Error converting OPTIMADE to Crystal format:`, err);
1305
+ diag_error(`Error converting OPTIMADE to Crystal format`, err);
1321
1306
  return null;
1322
1307
  }
1323
1308
  }
@@ -1351,12 +1336,8 @@ export function is_structure_file(filename) {
1351
1336
  return false;
1352
1337
  }
1353
1338
  export const detect_structure_type = (filename, content) => {
1354
- const lower_filename = filename.toLowerCase();
1355
1339
  // Normalize compressed suffixes (gz, gzip, zip, xz, bz2) for detection parity
1356
- let name_to_check = lower_filename;
1357
- while (COMPRESSION_EXTENSIONS_REGEX.test(name_to_check)) {
1358
- name_to_check = name_to_check.replace(COMPRESSION_EXTENSIONS_REGEX, ``);
1359
- }
1340
+ const name_to_check = strip_compression_extensions(filename);
1360
1341
  if (name_to_check.endsWith(`.json`)) {
1361
1342
  try {
1362
1343
  const parsed = JSON.parse(content);