matterviz 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (499) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +48 -60
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/api/optimade.js +10 -7
  6. package/dist/app.css +68 -0
  7. package/dist/brillouin/BrillouinZone.svelte +90 -91
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
  9. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
  12. package/dist/brillouin/BrillouinZoneScene.svelte +46 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  14. package/dist/brillouin/compute.js +10 -16
  15. package/dist/brillouin/geometry.js +2 -2
  16. package/dist/chempot-diagram/ChemPotDiagram.svelte +48 -50
  17. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +61 -169
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +559 -755
  20. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
  21. package/dist/chempot-diagram/async-compute.svelte.js +13 -14
  22. package/dist/chempot-diagram/color.d.ts +21 -1
  23. package/dist/chempot-diagram/color.js +70 -1
  24. package/dist/chempot-diagram/compute.d.ts +7 -0
  25. package/dist/chempot-diagram/compute.js +85 -26
  26. package/dist/chempot-diagram/controls-state.svelte.js +1 -1
  27. package/dist/chempot-diagram/export.js +1 -6
  28. package/dist/chempot-diagram/temperature.js +9 -10
  29. package/dist/colors/index.d.ts +0 -4
  30. package/dist/colors/index.js +19 -20
  31. package/dist/composition/BarChart.svelte +30 -26
  32. package/dist/composition/BubbleChart.svelte +17 -17
  33. package/dist/composition/Composition.svelte +10 -19
  34. package/dist/composition/Formula.svelte +20 -27
  35. package/dist/composition/FormulaFilter.svelte +115 -184
  36. package/dist/composition/PieChart.svelte +55 -41
  37. package/dist/composition/format.js +7 -9
  38. package/dist/composition/index.d.ts +2 -0
  39. package/dist/composition/index.js +14 -0
  40. package/dist/composition/parse.d.ts +2 -2
  41. package/dist/composition/parse.js +61 -115
  42. package/dist/constants.d.ts +0 -1
  43. package/dist/constants.js +17 -26
  44. package/dist/convex-hull/ConvexHull.svelte +12 -15
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +93 -94
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +127 -186
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +131 -180
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
  53. package/dist/convex-hull/ConvexHullControls.svelte +46 -48
  54. package/dist/convex-hull/ConvexHullStats.svelte +193 -195
  55. package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
  56. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  57. package/dist/convex-hull/StructurePopup.svelte +18 -22
  58. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  59. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  60. package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
  61. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  62. package/dist/convex-hull/helpers.d.ts +8 -10
  63. package/dist/convex-hull/helpers.js +37 -43
  64. package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
  65. package/dist/convex-hull/thermodynamics.js +37 -44
  66. package/dist/coordination/CoordinationBarPlot.svelte +49 -53
  67. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  68. package/dist/coordination/calc-coordination.d.ts +1 -1
  69. package/dist/coordination/calc-coordination.js +28 -25
  70. package/dist/element/BohrAtom.svelte +5 -9
  71. package/dist/element/ElementHeading.svelte +4 -1
  72. package/dist/element/ElementPhoto.svelte +11 -9
  73. package/dist/element/ElementStats.svelte +12 -10
  74. package/dist/element/ElementTile.svelte +44 -67
  75. package/dist/element/Nucleus.svelte +4 -2
  76. package/dist/element/data.d.ts +3 -2
  77. package/dist/element/data.js +5 -1
  78. package/dist/element/helpers.d.ts +4 -0
  79. package/dist/element/helpers.js +18 -0
  80. package/dist/element/index.d.ts +2 -5
  81. package/dist/element/index.js +4 -19
  82. package/dist/feedback/DragOverlay.svelte +5 -5
  83. package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
  84. package/dist/feedback/StatusMessage.svelte +7 -4
  85. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  86. package/dist/fermi-surface/FermiSurface.svelte +35 -44
  87. package/dist/fermi-surface/FermiSurface.svelte.d.ts +7 -13
  88. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  89. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  90. package/dist/fermi-surface/FermiSurfaceScene.svelte +64 -136
  91. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  92. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  93. package/dist/fermi-surface/compute.js +25 -58
  94. package/dist/fermi-surface/parse.js +54 -88
  95. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  96. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  97. package/dist/file-viewer/PlotPanel.svelte +350 -0
  98. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  99. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  100. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  101. package/dist/file-viewer/detect.d.ts +11 -0
  102. package/dist/file-viewer/detect.js +381 -0
  103. package/dist/file-viewer/eligibility.d.ts +4 -0
  104. package/dist/file-viewer/eligibility.js +53 -0
  105. package/dist/file-viewer/host-protocol.d.ts +50 -0
  106. package/dist/file-viewer/host-transfer.d.ts +25 -0
  107. package/dist/file-viewer/host-transfer.js +42 -0
  108. package/dist/file-viewer/index.d.ts +5 -0
  109. package/dist/file-viewer/index.js +5 -0
  110. package/dist/file-viewer/main.d.ts +44 -0
  111. package/dist/file-viewer/main.js +535 -0
  112. package/dist/file-viewer/parse.d.ts +9 -0
  113. package/dist/file-viewer/parse.js +170 -0
  114. package/dist/file-viewer/plot-utils.d.ts +24 -0
  115. package/dist/file-viewer/plot-utils.js +191 -0
  116. package/dist/file-viewer/types.d.ts +4 -0
  117. package/dist/file-viewer/types.js +5 -0
  118. package/dist/heatmap-matrix/HeatmapMatrix.svelte +153 -219
  119. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
  120. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
  121. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  122. package/dist/heatmap-matrix/index.d.ts +5 -0
  123. package/dist/icons.d.ts +4 -0
  124. package/dist/icons.js +4 -0
  125. package/dist/index.d.ts +2 -2
  126. package/dist/index.js +1 -1
  127. package/dist/io/ExportPane.svelte +11 -7
  128. package/dist/io/decompress.d.ts +4 -1
  129. package/dist/io/decompress.js +24 -36
  130. package/dist/io/export.d.ts +11 -4
  131. package/dist/io/export.js +75 -19
  132. package/dist/io/fetch.d.ts +0 -3
  133. package/dist/io/fetch.js +0 -11
  134. package/dist/io/file-drop.js +60 -35
  135. package/dist/io/index.d.ts +0 -1
  136. package/dist/io/index.js +2 -9
  137. package/dist/io/is-binary.d.ts +9 -0
  138. package/dist/io/is-binary.js +45 -1
  139. package/dist/io/url-drop.d.ts +2 -0
  140. package/dist/io/url-drop.js +40 -40
  141. package/dist/isosurface/Isosurface.svelte +700 -215
  142. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  143. package/dist/isosurface/IsosurfaceControls.svelte +590 -116
  144. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  145. package/dist/isosurface/VolumeSlice.svelte +224 -0
  146. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  147. package/dist/isosurface/coloring.d.ts +20 -0
  148. package/dist/isosurface/coloring.js +118 -0
  149. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  150. package/dist/isosurface/geometry.worker.d.ts +1 -0
  151. package/dist/isosurface/geometry.worker.js +59 -0
  152. package/dist/isosurface/grid.d.ts +10 -0
  153. package/dist/isosurface/grid.js +31 -0
  154. package/dist/isosurface/index.d.ts +4 -0
  155. package/dist/isosurface/index.js +4 -0
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +6 -6
  159. package/dist/isosurface/profile.d.ts +12 -0
  160. package/dist/isosurface/profile.js +19 -0
  161. package/dist/isosurface/sampling.d.ts +34 -0
  162. package/dist/isosurface/sampling.js +408 -0
  163. package/dist/isosurface/slice-rendering.d.ts +13 -0
  164. package/dist/isosurface/slice-rendering.js +79 -0
  165. package/dist/isosurface/slice.d.ts +22 -2
  166. package/dist/isosurface/slice.js +200 -116
  167. package/dist/isosurface/types.d.ts +24 -1
  168. package/dist/isosurface/types.js +120 -8
  169. package/dist/json-path.d.ts +4 -0
  170. package/dist/json-path.js +89 -0
  171. package/dist/labels.d.ts +6 -2
  172. package/dist/labels.js +44 -14
  173. package/dist/layout/FullscreenButton.svelte +2 -1
  174. package/dist/layout/FullscreenToggle.svelte +10 -4
  175. package/dist/layout/InfoCard.svelte +8 -21
  176. package/dist/layout/InfoTag.svelte +1 -3
  177. package/dist/layout/NumberRangeInput.svelte +46 -0
  178. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  179. package/dist/layout/PropertyFilter.svelte +2 -6
  180. package/dist/layout/SettingsSection.svelte +57 -62
  181. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  182. package/dist/layout/SubpageGrid.svelte +3 -1
  183. package/dist/layout/ViewerChrome.svelte +16 -11
  184. package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
  185. package/dist/layout/fullscreen.js +3 -5
  186. package/dist/layout/fullscreen.svelte.js +1 -2
  187. package/dist/layout/index.d.ts +1 -0
  188. package/dist/layout/index.js +1 -0
  189. package/dist/layout/json-tree/JsonNode.svelte +22 -59
  190. package/dist/layout/json-tree/JsonTree.svelte +101 -172
  191. package/dist/layout/json-tree/JsonValue.svelte +9 -32
  192. package/dist/layout/json-tree/utils.d.ts +0 -3
  193. package/dist/layout/json-tree/utils.js +50 -188
  194. package/dist/marching-cubes.d.ts +8 -1
  195. package/dist/marching-cubes.js +115 -48
  196. package/dist/math.d.ts +5 -2
  197. package/dist/math.js +48 -38
  198. package/dist/overlays/ContextMenu.svelte +19 -34
  199. package/dist/overlays/CopyButton.svelte +11 -5
  200. package/dist/overlays/DragControlTab.svelte +6 -1
  201. package/dist/overlays/DraggablePane.svelte +110 -74
  202. package/dist/overlays/DraggablePane.svelte.d.ts +2 -1
  203. package/dist/overlays/GlassChip.svelte +32 -0
  204. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  205. package/dist/overlays/InfoPaneCards.svelte +11 -23
  206. package/dist/overlays/index.d.ts +7 -0
  207. package/dist/overlays/index.js +26 -0
  208. package/dist/overlays/portal.d.ts +2 -0
  209. package/dist/overlays/portal.js +14 -0
  210. package/dist/periodic-table/PeriodicTable.svelte +113 -145
  211. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  212. package/dist/periodic-table/PeriodicTableControls.svelte +37 -115
  213. package/dist/periodic-table/TableInset.svelte +9 -4
  214. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +68 -105
  215. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  216. package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
  217. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  218. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  219. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
  220. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  221. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  222. package/dist/phase-diagram/build-diagram.js +4 -9
  223. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  224. package/dist/phase-diagram/parse.js +15 -15
  225. package/dist/phase-diagram/svg-to-diagram.js +28 -27
  226. package/dist/phase-diagram/utils.js +2 -3
  227. package/dist/plot/bar/BarPlot.svelte +461 -426
  228. package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
  229. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  230. package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
  231. package/dist/plot/bar/SpacegroupBarPlot.svelte +46 -77
  232. package/dist/plot/bar/data.d.ts +5 -4
  233. package/dist/plot/bar/data.js +2 -1
  234. package/dist/plot/box/BoxPlot.svelte +322 -226
  235. package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
  236. package/dist/plot/box/Violin.svelte.d.ts +1 -0
  237. package/dist/plot/box/box-plot.js +1 -1
  238. package/dist/plot/core/auto-place.d.ts +1 -1
  239. package/dist/plot/core/auto-place.js +2 -2
  240. package/dist/plot/core/axis-utils.d.ts +12 -7
  241. package/dist/plot/core/axis-utils.js +48 -48
  242. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  243. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  244. package/dist/plot/core/components/ColorBar.svelte +101 -106
  245. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  246. package/dist/plot/core/components/FillArea.svelte +10 -17
  247. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  248. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  249. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  250. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  251. package/dist/plot/core/components/Line.svelte +18 -9
  252. package/dist/plot/core/components/Line.svelte.d.ts +2 -0
  253. package/dist/plot/core/components/PlotAxis.svelte +40 -17
  254. package/dist/plot/core/components/PlotControls.svelte +65 -41
  255. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  256. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  257. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  258. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  259. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  260. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  261. package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
  262. package/dist/plot/core/components/ReferencePlane.svelte +7 -8
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +27 -474
  271. package/dist/plot/core/data-transform.d.ts +1 -2
  272. package/dist/plot/core/data-transform.js +9 -15
  273. package/dist/plot/core/fill-utils.d.ts +3 -1
  274. package/dist/plot/core/fill-utils.js +28 -6
  275. package/dist/plot/core/index.d.ts +1 -0
  276. package/dist/plot/core/index.js +1 -0
  277. package/dist/plot/core/interactions.js +0 -2
  278. package/dist/plot/core/layout.d.ts +15 -6
  279. package/dist/plot/core/layout.js +57 -38
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  283. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  284. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -21
  286. package/dist/plot/core/reference-line.js +1 -1
  287. package/dist/plot/core/scales.d.ts +23 -0
  288. package/dist/plot/core/scales.js +39 -13
  289. package/dist/plot/core/svg.js +0 -1
  290. package/dist/plot/core/types/fills.d.ts +101 -0
  291. package/dist/plot/core/types/fills.js +15 -0
  292. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  293. package/dist/plot/core/types/plot-3d.js +4 -0
  294. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  295. package/dist/plot/core/types/reference-lines.js +9 -0
  296. package/dist/plot/core/types.d.ts +29 -324
  297. package/dist/plot/core/types.js +23 -20
  298. package/dist/plot/core/utils/hierarchy-chart.d.ts +52 -0
  299. package/dist/plot/core/utils/hierarchy-chart.js +191 -0
  300. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  301. package/dist/plot/core/utils/hierarchy-labels.js +100 -0
  302. package/dist/plot/core/utils/label-placement.js +2 -2
  303. package/dist/plot/histogram/Histogram.svelte +314 -340
  304. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  305. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  306. package/dist/plot/histogram/histogram.d.ts +31 -0
  307. package/dist/plot/histogram/histogram.js +70 -0
  308. package/dist/plot/histogram/index.d.ts +1 -0
  309. package/dist/plot/histogram/index.js +1 -0
  310. package/dist/plot/index.d.ts +1 -0
  311. package/dist/plot/index.js +1 -0
  312. package/dist/plot/sankey/Sankey.svelte +78 -69
  313. package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
  314. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  315. package/dist/plot/sankey/sankey.js +13 -9
  316. package/dist/plot/scatter/BinnedScatterPlot.svelte +311 -141
  317. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
  318. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  319. package/dist/plot/scatter/ScatterPlot.svelte +517 -410
  320. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
  321. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  322. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  323. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  324. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  325. package/dist/plot/scatter/adaptive-density.js +36 -20
  326. package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
  327. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  328. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  329. package/dist/plot/scatter/scatter-data.js +8 -7
  330. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
  331. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
  332. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
  333. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
  334. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
  335. package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
  336. package/dist/plot/sunburst/Sunburst.svelte +269 -347
  337. package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
  338. package/dist/plot/sunburst/index.d.ts +1 -1
  339. package/dist/plot/sunburst/index.js +2 -1
  340. package/dist/plot/sunburst/render.d.ts +2 -2
  341. package/dist/plot/sunburst/render.js +47 -22
  342. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  343. package/dist/plot/sunburst/sunburst.js +10 -3
  344. package/dist/plot/treemap/Treemap.svelte +1022 -0
  345. package/dist/plot/treemap/Treemap.svelte.d.ts +90 -0
  346. package/dist/plot/treemap/index.d.ts +4 -0
  347. package/dist/plot/treemap/index.js +4 -0
  348. package/dist/plot/treemap/labels.d.ts +38 -0
  349. package/dist/plot/treemap/labels.js +80 -0
  350. package/dist/plot/treemap/treemap.d.ts +28 -0
  351. package/dist/plot/treemap/treemap.js +102 -0
  352. package/dist/rdf/RdfPlot.svelte +37 -59
  353. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  354. package/dist/rdf/calc-rdf.js +63 -52
  355. package/dist/rdf/index.d.ts +0 -1
  356. package/dist/sanitize.js +85 -27
  357. package/dist/scene/SceneCamera.svelte +1 -1
  358. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  359. package/dist/scene/bind-renderer.svelte.js +14 -6
  360. package/dist/scene/index.d.ts +1 -2
  361. package/dist/scene/index.js +1 -1
  362. package/dist/scene/{props.js → props.svelte.js} +15 -1
  363. package/dist/settings.d.ts +19 -9
  364. package/dist/settings.js +229 -525
  365. package/dist/spectral/Bands.svelte +207 -221
  366. package/dist/spectral/BandsAndDos.svelte +11 -9
  367. package/dist/spectral/BrillouinBandsDos.svelte +19 -42
  368. package/dist/spectral/Dos.svelte +92 -93
  369. package/dist/spectral/helpers.d.ts +3 -3
  370. package/dist/spectral/helpers.js +65 -140
  371. package/dist/state.svelte.js +5 -18
  372. package/dist/structure/Arrow.svelte +5 -18
  373. package/dist/structure/ArrowInstances.svelte +160 -0
  374. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  375. package/dist/structure/AtomLegend.svelte +120 -85
  376. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  377. package/dist/structure/Bond.svelte +31 -23
  378. package/dist/structure/CanvasTooltip.svelte +15 -3
  379. package/dist/structure/CellSelect.svelte +45 -35
  380. package/dist/structure/Cylinder.svelte +24 -40
  381. package/dist/structure/InstancedAtoms.svelte +114 -0
  382. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  383. package/dist/structure/Lattice.svelte +85 -149
  384. package/dist/structure/SiteLabels.svelte +122 -0
  385. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  386. package/dist/structure/Structure.svelte +636 -504
  387. package/dist/structure/Structure.svelte.d.ts +4 -2
  388. package/dist/structure/StructureCarousel.svelte +637 -0
  389. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  390. package/dist/structure/StructureControls.svelte +165 -425
  391. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  392. package/dist/structure/StructureExportPane.svelte +17 -21
  393. package/dist/structure/StructureInfoPane.svelte +128 -133
  394. package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
  395. package/dist/structure/StructureScene.svelte +535 -578
  396. package/dist/structure/StructureScene.svelte.d.ts +6 -2
  397. package/dist/structure/StructureViewport.svelte +451 -0
  398. package/dist/structure/StructureViewport.svelte.d.ts +65 -0
  399. package/dist/structure/atom-label-placement.d.ts +16 -12
  400. package/dist/structure/atom-label-placement.js +70 -37
  401. package/dist/structure/atom-properties.d.ts +2 -0
  402. package/dist/structure/atom-properties.js +105 -38
  403. package/dist/structure/bond-order-perception.js +3 -8
  404. package/dist/structure/bonding.d.ts +9 -3
  405. package/dist/structure/bonding.js +95 -46
  406. package/dist/structure/edit-history.d.ts +6 -0
  407. package/dist/structure/edit-history.js +27 -0
  408. package/dist/structure/export.d.ts +1 -0
  409. package/dist/structure/export.js +89 -105
  410. package/dist/structure/format-detect.d.ts +1 -0
  411. package/dist/structure/format-detect.js +20 -0
  412. package/dist/structure/geometry.d.ts +9 -0
  413. package/dist/structure/geometry.js +27 -0
  414. package/dist/structure/index.d.ts +16 -1
  415. package/dist/structure/index.js +15 -10
  416. package/dist/structure/measure.d.ts +1 -1
  417. package/dist/structure/measure.js +6 -12
  418. package/dist/structure/parse.d.ts +2 -1
  419. package/dist/structure/parse.js +272 -313
  420. package/dist/structure/partial-occupancy.js +10 -5
  421. package/dist/structure/pbc.js +85 -107
  422. package/dist/structure/polyhedra.d.ts +0 -1
  423. package/dist/structure/polyhedra.js +7 -21
  424. package/dist/structure/prop-groups.d.ts +85 -0
  425. package/dist/structure/prop-groups.js +18 -0
  426. package/dist/structure/supercell.d.ts +1 -2
  427. package/dist/structure/supercell.js +7 -13
  428. package/dist/symmetry/SymmetryElements.svelte +11 -15
  429. package/dist/symmetry/SymmetryStats.svelte +55 -71
  430. package/dist/symmetry/WyckoffTable.svelte +10 -28
  431. package/dist/symmetry/cell-transform.js +6 -14
  432. package/dist/symmetry/index.d.ts +1 -0
  433. package/dist/symmetry/index.js +13 -10
  434. package/dist/symmetry/spacegroups.js +3 -2
  435. package/dist/table/HeatmapTable.svelte +1142 -447
  436. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  437. package/dist/table/ToggleMenu.svelte +168 -53
  438. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  439. package/dist/table/index.d.ts +21 -6
  440. package/dist/table/index.js +34 -21
  441. package/dist/theme/ThemeControl.svelte +16 -8
  442. package/dist/theme/embedded.d.ts +18 -0
  443. package/dist/theme/embedded.js +206 -0
  444. package/dist/theme/index.d.ts +5 -5
  445. package/dist/theme/index.js +5 -12
  446. package/dist/theme/themes.mjs +44 -66
  447. package/dist/tooltip/KCoords.svelte +4 -1
  448. package/dist/tooltip/TooltipContent.svelte +3 -6
  449. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  450. package/dist/tooltip/index.d.ts +8 -1
  451. package/dist/trajectory/Trajectory.svelte +613 -386
  452. package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
  453. package/dist/trajectory/TrajectoryError.svelte +12 -74
  454. package/dist/trajectory/TrajectoryExportPane.svelte +14 -32
  455. package/dist/trajectory/TrajectoryInfoPane.svelte +127 -164
  456. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  457. package/dist/trajectory/extract.js +61 -49
  458. package/dist/trajectory/format-detect.d.ts +2 -0
  459. package/dist/trajectory/format-detect.js +17 -13
  460. package/dist/trajectory/frame-reader.d.ts +2 -0
  461. package/dist/trajectory/frame-reader.js +35 -32
  462. package/dist/trajectory/helpers.d.ts +2 -0
  463. package/dist/trajectory/helpers.js +34 -56
  464. package/dist/trajectory/index.d.ts +1 -0
  465. package/dist/trajectory/index.js +5 -8
  466. package/dist/trajectory/parse/ase.js +1 -1
  467. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  468. package/dist/trajectory/parse/h5-utils.js +109 -0
  469. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  470. package/dist/trajectory/parse/hdf5.js +116 -106
  471. package/dist/trajectory/parse/index.d.ts +6 -6
  472. package/dist/trajectory/parse/index.js +25 -33
  473. package/dist/trajectory/parse/lammps.js +8 -12
  474. package/dist/trajectory/parse/vasp.js +10 -7
  475. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  476. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  477. package/dist/trajectory/parse/vaspout-h5.d.ts +4 -0
  478. package/dist/trajectory/parse/vaspout-h5.js +281 -0
  479. package/dist/trajectory/parse/xyz.js +59 -9
  480. package/dist/trajectory/plotting.d.ts +8 -9
  481. package/dist/trajectory/plotting.js +113 -64
  482. package/dist/utils.d.ts +3 -0
  483. package/dist/utils.js +7 -0
  484. package/dist/xrd/XrdPlot.svelte +79 -118
  485. package/dist/xrd/broadening.js +2 -1
  486. package/dist/xrd/calc-xrd.js +24 -62
  487. package/dist/xrd/parse.d.ts +0 -1
  488. package/dist/xrd/parse.js +69 -95
  489. package/package.json +94 -34
  490. package/readme.md +6 -5
  491. package/dist/api/mp.d.ts +0 -6
  492. package/dist/api/mp.js +0 -22
  493. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  494. package/dist/scene/types.d.ts +0 -26
  495. package/dist/tooltip/types.d.ts +0 -8
  496. package/dist/trajectory/constants.d.ts +0 -6
  497. package/dist/trajectory/constants.js +0 -11
  498. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  499. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -1,12 +1,13 @@
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 { FALLBACK_ELEMENTS, is_elem_symbol } from '../element';
3
- import { strip_compression_extensions } from '../io';
1
+ import { XYZ_EXTXYZ_REGEX } from '../constants';
2
+ import { FALLBACK_ELEMENTS, is_elem_symbol } from '../element/helpers';
3
+ import { strip_compression_extensions } from '../io/decompress';
4
4
  import * as math from '../math';
5
5
  import { wrap_to_unit_cell } from './pbc';
6
6
  import { make_site } from './site';
7
- import { iter_xyz_frames } from '../trajectory/helpers';
8
- import { normalize_scientific_notation, to_error } from '../utils';
7
+ import { is_xyz_atom_line, iter_xyz_frames } from '../trajectory/helpers';
8
+ import { normalize_scientific_notation, parse_leading_num, parse_num_token, to_error, } from '../utils';
9
9
  import { load as yaml_load } from 'js-yaml';
10
+ export { is_structure_file } from './format-detect';
10
11
  // === Parse error contract ===
11
12
  // Individual format parsers (parse_poscar, parse_cif, parse_xyz, parse_phonopy_yaml,
12
13
  // parse_optimade_json, ...) return `T | null` on failure and record failure reasons in a
@@ -36,27 +37,48 @@ const aggregate_parse_error = (filename) => {
36
37
  return new Error(`Failed to parse structure${filename ? ` from '${filename}'` : ``}${detail}`);
37
38
  };
38
39
  const cif_coords_key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
39
- const cif_site_key = (element, abc, label) => `${element}|${label}|${cif_coords_key(abc)}`;
40
- const clone_structure_properties = (properties) => structuredClone(properties);
40
+ // Bravais lattice centering translations (excluding the identity) keyed by the
41
+ // leading letter of a space-group Hermann-Mauguin symbol. R is the obverse
42
+ // hexagonal setting.
43
+ const CENTERING_VECTORS = {
44
+ P: [],
45
+ I: [[0.5, 0.5, 0.5]],
46
+ F: [
47
+ [0, 0.5, 0.5],
48
+ [0.5, 0, 0.5],
49
+ [0.5, 0.5, 0],
50
+ ],
51
+ A: [[0, 0.5, 0.5]],
52
+ B: [[0.5, 0, 0.5]],
53
+ C: [[0.5, 0.5, 0]],
54
+ R: [
55
+ [2 / 3, 1 / 3, 1 / 3],
56
+ [1 / 3, 2 / 3, 2 / 3],
57
+ ],
58
+ };
59
+ // Detect the centering letter from a CIF's space-group H-M symbol, if present.
60
+ const extract_cif_centering = (text) => {
61
+ for (const line of text.split(`\n`)) {
62
+ const match = /^_(?:symmetry_space_group_name_h-m|space_group_name_h-m(?:_alt)?)\s+(?<symbol>.+)/i.exec(line.trim());
63
+ const letter = match?.groups?.symbol.replaceAll(/['"]/g, ``).trim()[0]?.toUpperCase();
64
+ if (letter && letter in CENTERING_VECTORS)
65
+ return letter;
66
+ }
67
+ return null;
68
+ };
41
69
  const vec3_from_values = (values, context) => {
42
70
  if (values?.length !== 3) {
43
71
  throw new Error(`Invalid ${context}: expected 3 coordinates, got ${values?.length ?? 0}`);
44
72
  }
45
73
  const coords = math.finite_vec3_from_values(values);
46
- if (coords)
47
- return coords;
48
- for (let idx = 0; idx < 3; idx++) {
49
- const value = values[idx];
50
- if (typeof value !== `number` || !Number.isFinite(value)) {
51
- throw new TypeError(`Invalid ${context}: coordinate ${idx} must be finite, got ${String(value)}`);
52
- }
53
- }
54
- throw new Error(`Invalid ${context}: expected 3 finite coordinates`);
74
+ if (!coords)
75
+ throw new Error(`Invalid ${context}: expected 3 finite coordinates, got [${values.map(String).join(`, `)}]`);
76
+ return coords;
55
77
  };
56
78
  // Parse a coordinate value that might be in various scientific notation formats
57
79
  function parse_coordinate(str) {
58
80
  const normalized = normalize_scientific_notation(str.trim());
59
- const value = parseFloat(normalized);
81
+ const value = Number(normalized);
60
82
  if (isNaN(value))
61
83
  throw new Error(`Invalid coordinate value: ${str}`);
62
84
  return value;
@@ -70,9 +92,9 @@ function parse_coordinate_line(line) {
70
92
  const sanitized = line
71
93
  .trim()
72
94
  // Add space when '-' follows a digit and precedes a digit or dot
73
- .replaceAll(/(\d)-(?=[\d.])/g, `$1 -`)
95
+ .replaceAll(/(?<digit>\d)-(?=[\d.])/g, `$1 -`)
74
96
  // Revert accidental spaces after exponent markers
75
- .replaceAll(/([eE])\s-\s/g, `$1-`);
97
+ .replaceAll(/(?<exp_marker>[eE])\s-\s/g, `$1-`);
76
98
  tokens = sanitized.split(/\s+/);
77
99
  }
78
100
  if (tokens.length < 3)
@@ -114,14 +136,6 @@ function resolve_optimade_element(species_name, species_list, index) {
114
136
  return { symbol: stripped, sym_idx: -1 };
115
137
  return { symbol: validate_element_symbol(species_name, index), sym_idx: -1 };
116
138
  }
117
- const try_create_cart_to_frac = (lattice_matrix) => {
118
- try {
119
- return math.create_cart_to_frac(lattice_matrix);
120
- }
121
- catch {
122
- return null;
123
- }
124
- };
125
139
  const approximate_cart_to_frac = (xyz, axis_lengths) => [
126
140
  Math.abs(axis_lengths[0]) > math.EPS ? xyz[0] / axis_lengths[0] : 0,
127
141
  Math.abs(axis_lengths[1]) > math.EPS ? xyz[1] / axis_lengths[1] : 0,
@@ -136,9 +150,12 @@ const matrix3x3_from_rows = (rows, context) => [
136
150
  // cart→frac converter that falls back to per-axis-length division for singular lattices.
137
151
  // axis_lengths defaults to the row norms of the lattice matrix.
138
152
  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 };
153
+ try {
154
+ return { convert: math.create_cart_to_frac(matrix), exact: true };
155
+ }
156
+ catch {
157
+ // fall through to the per-axis-length approximation below
158
+ }
142
159
  const lengths = axis_lengths ?? [
143
160
  Math.hypot(...matrix[0]),
144
161
  Math.hypot(...matrix[1]),
@@ -193,38 +210,24 @@ export function parse_poscar(content) {
193
210
  let atom_counts = [];
194
211
  // Detect if this is VASP 5+ format (has element symbols)
195
212
  // Try to parse the first token as a number - if it succeeds, it's VASP 4 format
196
- const first_token = lines[line_index].trim().split(/\s+/)[0];
197
- const first_token_as_number = parseInt(first_token, 10);
198
- const has_element_symbols = isNaN(first_token_as_number);
213
+ const has_element_symbols = isNaN(parse_leading_num(lines[line_index]));
199
214
  if (has_element_symbols) {
200
- // VASP 5+ format - parse element symbols (may span multiple lines)
215
+ // VASP 5+ format - element symbols (possibly spanning multiple lines),
216
+ // followed by as many atom-count lines. Look ahead to find where numbers start.
201
217
  let symbol_lines = 1;
202
- // Look ahead to find where numbers start (atom counts)
203
218
  for (let lookahead_idx = 1; lookahead_idx < 10; lookahead_idx++) {
204
219
  if (line_index + lookahead_idx >= lines.length)
205
220
  break;
206
- const next_line_first_token = lines[line_index + lookahead_idx].trim().split(/\s+/)[0];
207
- const next_token_as_number = parseInt(next_line_first_token, 10);
208
- if (!isNaN(next_token_as_number)) {
221
+ if (!isNaN(parse_leading_num(lines[line_index + lookahead_idx]))) {
209
222
  symbol_lines = lookahead_idx;
210
223
  break;
211
224
  }
212
225
  }
213
- // Collect all element symbols from the symbol lines
214
- for (let symbol_line_idx = 0; symbol_line_idx < symbol_lines; symbol_line_idx++) {
215
- if (line_index + symbol_line_idx < lines.length) {
216
- element_symbols.push(...lines[line_index + symbol_line_idx].trim().split(/\s+/));
217
- }
218
- }
219
- // Parse atom counts (may span multiple lines)
220
- for (let count_line_idx = 0; count_line_idx < symbol_lines; count_line_idx++) {
221
- if (line_index + symbol_lines + count_line_idx < lines.length) {
222
- const counts = lines[line_index + symbol_lines + count_line_idx]
223
- .trim()
224
- .split(/\s+/)
225
- .map(Number);
226
- atom_counts.push(...counts);
227
- }
226
+ for (let offset = 0; offset < symbol_lines; offset++) {
227
+ const symbol_tokens = lines[line_index + offset]?.trim().split(/\s+/) ?? [];
228
+ element_symbols.push(...symbol_tokens);
229
+ const count_tokens = lines[line_index + symbol_lines + offset]?.trim().split(/\s+/);
230
+ atom_counts.push(...(count_tokens?.map(Number) ?? []));
228
231
  }
229
232
  line_index += 2 * symbol_lines;
230
233
  }
@@ -329,8 +332,9 @@ export function parse_xyz(content) {
329
332
  diag_error(`XYZ frame too short`);
330
333
  return null;
331
334
  }
332
- // Parse number of atoms (line 1)
333
- const num_atoms = parseInt(lines[0].trim(), 10);
335
+ // Parse number of atoms (line 1). Only the first token counts: Tinker-style
336
+ // XYZ files put a title after the count (e.g. `6 methane`)
337
+ const num_atoms = Math.trunc(parse_leading_num(lines[0]));
334
338
  if (isNaN(num_atoms) || num_atoms <= 0) {
335
339
  diag_error(`Invalid number of atoms in XYZ file`);
336
340
  return null;
@@ -339,7 +343,7 @@ export function parse_xyz(content) {
339
343
  const comment_line = lines[1];
340
344
  let lattice;
341
345
  // Check for extended XYZ lattice information in comment line
342
- const lattice_match = /Lattice="([^"]+)"/.exec(comment_line);
346
+ const lattice_match = /Lattice="(?<lattice>[^"]+)"/.exec(comment_line);
343
347
  if (lattice_match) {
344
348
  const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
345
349
  if (lattice_values.length === 9) {
@@ -372,17 +376,14 @@ export function parse_xyz(content) {
372
376
  return null;
373
377
  }
374
378
  const element = validate_element_symbol(parts[0], atom_idx);
375
- const xyz = vec3_from_values(parts.slice(1, 4).map(parse_coordinate), `XYZ atom position ${atom_idx + 1}`);
379
+ let xyz = vec3_from_values(parts.slice(1, 4).map(parse_coordinate), `XYZ atom position ${atom_idx + 1}`);
376
380
  // Calculate fractional coordinates if lattice is available
377
381
  let abc = [0, 0, 0];
378
382
  if (lattice && xyz_frac_to_cart && xyz_cart_to_frac) {
379
- // Ensure fractional coordinates are wrapped into [0, 1) for consistency
383
+ // Wrap fractional coordinates into [0, 1) and recompute xyz from them so
384
+ // rendered atoms stay inside the primary unit cell
380
385
  abc = wrap_to_unit_cell(xyz_cart_to_frac(xyz));
381
- // Keep rendered atoms inside primary unit cell by recomputing xyz
382
- const wrapped_xyz = xyz_frac_to_cart(abc);
383
- xyz[0] = wrapped_xyz[0];
384
- xyz[1] = wrapped_xyz[1];
385
- xyz[2] = wrapped_xyz[2];
386
+ xyz = xyz_frac_to_cart(abc);
386
387
  }
387
388
  sites.push(make_site(element, abc, xyz, `${element}${atom_idx + 1}`));
388
389
  }
@@ -399,114 +400,78 @@ export function parse_xyz(content) {
399
400
  const parse_symmetry_expression = (expr_input) => {
400
401
  const coefficients = [0, 0, 0];
401
402
  let translation = 0;
402
- // Remove all whitespace
403
+ // Strip whitespace, then split into signed terms: "x-y+1/3" → ["x", "-y", "+1/3"].
404
+ // Dangling operators (e.g. "x+") produce no token and are silently ignored.
403
405
  const expr = expr_input.replaceAll(/\s+/g, ``);
404
- if (!expr)
405
- return { coefficients, translation };
406
- // Tokenize: split into terms while preserving signs
407
- // E.g., "x-y+1/3" → ["x", "-y", "+1/3"] or "-x+y" → ["-x", "+y"]
408
- const tokens = [];
409
- let current_token = ``;
410
- for (let idx = 0; idx < expr.length; idx++) {
411
- const char = expr[idx];
412
- if ((char === `+` || char === `-`) && current_token.length > 0) {
413
- tokens.push(current_token);
414
- current_token = char;
415
- }
416
- else {
417
- current_token += char;
418
- }
419
- }
420
- if (current_token)
421
- tokens.push(current_token);
422
- for (const token of tokens) {
423
- // Check if this token is a variable term (x, y, or z with optional sign)
424
- const var_match = /^([+-]?)([xyz])$/.exec(token);
425
- if (var_match) {
426
- const sign = var_match[1] === `-` ? -1 : 1;
427
- const var_char = var_match[2];
428
- const var_idx = var_char === `x` ? 0 : var_char === `y` ? 1 : 2;
406
+ for (const token of expr.match(/[+-]?[^+-]+/g) ?? []) {
407
+ const sign = token.startsWith(`-`) ? -1 : 1;
408
+ const term = token.replace(/^[+-]/, ``);
409
+ // Variable term (x, y, or z)
410
+ const var_idx = [`x`, `y`, `z`].indexOf(term);
411
+ if (var_idx !== -1) {
429
412
  coefficients[var_idx] += sign;
430
413
  continue;
431
414
  }
432
- // Check if this is a numeric term (integer, decimal, or fraction)
433
- let sign = 1;
434
- let num_str = token;
435
- // Handle leading sign
436
- if (num_str.startsWith(`+`)) {
437
- num_str = num_str.slice(1);
438
- }
439
- else if (num_str.startsWith(`-`)) {
440
- sign = -1;
441
- num_str = num_str.slice(1);
442
- }
443
- // Skip empty tokens (from dangling operators like "x+")
444
- if (!num_str || num_str === `+` || num_str === `-`)
415
+ // Numeric term: integer, decimal, or fraction like "1/3"
416
+ const parts = term.split(`/`);
417
+ // skip malformed terms like "1/2/3"
418
+ if (parts.length > 2) {
419
+ diag_warn(`Skipping malformed symmetry term '${term}'`);
445
420
  continue;
446
- if (num_str.includes(`/`)) {
447
- // Fraction
448
- const parts = num_str.split(`/`);
449
- if (parts.length === 2) {
450
- const numerator = parseFloat(parts[0]);
451
- const denominator = parseFloat(parts[1]);
452
- if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {
453
- translation += sign * (numerator / denominator);
454
- }
455
- }
456
- }
457
- else {
458
- // Integer or decimal
459
- const val = parseFloat(num_str);
460
- if (!isNaN(val)) {
461
- translation += sign * val;
462
- }
463
421
  }
422
+ const [numerator, denominator = `1`] = parts;
423
+ const value = Number(numerator) / Number(denominator);
424
+ if (Number.isFinite(value))
425
+ translation += sign * value;
426
+ else
427
+ diag_warn(`Skipping non-finite symmetry term '${term}'`);
464
428
  }
465
429
  return { coefficients, translation };
466
430
  };
467
- // Apply symmetry operations to generate equivalent positions
468
- const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords) => {
469
- if (symmetry_ops.length === 0)
431
+ // Apply symmetry operations (and optional lattice-centering translations) to
432
+ // generate all equivalent positions. Deduplication uses 6 decimal places to
433
+ // absorb floating point error from compound ops like x-y, -x+y.
434
+ const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords, centering = []) => {
435
+ if (symmetry_ops.length === 0 && centering.length === 0)
470
436
  return [atom];
471
437
  const equivalent_atoms = [];
472
438
  const seen = new Set();
473
439
  const wrap = (coords) => wrap_fractional_coords ? wrap_to_unit_cell(coords) : coords;
474
- // Use 6 decimal places for deduplication to handle floating point imprecision
475
- // from compound symmetry operations like x-y, -x+y which can produce small errors
476
- // Always include base atom (optionally wrapped)
477
- const base_coords = wrap(atom.coords);
478
- seen.add(cif_coords_key(base_coords));
479
- equivalent_atoms.push({ ...atom, coords: base_coords });
440
+ // Every generated position is also offset by each centering translation
441
+ const shifts = [[0, 0, 0], ...centering];
442
+ // Record a position plus its centering images, deduplicating on wrapped coords
443
+ const add_position = (coords) => {
444
+ for (const [dx, dy, dz] of shifts) {
445
+ const wrapped = wrap([coords[0] + dx, coords[1] + dy, coords[2] + dz]);
446
+ const key = cif_coords_key(wrapped);
447
+ if (seen.has(key))
448
+ continue;
449
+ seen.add(key);
450
+ const id = equivalent_atoms.length === 0 ? atom.id : `${atom.id}_${equivalent_atoms.length}`;
451
+ equivalent_atoms.push({ ...atom, coords: wrapped, id });
452
+ }
453
+ };
454
+ add_position(atom.coords); // base atom (+ centering images)
455
+ // ops arrive pre-normalized (quotes + whitespace already stripped, see normalized_ops)
480
456
  for (const operation of symmetry_ops) {
481
- const operation_match = /['"]([^'"]+)['"]/.exec(operation);
482
- const expr_str = operation_match ? operation_match[1] : operation.trim();
483
- const parts = expr_str.split(`,`).map((part) => part.trim());
457
+ const parts = operation.split(`,`);
484
458
  if (parts.length !== 3)
485
459
  continue;
486
460
  const new_coords = [0, 0, 0];
487
461
  for (let dim = 0; dim < 3; dim++) {
488
462
  const { coefficients, translation } = parse_symmetry_expression(parts[dim]);
489
- // Apply: new_coord = coeff_x * x + coeff_y * y + coeff_z * z + translation
490
- new_coords[dim] =
491
- coefficients[0] * atom.coords[0] +
492
- coefficients[1] * atom.coords[1] +
493
- coefficients[2] * atom.coords[2] +
494
- translation;
463
+ // new_coord = coeff_x * x + coeff_y * y + coeff_z * z + translation
464
+ new_coords[dim] = math.dot(coefficients, atom.coords) + translation;
495
465
  }
496
- // Wrap and deduplicate transformed coordinates
497
- const wrapped = wrap(new_coords);
498
- const cache_key = cif_coords_key(wrapped);
499
- if (seen.has(cache_key))
500
- continue;
501
- seen.add(cache_key);
502
- equivalent_atoms.push({
503
- ...atom,
504
- coords: wrapped,
505
- id: `${atom.id}_${equivalent_atoms.length}`,
506
- });
466
+ add_position(new_coords);
507
467
  }
508
468
  return equivalent_atoms;
509
469
  };
470
+ // Parse a CIF numeric token, stripping a trailing uncertainty like "1.234(5)"
471
+ const parse_cif_uncertain_number = (token) => {
472
+ const value = parse_num_token(token.split(`(`)[0]);
473
+ return isNaN(value) ? null : value;
474
+ };
510
475
  const extract_cif_cell_parameters = (text, type, strict = true) => text
511
476
  .split(`\n`)
512
477
  .filter((line) => line.startsWith(`_${type}`))
@@ -519,11 +484,9 @@ const extract_cif_cell_parameters = (text, type, strict = true) => text
519
484
  throw new Error(`Invalid CIF cell parameter line format: ${line}`);
520
485
  return null;
521
486
  }
522
- const value = parseFloat(tokens[1].split(`(`)[0]);
523
- if (isNaN(value)) {
524
- if (strict)
525
- throw new Error(`Invalid CIF cell parameter in line: ${line}`);
526
- return null; // Return null for invalid values in non-strict mode
487
+ const value = parse_cif_uncertain_number(tokens[1]);
488
+ if (value === null && strict) {
489
+ throw new Error(`Invalid CIF cell parameter in line: ${line}`);
527
490
  }
528
491
  return value;
529
492
  })
@@ -551,6 +514,22 @@ const build_cif_atom_site_header_indices = (headers) => {
551
514
  });
552
515
  return indices;
553
516
  };
517
+ // Which coordinate triple a CIF atom-site loop provides (fractional preferred), or null
518
+ const cif_coords_type = (indices) => {
519
+ if (indices.x !== undefined && indices.y !== undefined && indices.z !== undefined) {
520
+ return `fract`;
521
+ }
522
+ if (indices.cart_x !== undefined &&
523
+ indices.cart_y !== undefined &&
524
+ indices.cart_z !== undefined) {
525
+ return `cart`;
526
+ }
527
+ return null;
528
+ };
529
+ // The 3 column indices for the requested coordinate type
530
+ const cif_coord_indices = (indices, coords_type) => coords_type === `fract`
531
+ ? [indices.x, indices.y, indices.z]
532
+ : [indices.cart_x, indices.cart_y, indices.cart_z];
554
533
  // Walk CIF loop_ blocks: yields each loop's header tags plus the index of its first data line
555
534
  function* iter_cif_loops(lines) {
556
535
  for (let idx = 0; idx < lines.length; idx++) {
@@ -571,28 +550,24 @@ const split_cif_tokens = (line) => (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)
571
550
  // Parse atom data from CIF with robust error handling
572
551
  const parse_cif_atom_data = (raw_data, indices, coords_type) => {
573
552
  const { label = 0, symbol = -1, occupancy = -1 } = indices;
574
- const coord_indices = coords_type === `fract`
575
- ? [indices.x, indices.y, indices.z]
576
- : [indices.cart_x, indices.cart_y, indices.cart_z];
577
- if (coord_indices.some((idx) => idx === undefined)) {
578
- throw new Error(`Missing coordinate indices`);
579
- }
553
+ // cif_coords_type already guaranteed all three indices for coords_type exist
554
+ const coord_indices = cif_coord_indices(indices, coords_type);
580
555
  const coords_triplet = vec3_from_values(coord_indices.map((idx) => {
581
- if (idx === undefined)
582
- throw new Error(`Invalid coordinate index`);
583
556
  const coord_str = raw_data[idx];
584
557
  if (!coord_str)
585
558
  throw new Error(`Missing coordinate at index ${idx}`);
586
- const coord = parseFloat(coord_str.split(`(`)[0]);
587
- if (isNaN(coord))
559
+ const coord = parse_cif_uncertain_number(coord_str);
560
+ if (coord === null)
588
561
  throw new Error(`Invalid coordinate: ${coord_str}`);
589
562
  return coord;
590
563
  }), `CIF atom coordinates`);
591
- const occu = occupancy >= 0 && raw_data[occupancy]
592
- ? parseFloat(raw_data[occupancy].split(`(`)[0]) || 1.0
593
- : 1.0;
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];
564
+ const raw_occu = occupancy >= 0 && raw_data[occupancy]
565
+ ? parse_cif_uncertain_number(raw_data[occupancy])
566
+ : null;
567
+ // Explicit 0 is kept; missing / `.` / `?` (null) default to fully occupied.
568
+ const occu = raw_occu ?? 1.0;
569
+ const from_symbol = symbol >= 0 ? /^(?<element>[A-Z][a-z]*)/.exec(raw_data[symbol])?.[1] : undefined;
570
+ const element_symbol = from_symbol ?? raw_data[label]?.match(/(?:[A-Z][a-z]*)/g)?.[0];
596
571
  if (!element_symbol) {
597
572
  throw new Error(`Could not extract element symbol from: ${raw_data.join(` `)}`);
598
573
  }
@@ -639,13 +614,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
639
614
  continue;
640
615
  // Check if this loop contains coordinate headers
641
616
  const indices_preview = build_cif_atom_site_header_indices(headers);
642
- const has_coords = (indices_preview.x !== undefined &&
643
- indices_preview.y !== undefined &&
644
- indices_preview.z !== undefined) ||
645
- (indices_preview.cart_x !== undefined &&
646
- indices_preview.cart_y !== undefined &&
647
- indices_preview.cart_z !== undefined);
648
- if (!has_coords)
617
+ if (cif_coords_type(indices_preview) === null)
649
618
  continue;
650
619
  // This is the desired atom-site loop with coordinates: collect data lines
651
620
  atom_headers = headers;
@@ -679,23 +648,13 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
679
648
  // Parse atom data with error handling
680
649
  const header_indices = build_cif_atom_site_header_indices(atom_headers);
681
650
  // Determine available coordinate type
682
- const coords_type = header_indices.x !== undefined &&
683
- header_indices.y !== undefined &&
684
- header_indices.z !== undefined
685
- ? `fract`
686
- : header_indices.cart_x !== undefined &&
687
- header_indices.cart_y !== undefined &&
688
- header_indices.cart_z !== undefined
689
- ? `cart`
690
- : null;
651
+ const coords_type = cif_coords_type(header_indices);
691
652
  if (!coords_type) {
692
653
  diag_error(`CIF atom site loop missing coordinates (fract or Cartn)`);
693
654
  return null;
694
655
  }
695
656
  // Collect required coordinate indices
696
- const required_indices = coords_type === `fract`
697
- ? [header_indices.x, header_indices.y, header_indices.z]
698
- : [header_indices.cart_x, header_indices.cart_y, header_indices.cart_z];
657
+ const required_indices = cif_coord_indices(header_indices, coords_type);
699
658
  const atoms = atom_data_lines
700
659
  .map(split_cif_tokens)
701
660
  .filter((tokens) => {
@@ -734,15 +693,9 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
734
693
  const cart_to_frac = cart_to_frac_with_fallback(lattice_matrix, [a, b, c]).convert;
735
694
  // Create sites with coordinate conversion and symmetry operations
736
695
  const wrap_vec3 = (vec) => wrap_fractional_coords ? wrap_to_unit_cell(vec) : vec;
737
- // Apply symmetry operations to generate all equivalent positions
738
- const all_sites = [];
739
- // Normalize symmetry operations (trim/strip quotes) but preserve duplicates; we deduplicate positions later
740
- const normalized_ops = symmetry_ops
741
- .map((op) => /['"]([^'"]+)['"]/.exec(op)?.[1] ?? op.trim())
742
- .map((op) => op.replaceAll(/\s+/g, ``));
743
- // Rely on symmetry operations list for all centering/translations to avoid double-counting
744
- // TODO: Support conventional cells with centering by discovering centering from space group metadata
745
- // when present (e.g. P, I, F, C, R centering types)
696
+ // Strip surrounding quotes and all whitespace (preserving duplicates; positions
697
+ // are deduplicated later). Leaves ops as bare `x,y,z`-style expressions.
698
+ const normalized_ops = symmetry_ops.map((op) => (/['"](?<expr>[^'"]+)['"]/.exec(op)?.groups?.expr ?? op).replaceAll(/\s+/g, ``));
746
699
  // Inspect optional _atom_type_number_in_cell loop to see if atom sites are already expanded
747
700
  const atom_type_counts = {};
748
701
  for (const { headers, data_start } of iter_cif_loops(lines)) {
@@ -760,11 +713,15 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
760
713
  const toks = split_cif_tokens(line);
761
714
  if (toks.length > Math.max(sym_idx, num_idx)) {
762
715
  // Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
763
- const match = /^([A-Z][a-z]*)/.exec(toks[sym_idx]);
716
+ const match = /^(?<element>[A-Z][a-z]*)/.exec(toks[sym_idx]);
764
717
  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;
718
+ // Strip standard-uncertainty parentheses (`8(0)` -> `8`) like other CIF
719
+ // readers; empty prefixes like `(8)` parse as NaN and get skipped
720
+ const num = Math.trunc(parse_num_token(toks[num_idx].split(`(`)[0]));
721
+ // sum rows that normalize to the same element (e.g. Fe2+ and Fe3+ → Fe)
722
+ if (sym && !Number.isNaN(num)) {
723
+ atom_type_counts[sym] = (atom_type_counts[sym] ?? 0) + num;
724
+ }
768
725
  }
769
726
  }
770
727
  break;
@@ -777,38 +734,61 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
777
734
  const already_enumerated = has_expected_counts &&
778
735
  Object.entries(atom_type_counts).every(([el, exp]) => (observed_counts[el] || 0) >= exp);
779
736
  const ops_to_use = already_enumerated ? [] : normalized_ops;
780
- // Global deduplication of final sites (per element + coordinates + label)
781
- // Use 6 decimal places to handle floating point imprecision from compound symmetry ops
782
- const seen_site_keys = new Set();
783
- for (const atom of atoms) {
784
- const element = validate_element_symbol(atom.element, all_sites.length);
785
- // Convert to fractional coordinates if needed
786
- let fractional_atom;
787
- if (atom.coords_type === `fract`) {
788
- fractional_atom = {
789
- ...atom,
790
- coords: wrap_vec3(atom.coords),
791
- coords_type: `fract`,
792
- };
793
- }
794
- else {
795
- const xyz_base = [atom.coords[0], atom.coords[1], atom.coords[2]];
796
- const atom_abc = wrap_vec3(cart_to_frac(xyz_base));
797
- fractional_atom = { ...atom, coords: atom_abc, coords_type: `fract` };
737
+ // Candidate lattice-centering translations from the space-group symbol (R
738
+ // only valid in the hexagonal setting, α≈β≈90°, γ≈120°). Whether to actually
739
+ // apply them is decided below by reconciling against _atom_type_number_in_cell.
740
+ const centering_letter = extract_cif_centering(text);
741
+ const is_hexagonal_setting = Math.abs(alpha - 90) <= 1 && Math.abs(beta - 90) <= 1 && Math.abs(gamma - 120) <= 1;
742
+ const centering = centering_letter && (centering_letter !== `R` || is_hexagonal_setting)
743
+ ? CENTERING_VECTORS[centering_letter]
744
+ : [];
745
+ // Build all sites by expanding each atom via the symmetry ops (+ optional
746
+ // centering). Deduplicate globally on element + coordinates + label (6 dp to
747
+ // absorb floating point error from compound ops).
748
+ const build_sites = (extra_centering) => {
749
+ const sites = [];
750
+ const seen_site_keys = new Set();
751
+ for (const atom of atoms) {
752
+ const element = validate_element_symbol(atom.element, sites.length);
753
+ const coords = atom.coords_type === `fract`
754
+ ? wrap_vec3(atom.coords)
755
+ : wrap_vec3(cart_to_frac([atom.coords[0], atom.coords[1], atom.coords[2]]));
756
+ const fractional_atom = { ...atom, coords, coords_type: `fract` };
757
+ const equiv_atoms = apply_symmetry_ops(fractional_atom, ops_to_use, wrap_fractional_coords, extra_centering);
758
+ for (const equiv_atom of equiv_atoms) {
759
+ const abc = wrap_vec3(equiv_atom.coords);
760
+ const key = `${element}|${equiv_atom.id}|${cif_coords_key(abc)}`;
761
+ if (seen_site_keys.has(key))
762
+ continue;
763
+ seen_site_keys.add(key);
764
+ sites.push(make_site(element, abc, frac_to_cart(abc), equiv_atom.id, {}, equiv_atom.occupancy));
765
+ }
798
766
  }
799
- // First apply symmetry operations in fractional space
800
- const equiv_atoms = apply_symmetry_ops(fractional_atom, ops_to_use, wrap_fractional_coords);
801
- for (const equiv_atom of equiv_atoms) {
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));
767
+ return sites;
768
+ };
769
+ // Expand with point-group ops first. If the space group is centered and the
770
+ // result falls short of _atom_type_number_in_cell, retry with centering and
771
+ // adopt it only when it reconciles the expected total exactly — this fixes
772
+ // CIFs listing point-only ops for the asymmetric unit while avoiding
773
+ // double-counting CIFs whose atom list already embeds centering (e.g. C2/c
774
+ // COD 7008984, where listed ops + atoms already total the cell contents).
775
+ let sites = build_sites([]);
776
+ const expected_total = Object.values(atom_type_counts).reduce((sum, num) => sum + num, 0);
777
+ if (centering.length > 0 && expected_total > sites.length) {
778
+ const centered_sites = build_sites(centering);
779
+ // Adopt centering only when per-element counts reconcile exactly. Checking
780
+ // the total alone is insufficient: it can coincide while individual element
781
+ // counts are wrong (e.g. expected Fe 1 / O 3 but centering yields Fe 2 / O 2).
782
+ const counts = {};
783
+ for (const site of centered_sites) {
784
+ const element = site.species[0].element;
785
+ counts[element] = (counts[element] ?? 0) + 1;
809
786
  }
787
+ const reconciles = centered_sites.length === expected_total &&
788
+ Object.entries(atom_type_counts).every(([element, exp]) => counts[element] === exp);
789
+ if (reconciles)
790
+ sites = centered_sites;
810
791
  }
811
- const sites = all_sites;
812
792
  return { sites, lattice: { matrix: lattice_matrix, ...lattice_params } };
813
793
  }
814
794
  catch (error) {
@@ -865,7 +845,7 @@ export function parse_phonopy_yaml(content, cell_type) {
865
845
  }
866
846
  // Check if we're still in the phonon_displacements section
867
847
  if (skip_displacements) {
868
- if (/^[a-zA-Z_]/.exec(line)) {
848
+ if (/^[a-zA-Z_]/.test(line)) {
869
849
  // New top-level key, stop skipping
870
850
  skip_displacements = false;
871
851
  }
@@ -888,17 +868,16 @@ export function parse_phonopy_yaml(content, cell_type) {
888
868
  diag_error(`Requested cell type '${cell_type}' not found in phonopy YAML`);
889
869
  return null;
890
870
  }
891
- // Auto mode: return preferred structure in order of preference
892
- // 1. supercell (most detailed)
893
- // 2. phonon_supercell
894
- // 3. unit_cell
895
- // 4. phonon_primitive_cell
896
- // 5. primitive_cell
897
- const auto_cell = get_phonopy_cell(data, `supercell`) ??
898
- get_phonopy_cell(data, `phonon_supercell`) ??
899
- get_phonopy_cell(data, `unit_cell`) ??
900
- get_phonopy_cell(data, `phonon_primitive_cell`) ??
901
- get_phonopy_cell(data, `primitive_cell`);
871
+ // Auto mode: return first available cell, most detailed first
872
+ const auto_cell = [
873
+ `supercell`,
874
+ `phonon_supercell`,
875
+ `unit_cell`,
876
+ `phonon_primitive_cell`,
877
+ `primitive_cell`,
878
+ ]
879
+ .map((kind) => get_phonopy_cell(data, kind))
880
+ .find(Boolean);
902
881
  if (auto_cell)
903
882
  return convert_phonopy_cell(auto_cell);
904
883
  diag_error(`No valid cells found in phonopy YAML`);
@@ -956,6 +935,31 @@ export function is_parsed_structure(obj) {
956
935
  const has_coords = Array.isArray(abc) || Array.isArray(xyz);
957
936
  return has_species && has_coords;
958
937
  }
938
+ // Structure JSON serialized by pymatgen (default verbosity) stores only the lattice
939
+ // matrix + pbc; derive the missing scalar params (a/b/c/angles/volume) from the matrix
940
+ // so downstream consumers (camera auto-fit, density, export) never see NaN.
941
+ export function ensure_lattice_params(structure) {
942
+ const lattice = structure.lattice;
943
+ if (!lattice?.matrix)
944
+ return structure;
945
+ const params = [
946
+ lattice.a,
947
+ lattice.b,
948
+ lattice.c,
949
+ lattice.alpha,
950
+ lattice.beta,
951
+ lattice.gamma,
952
+ lattice.volume,
953
+ ];
954
+ if (params.every(Number.isFinite))
955
+ return structure;
956
+ // The matrix is authoritative: recompute all params from it rather than
957
+ // trusting a partially-populated (or non-numeric) set of values.
958
+ return {
959
+ ...structure,
960
+ lattice: { ...lattice, ...math.calc_lattice_params(lattice.matrix) },
961
+ };
962
+ }
959
963
  // Normalize structure coordinates: wrap fractional coords to [0,1) and recompute Cartesian
960
964
  // Only normalizes when lattice matrix is available to ensure abc/xyz stay consistent
961
965
  export function normalize_fractional_coords(structure) {
@@ -989,7 +993,7 @@ const detect_json_structure = (content) => {
989
993
  }
990
994
  // Otherwise try parsing as pymatgen/nested structure JSON
991
995
  const structure = find_structure_in_json(parsed);
992
- return structure ? normalize_fractional_coords(structure) : null;
996
+ return structure ? ensure_lattice_params(normalize_fractional_coords(structure)) : null;
993
997
  };
994
998
  // Internal: auto-detect file format, returns null on failure after recording reasons (see parse error contract at top)
995
999
  function parse_structure_file_impl(content, filename) {
@@ -1048,36 +1052,20 @@ function parse_structure_file_impl(content, filename) {
1048
1052
  diag_error(`File too short to determine format`);
1049
1053
  return null;
1050
1054
  }
1051
- // XYZ format detection: first line should be a number, second line is comment
1052
- const first_line_number = parseInt(lines[0].trim(), 10);
1053
- if (!isNaN(first_line_number) && first_line_number > 0) {
1054
- // Check if this looks like XYZ format
1055
- if (lines.length >= first_line_number + 2) {
1056
- // Try to parse a coordinate line to see if it looks like XYZ
1057
- const coord_line_idx = 2; // First coordinate line in XYZ
1058
- if (coord_line_idx < lines.length) {
1059
- const parts = lines[coord_line_idx].trim().split(/\s+/);
1060
- // XYZ format: element symbol followed by 3 coordinates
1061
- if (parts.length >= 4) {
1062
- const first_token = parts[0];
1063
- const coords = parts.slice(1, 4);
1064
- // Check if first token looks like an element symbol (not a number)
1065
- // and the next 3 tokens look like coordinates (numbers)
1066
- const is_element_symbol = isNaN(parseInt(first_token, 10)) && first_token.length <= 3;
1067
- const are_coordinates = coords.every((coord) => !isNaN(parseFloat(coord)));
1068
- if (is_element_symbol && are_coordinates) {
1069
- // First token is likely an element symbol, likely XYZ
1070
- return parse_xyz(content);
1071
- }
1072
- }
1073
- }
1074
- }
1075
- }
1055
+ // XYZ format detection: first line is a positive atom count, second line is a
1056
+ // comment, and the first coordinate line looks like "<element> <x> <y> <z>"
1057
+ const first_line_number = Math.trunc(parse_leading_num(lines[0]));
1058
+ if (!isNaN(first_line_number) &&
1059
+ first_line_number > 0 &&
1060
+ lines.length >= first_line_number + 2 &&
1061
+ is_xyz_atom_line(lines[2]?.trim().split(/\s+/)))
1062
+ return parse_xyz(content);
1076
1063
  // POSCAR format detection: look for typical structure
1077
1064
  if (lines.length >= 8) {
1078
- const second_line_number = parseFloat(lines[1].trim());
1079
- // Second line is a number (scale factor), likely POSCAR
1080
- if (!isNaN(second_line_number))
1065
+ // Second line starts with a number (scale factor), likely POSCAR. First
1066
+ // token only: POSCAR allows three per-axis scale factors (or trailing
1067
+ // comments) on line 2, and blank lines must not pass
1068
+ if (!isNaN(parse_leading_num(lines[1])))
1081
1069
  return parse_poscar(content);
1082
1070
  }
1083
1071
  // CIF format detection: look for CIF-specific keywords
@@ -1109,16 +1097,19 @@ export function parse_structure_file(content, filename) {
1109
1097
  // Universal parser for JSON and structure files; throws an Error aggregating per-format failure reasons when nothing parses
1110
1098
  export function parse_any_structure(content, filename) {
1111
1099
  reset_parse_diagnostics();
1112
- const finalize_structure = (structure) => ({
1113
- sites: structure.sites,
1114
- charge: 0,
1115
- ...(structure.properties && {
1116
- properties: clone_structure_properties(structure.properties),
1117
- }),
1118
- ...(structure.lattice && {
1119
- lattice: { ...structure.lattice, pbc: [true, true, true] },
1120
- }),
1121
- });
1100
+ const finalize_structure = (parsed_structure) => {
1101
+ const structure = ensure_lattice_params(parsed_structure);
1102
+ return {
1103
+ sites: structure.sites,
1104
+ charge: 0,
1105
+ ...(structure.properties && {
1106
+ properties: structuredClone(structure.properties),
1107
+ }),
1108
+ ...(structure.lattice && {
1109
+ lattice: { ...structure.lattice, pbc: [true, true, true] },
1110
+ }),
1111
+ };
1112
+ };
1122
1113
  // Fast path: content is already a serialized structure object
1123
1114
  try {
1124
1115
  const parsed = JSON.parse(content);
@@ -1271,10 +1262,7 @@ const unwrap_data = (value) => value && typeof value === `object` && `data` in v
1271
1262
  function is_optimade_structure_object(value) {
1272
1263
  if (!value || typeof value !== `object`)
1273
1264
  return false;
1274
- const obj = value;
1275
- const type = obj.type;
1276
- const id = obj.id;
1277
- const attributes = obj.attributes;
1265
+ const { type, id, attributes } = value;
1278
1266
  return (type === `structures` &&
1279
1267
  typeof id === `string` &&
1280
1268
  typeof attributes === `object` &&
@@ -1306,35 +1294,6 @@ export function optimade_to_crystal(optimade_structure) {
1306
1294
  return null;
1307
1295
  }
1308
1296
  }
1309
- // Check if filename indicates a structure file
1310
- export function is_structure_file(filename) {
1311
- const name = filename.toLowerCase();
1312
- // Trajectory-only formats (can't be structures)
1313
- if (/\.(traj|xtc|h5|hdf5)$/i.test(name) || /xdatcar/i.test(name))
1314
- return false;
1315
- // Always structure formats
1316
- if (STRUCTURE_EXTENSIONS_REGEX.test(name))
1317
- return true;
1318
- if (VASP_FILES_REGEX.test(name))
1319
- return true;
1320
- // .xyz/.extxyz files: structure unless they have trajectory keywords
1321
- if (/\.(xyz|extxyz)$/i.test(name))
1322
- return !TRAJ_KEYWORDS_REGEX.test(name);
1323
- // Keyword-based detection for YAML/XML
1324
- if (/\.(yaml|yml|xml)$/i.test(name) && STRUCT_KEYWORDS_REGEX.test(name))
1325
- return true;
1326
- // More restrictive keyword detection for JSON files
1327
- if (/\.json$/i.test(name) &&
1328
- STRUCT_KEYWORDS_STRICT_REGEX.test(name) &&
1329
- !TRAJ_KEYWORDS_REGEX.test(name) &&
1330
- !CONFIG_DIRS_REGEX.test(name))
1331
- return true;
1332
- // Compressed files - check base filename recursively
1333
- if (COMPRESSION_EXTENSIONS_REGEX.test(name)) {
1334
- return is_structure_file(name.replace(COMPRESSION_EXTENSIONS_REGEX, ``));
1335
- }
1336
- return false;
1337
- }
1338
1297
  export const detect_structure_type = (filename, content) => {
1339
1298
  // Normalize compressed suffixes (gz, gzip, zip, xz, bz2) for detection parity
1340
1299
  const name_to_check = strip_compression_extensions(filename);