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.
- package/dist/Icon.svelte +7 -4
- package/dist/MillerIndexInput.svelte +1 -1
- package/dist/api/optimade.js +32 -26
- package/dist/app.css +0 -3
- package/dist/brillouin/BrillouinZone.svelte +76 -148
- package/dist/brillouin/BrillouinZone.svelte.d.ts +6 -14
- package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
- package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
- package/dist/brillouin/BrillouinZoneScene.svelte +97 -205
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +4 -23
- package/dist/brillouin/BrillouinZoneTooltip.svelte +16 -25
- package/dist/brillouin/ReciprocalVectors.svelte +39 -0
- package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
- package/dist/brillouin/compute.d.ts +2 -0
- package/dist/brillouin/compute.js +89 -90
- package/dist/brillouin/geometry.d.ts +8 -0
- package/dist/brillouin/geometry.js +57 -0
- package/dist/brillouin/index.d.ts +2 -0
- package/dist/brillouin/index.js +2 -0
- package/dist/brillouin/types.d.ts +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte +14 -13
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +109 -203
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +180 -470
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
- package/dist/chempot-diagram/async-compute.svelte.js +3 -1
- package/dist/chempot-diagram/chempot-worker.js +2 -1
- package/dist/chempot-diagram/color.d.ts +3 -6
- package/dist/chempot-diagram/color.js +5 -5
- package/dist/chempot-diagram/compute.d.ts +4 -4
- package/dist/chempot-diagram/compute.js +20 -20
- package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
- package/dist/chempot-diagram/controls-state.svelte.js +42 -0
- package/dist/chempot-diagram/export.d.ts +47 -0
- package/dist/chempot-diagram/export.js +133 -0
- package/dist/chempot-diagram/index.d.ts +1 -0
- package/dist/chempot-diagram/index.js +1 -0
- package/dist/chempot-diagram/pointer.d.ts +0 -10
- package/dist/chempot-diagram/pointer.js +4 -4
- package/dist/chempot-diagram/types.d.ts +3 -3
- package/dist/colors/index.js +8 -7
- package/dist/composition/FormulaFilter.svelte +18 -11
- package/dist/composition/PieChart.svelte +11 -10
- package/dist/composition/chem-sys.d.ts +8 -0
- package/dist/composition/chem-sys.js +86 -0
- package/dist/composition/format.js +7 -4
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/composition/parse.d.ts +0 -1
- package/dist/composition/parse.js +41 -31
- package/dist/controls.d.ts +1 -0
- package/dist/controls.js +0 -1
- package/dist/convex-hull/ConvexHull.svelte +8 -10
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
- package/dist/convex-hull/ConvexHull2D.svelte +106 -185
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +179 -683
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +183 -687
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
- package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
- package/dist/convex-hull/ConvexHullControls.svelte +88 -7
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
- package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
- package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
- package/dist/convex-hull/ConvexHullStats.svelte +36 -175
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
- package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
- package/dist/convex-hull/GasPressureControls.svelte +4 -4
- package/dist/convex-hull/TemperatureSlider.svelte +2 -2
- package/dist/convex-hull/barycentric-coords.d.ts +2 -4
- package/dist/convex-hull/barycentric-coords.js +6 -33
- package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
- package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
- package/dist/convex-hull/demo-temperature.d.ts +1 -1
- package/dist/convex-hull/demo-temperature.js +20 -22
- package/dist/convex-hull/gas-thermodynamics.d.ts +2 -2
- package/dist/convex-hull/gas-thermodynamics.js +22 -30
- package/dist/convex-hull/helpers.d.ts +42 -7
- package/dist/convex-hull/helpers.js +171 -78
- package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
- package/dist/convex-hull/hull-state.svelte.js +124 -0
- package/dist/convex-hull/index.d.ts +10 -8
- package/dist/convex-hull/index.js +7 -2
- package/dist/convex-hull/thermodynamics.js +136 -960
- package/dist/convex-hull/types.d.ts +13 -5
- package/dist/convex-hull/types.js +12 -0
- package/dist/coordination/CoordinationBarPlot.svelte +27 -34
- package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
- package/dist/element/BohrAtom.svelte +2 -1
- package/dist/element/index.d.ts +4 -0
- package/dist/element/index.js +18 -0
- package/dist/feedback/DragOverlay.svelte +3 -1
- package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
- package/dist/feedback/StatusMessage.svelte +13 -3
- package/dist/fermi-surface/FermiSlice.svelte +13 -5
- package/dist/fermi-surface/FermiSurface.svelte +78 -151
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
- package/dist/fermi-surface/FermiSurfaceControls.svelte +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -221
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +8 -34
- package/dist/fermi-surface/compute.js +67 -66
- package/dist/fermi-surface/export.js +6 -16
- package/dist/fermi-surface/index.d.ts +0 -1
- package/dist/fermi-surface/index.js +0 -1
- package/dist/fermi-surface/parse.d.ts +1 -1
- package/dist/fermi-surface/parse.js +71 -79
- package/dist/fermi-surface/types.d.ts +3 -2
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +69 -52
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
- package/dist/heatmap-matrix/index.d.ts +3 -2
- package/dist/heatmap-matrix/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/io/ExportPane.svelte +166 -0
- package/dist/io/ExportPane.svelte.d.ts +17 -0
- package/dist/io/decompress.js +5 -4
- package/dist/io/export.d.ts +9 -5
- package/dist/io/export.js +77 -51
- package/dist/io/fetch.d.ts +2 -1
- package/dist/io/fetch.js +5 -1
- package/dist/io/file-drop.d.ts +8 -1
- package/dist/io/file-drop.js +48 -36
- package/dist/io/index.d.ts +2 -0
- package/dist/io/index.js +10 -0
- package/dist/io/types.d.ts +13 -0
- package/dist/io/url-drop.js +64 -33
- package/dist/isosurface/parse.js +52 -51
- package/dist/isosurface/slice.js +5 -4
- package/dist/isosurface/types.js +1 -1
- package/dist/keyboard.d.ts +3 -0
- package/dist/keyboard.js +23 -0
- package/dist/labels.d.ts +1 -1
- package/dist/labels.js +9 -8
- package/dist/layout/FullscreenButton.svelte +33 -0
- package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
- package/dist/layout/FullscreenToggle.svelte +8 -14
- package/dist/layout/PropertyFilter.svelte +3 -2
- package/dist/layout/SettingsSection.svelte +1 -1
- package/dist/layout/ViewerChrome.svelte +116 -0
- package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
- package/dist/layout/fullscreen.d.ts +4 -0
- package/dist/layout/fullscreen.svelte.d.ts +8 -0
- package/dist/layout/fullscreen.svelte.js +37 -0
- package/dist/layout/index.d.ts +3 -0
- package/dist/layout/index.js +3 -0
- package/dist/layout/json-tree/JsonNode.svelte +1 -1
- package/dist/layout/json-tree/JsonTree.svelte +2 -2
- package/dist/layout/json-tree/utils.js +5 -4
- package/dist/marching-cubes.js +8 -13
- package/dist/math.d.ts +12 -4
- package/dist/math.js +42 -30
- package/dist/overlays/DraggablePane.svelte +4 -4
- package/dist/overlays/index.d.ts +4 -0
- package/dist/periodic-table/PeriodicTable.svelte +27 -15
- package/dist/periodic-table/PropertySelect.svelte +1 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +9 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +1 -1
- package/dist/phase-diagram/build-diagram.js +2 -2
- package/dist/phase-diagram/colors.js +1 -1
- package/dist/phase-diagram/parse.d.ts +2 -1
- package/dist/phase-diagram/parse.js +6 -5
- package/dist/phase-diagram/types.d.ts +1 -1
- package/dist/phase-diagram/utils.d.ts +3 -3
- package/dist/phase-diagram/utils.js +8 -12
- package/dist/plot/{BarPlot.svelte → bar/BarPlot.svelte} +246 -841
- package/dist/plot/{BarPlot.svelte.d.ts → bar/BarPlot.svelte.d.ts} +8 -16
- package/dist/plot/{BarPlotControls.svelte → bar/BarPlotControls.svelte} +6 -5
- package/dist/plot/{BarPlotControls.svelte.d.ts → bar/BarPlotControls.svelte.d.ts} +3 -3
- package/dist/plot/{SpacegroupBarPlot.svelte → bar/SpacegroupBarPlot.svelte} +8 -7
- package/dist/plot/{SpacegroupBarPlot.svelte.d.ts → bar/SpacegroupBarPlot.svelte.d.ts} +1 -1
- package/dist/plot/bar/data.d.ts +40 -0
- package/dist/plot/bar/data.js +154 -0
- package/dist/plot/bar/geometry.d.ts +39 -0
- package/dist/plot/bar/geometry.js +60 -0
- package/dist/plot/bar/index.d.ts +3 -0
- package/dist/plot/bar/index.js +3 -0
- package/dist/plot/box/BoxPlot.svelte +1292 -0
- package/dist/plot/box/BoxPlot.svelte.d.ts +95 -0
- package/dist/plot/box/BoxPlotControls.svelte +109 -0
- package/dist/plot/box/BoxPlotControls.svelte.d.ts +19 -0
- package/dist/plot/box/Violin.svelte +14 -0
- package/dist/plot/box/Violin.svelte.d.ts +70 -0
- package/dist/plot/box/box-plot.d.ts +56 -0
- package/dist/plot/box/box-plot.js +129 -0
- package/dist/plot/box/index.d.ts +5 -0
- package/dist/plot/box/index.js +5 -0
- package/dist/plot/box/kde.d.ts +17 -0
- package/dist/plot/box/kde.js +160 -0
- package/dist/plot/box/quantile.d.ts +3 -0
- package/dist/plot/box/quantile.js +53 -0
- package/dist/plot/{auto-place.d.ts → core/auto-place.d.ts} +1 -1
- package/dist/plot/{auto-place.js → core/auto-place.js} +6 -3
- package/dist/plot/core/axis-utils.d.ts +46 -0
- package/dist/plot/core/axis-utils.js +110 -0
- package/dist/plot/{AxisLabel.svelte → core/components/AxisLabel.svelte} +2 -2
- package/dist/plot/{AxisLabel.svelte.d.ts → core/components/AxisLabel.svelte.d.ts} +1 -1
- package/dist/plot/{ColorBar.svelte → core/components/ColorBar.svelte} +41 -38
- package/dist/plot/{ColorBar.svelte.d.ts → core/components/ColorBar.svelte.d.ts} +7 -6
- package/dist/plot/{ColorScaleSelect.svelte → core/components/ColorScaleSelect.svelte} +4 -3
- package/dist/plot/{ColorScaleSelect.svelte.d.ts → core/components/ColorScaleSelect.svelte.d.ts} +2 -2
- package/dist/plot/core/components/ControlPane.svelte +46 -0
- package/dist/plot/core/components/ControlPane.svelte.d.ts +13 -0
- package/dist/plot/{FillArea.svelte → core/components/FillArea.svelte} +17 -6
- package/dist/plot/{FillArea.svelte.d.ts → core/components/FillArea.svelte.d.ts} +1 -1
- package/dist/plot/{InteractiveAxisLabel.svelte → core/components/InteractiveAxisLabel.svelte} +3 -3
- package/dist/plot/{InteractiveAxisLabel.svelte.d.ts → core/components/InteractiveAxisLabel.svelte.d.ts} +2 -2
- package/dist/plot/{Line.svelte → core/components/Line.svelte} +33 -15
- package/dist/plot/{Line.svelte.d.ts → core/components/Line.svelte.d.ts} +3 -2
- package/dist/plot/{PlotAxis.svelte → core/components/PlotAxis.svelte} +9 -6
- package/dist/plot/{PlotAxis.svelte.d.ts → core/components/PlotAxis.svelte.d.ts} +5 -3
- package/dist/plot/{PlotControls.svelte → core/components/PlotControls.svelte} +17 -29
- package/dist/plot/core/components/PlotControls.svelte.d.ts +4 -0
- package/dist/plot/{PlotLegend.svelte → core/components/PlotLegend.svelte} +21 -10
- package/dist/plot/{PlotLegend.svelte.d.ts → core/components/PlotLegend.svelte.d.ts} +3 -2
- package/dist/plot/{PlotTooltip.svelte → core/components/PlotTooltip.svelte} +17 -1
- package/dist/plot/{PlotTooltip.svelte.d.ts → core/components/PlotTooltip.svelte.d.ts} +8 -0
- package/dist/plot/{PortalSelect.svelte → core/components/PortalSelect.svelte} +11 -7
- package/dist/plot/{ReferenceLine.svelte → core/components/ReferenceLine.svelte} +3 -3
- package/dist/plot/{ReferenceLine.svelte.d.ts → core/components/ReferenceLine.svelte.d.ts} +1 -1
- package/dist/plot/{ReferenceLine3D.svelte → core/components/ReferenceLine3D.svelte} +5 -5
- package/dist/plot/{ReferenceLine3D.svelte.d.ts → core/components/ReferenceLine3D.svelte.d.ts} +5 -5
- package/dist/plot/{ReferencePlane.svelte → core/components/ReferencePlane.svelte} +8 -8
- package/dist/plot/{ReferencePlane.svelte.d.ts → core/components/ReferencePlane.svelte.d.ts} +5 -5
- package/dist/plot/{ZeroLines.svelte → core/components/ZeroLines.svelte} +3 -3
- package/dist/plot/{ZeroLines.svelte.d.ts → core/components/ZeroLines.svelte.d.ts} +3 -3
- package/dist/plot/{ZoomRect.svelte → core/components/ZoomRect.svelte} +1 -1
- package/dist/plot/{ZoomRect.svelte.d.ts → core/components/ZoomRect.svelte.d.ts} +1 -1
- package/dist/plot/core/components/index.d.ts +17 -0
- package/dist/plot/core/components/index.js +17 -0
- package/dist/plot/{data-cleaning.d.ts → core/data-cleaning.d.ts} +71 -1
- package/dist/plot/{data-cleaning.js → core/data-cleaning.js} +21 -23
- package/dist/plot/{data-transform.d.ts → core/data-transform.d.ts} +2 -2
- package/dist/plot/{data-transform.js → core/data-transform.js} +3 -3
- package/dist/plot/core/fill-utils.d.ts +34 -0
- package/dist/plot/core/fill-utils.js +391 -0
- package/dist/plot/core/index.d.ts +10 -0
- package/dist/plot/core/index.js +11 -0
- package/dist/plot/core/interactions.d.ts +39 -0
- package/dist/plot/core/interactions.js +209 -0
- package/dist/plot/{layout.d.ts → core/layout.d.ts} +1 -0
- package/dist/plot/{layout.js → core/layout.js} +16 -8
- package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
- package/dist/plot/core/pan-zoom.svelte.js +221 -0
- package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -0
- package/dist/plot/{reference-line.d.ts → core/reference-line.d.ts} +11 -11
- package/dist/plot/{reference-line.js → core/reference-line.js} +29 -42
- package/dist/plot/core/scales.d.ts +40 -0
- package/dist/plot/{scales.js → core/scales.js} +94 -93
- package/dist/plot/core/svg.d.ts +3 -0
- package/dist/plot/core/svg.js +41 -0
- package/dist/plot/{types.d.ts → core/types.d.ts} +36 -85
- package/dist/plot/{types.js → core/types.js} +1 -1
- package/dist/plot/{utils → core/utils}/label-placement.d.ts +3 -3
- package/dist/plot/{utils → core/utils}/label-placement.js +3 -3
- package/dist/plot/core/utils/series-visibility.d.ts +26 -0
- package/dist/plot/{utils → core/utils}/series-visibility.js +29 -2
- package/dist/plot/core/utils.d.ts +12 -0
- package/dist/plot/core/utils.js +27 -0
- package/dist/plot/{Histogram.svelte → histogram/Histogram.svelte} +174 -551
- package/dist/plot/{Histogram.svelte.d.ts → histogram/Histogram.svelte.d.ts} +2 -2
- package/dist/plot/{HistogramControls.svelte → histogram/HistogramControls.svelte} +6 -6
- package/dist/plot/{HistogramControls.svelte.d.ts → histogram/HistogramControls.svelte.d.ts} +4 -4
- package/dist/plot/histogram/index.d.ts +2 -0
- package/dist/plot/histogram/index.js +2 -0
- package/dist/plot/index.d.ts +8 -41
- package/dist/plot/index.js +10 -39
- package/dist/plot/sankey/Sankey.svelte +697 -0
- package/dist/plot/sankey/Sankey.svelte.d.ts +74 -0
- package/dist/plot/sankey/SankeyControls.svelte +98 -0
- package/dist/plot/sankey/SankeyControls.svelte.d.ts +19 -0
- package/dist/plot/sankey/index.d.ts +4 -0
- package/dist/plot/sankey/index.js +3 -0
- package/dist/plot/sankey/sankey-types.d.ts +42 -0
- package/dist/plot/sankey/sankey-types.js +4 -0
- package/dist/plot/sankey/sankey.d.ts +52 -0
- package/dist/plot/sankey/sankey.js +189 -0
- package/dist/plot/{BinnedScatterPlot.svelte → scatter/BinnedScatterPlot.svelte} +64 -64
- package/dist/plot/{BinnedScatterPlot.svelte.d.ts → scatter/BinnedScatterPlot.svelte.d.ts} +6 -6
- package/dist/plot/{ElementScatter.svelte → scatter/ElementScatter.svelte} +6 -6
- package/dist/plot/{ElementScatter.svelte.d.ts → scatter/ElementScatter.svelte.d.ts} +2 -2
- package/dist/plot/{ScatterPlot.svelte → scatter/ScatterPlot.svelte} +297 -1008
- package/dist/plot/{ScatterPlot.svelte.d.ts → scatter/ScatterPlot.svelte.d.ts} +10 -18
- package/dist/plot/{ScatterPlotControls.svelte → scatter/ScatterPlotControls.svelte} +6 -5
- package/dist/plot/{ScatterPlotControls.svelte.d.ts → scatter/ScatterPlotControls.svelte.d.ts} +2 -2
- package/dist/plot/{ScatterPoint.svelte → scatter/ScatterPoint.svelte} +7 -7
- package/dist/plot/{ScatterPoint.svelte.d.ts → scatter/ScatterPoint.svelte.d.ts} +3 -3
- package/dist/plot/{adaptive-density.d.ts → scatter/adaptive-density.d.ts} +14 -4
- package/dist/plot/{adaptive-density.js → scatter/adaptive-density.js} +46 -20
- package/dist/plot/{binned-scatter-types.d.ts → scatter/binned-scatter-types.d.ts} +5 -12
- package/dist/plot/scatter/index.d.ts +7 -0
- package/dist/plot/scatter/index.js +5 -0
- package/dist/plot/scatter/scatter-data.d.ts +19 -0
- package/dist/plot/scatter/scatter-data.js +212 -0
- package/dist/plot/{ScatterPlot3D.svelte → scatter-3d/ScatterPlot3D.svelte} +25 -34
- package/dist/plot/{ScatterPlot3D.svelte.d.ts → scatter-3d/ScatterPlot3D.svelte.d.ts} +9 -17
- package/dist/plot/{ScatterPlot3DControls.svelte → scatter-3d/ScatterPlot3DControls.svelte} +14 -14
- package/dist/plot/{ScatterPlot3DControls.svelte.d.ts → scatter-3d/ScatterPlot3DControls.svelte.d.ts} +6 -6
- package/dist/plot/{ScatterPlot3DScene.svelte → scatter-3d/ScatterPlot3DScene.svelte} +129 -128
- package/dist/plot/{ScatterPlot3DScene.svelte.d.ts → scatter-3d/ScatterPlot3DScene.svelte.d.ts} +6 -15
- package/dist/plot/{Surface3D.svelte → scatter-3d/Surface3D.svelte} +7 -6
- package/dist/plot/{Surface3D.svelte.d.ts → scatter-3d/Surface3D.svelte.d.ts} +5 -4
- package/dist/plot/scatter-3d/index.d.ts +4 -0
- package/dist/plot/scatter-3d/index.js +4 -0
- package/dist/plot/sunburst/Sunburst.svelte +1041 -0
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +97 -0
- package/dist/plot/sunburst/SunburstControls.svelte +200 -0
- package/dist/plot/sunburst/SunburstControls.svelte.d.ts +26 -0
- package/dist/plot/sunburst/index.d.ts +4 -0
- package/dist/plot/sunburst/index.js +4 -0
- package/dist/plot/sunburst/render.d.ts +34 -0
- package/dist/plot/sunburst/render.js +122 -0
- package/dist/plot/sunburst/sunburst.d.ts +62 -0
- package/dist/plot/sunburst/sunburst.js +269 -0
- package/dist/rdf/RdfPlot.svelte +2 -1
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/calc-rdf.js +11 -24
- package/dist/sanitize.js +14 -3
- package/dist/scene/SceneCamera.svelte +62 -0
- package/dist/scene/SceneCamera.svelte.d.ts +19 -0
- package/dist/scene/bind-renderer.svelte.d.ts +2 -0
- package/dist/scene/bind-renderer.svelte.js +14 -0
- package/dist/scene/index.d.ts +4 -0
- package/dist/scene/index.js +5 -0
- package/dist/scene/props.js +52 -0
- package/dist/scene/types.d.ts +26 -0
- package/dist/scene/types.js +1 -0
- package/dist/settings.d.ts +79 -3
- package/dist/settings.js +321 -1
- package/dist/spectral/Bands.svelte +47 -36
- package/dist/spectral/Bands.svelte.d.ts +6 -6
- package/dist/spectral/BandsAndDos.svelte +23 -25
- package/dist/spectral/BrillouinBandsDos.svelte +42 -30
- package/dist/spectral/Dos.svelte +15 -23
- package/dist/spectral/Dos.svelte.d.ts +4 -3
- package/dist/spectral/helpers.d.ts +8 -6
- package/dist/spectral/helpers.js +137 -65
- package/dist/state.svelte.d.ts +0 -7
- package/dist/state.svelte.js +0 -6
- package/dist/structure/Arrow.svelte +2 -4
- package/dist/structure/AtomLegend.svelte +8 -9
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CanvasTooltip.svelte +1 -0
- package/dist/structure/CellSelect.svelte +12 -5
- package/dist/structure/CellSelect.svelte.d.ts +2 -1
- package/dist/structure/Cylinder.svelte +12 -8
- package/dist/structure/Cylinder.svelte.d.ts +4 -1
- package/dist/structure/Lattice.svelte +2 -2
- package/dist/structure/Structure.svelte +365 -423
- package/dist/structure/Structure.svelte.d.ts +5 -15
- package/dist/structure/StructureControls.svelte +217 -2
- package/dist/structure/StructureControls.svelte.d.ts +5 -3
- package/dist/structure/StructureExportPane.svelte +54 -156
- package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
- package/dist/structure/StructureInfoPane.svelte +10 -9
- package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
- package/dist/structure/StructureScene.svelte +376 -208
- package/dist/structure/StructureScene.svelte.d.ts +22 -20
- package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
- package/dist/structure/{label-placement.js → atom-label-placement.js} +15 -5
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +17 -22
- package/dist/structure/bond-order-perception.js +3 -5
- package/dist/structure/bonding.d.ts +4 -0
- package/dist/structure/bonding.js +134 -63
- package/dist/structure/export.d.ts +24 -4
- package/dist/structure/export.js +89 -143
- package/dist/structure/index.d.ts +4 -4
- package/dist/structure/index.js +3 -3
- package/dist/structure/measure.d.ts +3 -2
- package/dist/structure/measure.js +6 -5
- package/dist/structure/parse.d.ts +3 -2
- package/dist/structure/parse.js +419 -438
- package/dist/structure/partial-occupancy.d.ts +0 -1
- package/dist/structure/partial-occupancy.js +1 -1
- package/dist/structure/pbc.d.ts +1 -1
- package/dist/structure/pbc.js +190 -13
- package/dist/structure/polyhedra.d.ts +41 -0
- package/dist/structure/polyhedra.js +602 -0
- package/dist/structure/site.d.ts +4 -0
- package/dist/structure/site.js +1 -0
- package/dist/structure/supercell.js +3 -2
- package/dist/structure/validation.js +5 -6
- package/dist/symmetry/SymmetryElementControls.svelte +69 -0
- package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
- package/dist/symmetry/SymmetryElements.svelte +354 -0
- package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
- package/dist/symmetry/SymmetryStats.svelte +113 -8
- package/dist/symmetry/WyckoffTable.svelte +68 -7
- package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
- package/dist/symmetry/cell-transform.js +7 -14
- package/dist/symmetry/index.d.ts +14 -4
- package/dist/symmetry/index.js +291 -72
- package/dist/symmetry/spacegroups.d.ts +12 -1
- package/dist/symmetry/spacegroups.js +63 -14
- package/dist/symmetry/symmetry-elements.d.ts +33 -0
- package/dist/symmetry/symmetry-elements.js +521 -0
- package/dist/symmetry/wyckoff-db.d.ts +9 -0
- package/dist/symmetry/wyckoff-db.js +87 -0
- package/dist/table/HeatmapTable.svelte +66 -25
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/index.d.ts +1 -3
- package/dist/table/index.js +1 -1
- package/dist/theme/index.js +8 -8
- package/dist/tooltip/KCoords.svelte +45 -0
- package/dist/tooltip/KCoords.svelte.d.ts +8 -0
- package/dist/tooltip/index.d.ts +1 -0
- package/dist/tooltip/index.js +1 -0
- package/dist/trajectory/Trajectory.svelte +123 -100
- package/dist/trajectory/Trajectory.svelte.d.ts +11 -22
- package/dist/trajectory/TrajectoryExportPane.svelte +17 -25
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
- package/dist/trajectory/TrajectoryInfoPane.svelte +5 -3
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
- package/dist/trajectory/constants.js +6 -2
- package/dist/trajectory/extract.js +17 -37
- package/dist/trajectory/format-detect.d.ts +1 -1
- package/dist/trajectory/format-detect.js +27 -19
- package/dist/trajectory/frame-reader.d.ts +0 -1
- package/dist/trajectory/frame-reader.js +63 -162
- package/dist/trajectory/helpers.d.ts +10 -2
- package/dist/trajectory/helpers.js +56 -36
- package/dist/trajectory/index.js +1 -1
- package/dist/trajectory/parse/ase.d.ts +9 -1
- package/dist/trajectory/parse/ase.js +47 -32
- package/dist/trajectory/parse/diagnostics.d.ts +3 -0
- package/dist/trajectory/parse/diagnostics.js +14 -0
- package/dist/trajectory/parse/hdf5.js +1 -1
- package/dist/trajectory/parse/index.d.ts +1 -1
- package/dist/trajectory/parse/index.js +65 -105
- package/dist/trajectory/parse/lammps.d.ts +0 -2
- package/dist/trajectory/parse/lammps.js +8 -6
- package/dist/trajectory/parse/pymatgen.d.ts +2 -0
- package/dist/trajectory/parse/pymatgen.js +74 -0
- package/dist/trajectory/parse/vasp.js +38 -18
- package/dist/trajectory/parse/xyz.d.ts +13 -1
- package/dist/trajectory/parse/xyz.js +102 -94
- package/dist/trajectory/plotting.d.ts +1 -2
- package/dist/trajectory/plotting.js +16 -113
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +7 -5
- package/dist/xrd/XrdPlot.svelte +16 -30
- package/dist/xrd/broadening.d.ts +2 -1
- package/dist/xrd/calc-xrd.js +18 -20
- package/dist/xrd/index.d.ts +2 -2
- package/dist/xrd/parse.js +2 -2
- package/package.json +43 -26
- package/dist/element/data.json +0 -11864
- package/dist/fermi-surface/marching-cubes.d.ts +0 -2
- package/dist/fermi-surface/marching-cubes.js +0 -2
- package/dist/plot/PlotControls.svelte.d.ts +0 -4
- package/dist/plot/axis-utils.d.ts +0 -19
- package/dist/plot/axis-utils.js +0 -78
- package/dist/plot/defaults.d.ts +0 -19
- package/dist/plot/defaults.js +0 -9
- package/dist/plot/fill-utils.d.ts +0 -46
- package/dist/plot/fill-utils.js +0 -322
- package/dist/plot/hover-lock.svelte.d.ts +0 -14
- package/dist/plot/hover-lock.svelte.js +0 -46
- package/dist/plot/interactions.d.ts +0 -12
- package/dist/plot/interactions.js +0 -101
- package/dist/plot/scales.d.ts +0 -48
- package/dist/plot/svg.d.ts +0 -1
- package/dist/plot/svg.js +0 -11
- package/dist/plot/utils/series-visibility.d.ts +0 -15
- package/dist/plot/utils.d.ts +0 -1
- package/dist/plot/utils.js +0 -14
- /package/dist/plot/{PortalSelect.svelte.d.ts → core/components/PortalSelect.svelte.d.ts} +0 -0
- /package/dist/plot/{binned-scatter-types.js → scatter/binned-scatter-types.js} +0 -0
package/dist/structure/parse.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
.
|
|
73
|
+
.replaceAll(/(\d)-(?=[\d.])/g, `$1 -`)
|
|
45
74
|
// Revert accidental spaces after exponent markers
|
|
46
|
-
.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
diag_error(`POSCAR file too short`);
|
|
83
156
|
return null;
|
|
84
157
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
diag_error(`Error parsing POSCAR file`, error);
|
|
257
308
|
return null;
|
|
258
309
|
}
|
|
259
310
|
}
|
|
260
|
-
//
|
|
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
|
-
|
|
316
|
+
diag_error(`Empty XYZ file`);
|
|
266
317
|
return null;
|
|
267
318
|
}
|
|
268
|
-
//
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
571
|
-
|
|
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]
|
|
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 (
|
|
636
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
712
|
+
diag_warn(`Skipping invalid atom data: ${error}`);
|
|
678
713
|
return null;
|
|
679
714
|
}
|
|
680
715
|
})
|
|
681
716
|
.filter((atom) => atom !== null);
|
|
682
|
-
if (
|
|
683
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = (
|
|
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]
|
|
707
|
-
.map((op) => op.
|
|
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
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
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
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
891
|
-
|
|
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
|
-
|
|
904
|
+
diag_error(`No valid cells found in phonopy YAML`);
|
|
909
905
|
return null;
|
|
910
906
|
}
|
|
911
907
|
catch (error) {
|
|
912
|
-
|
|
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
|
-
//
|
|
986
|
-
|
|
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
|
-
|
|
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 -
|
|
1007
|
+
// JSON files - extension is authoritative, so failures return null
|
|
1002
1008
|
if (ext === `json`) {
|
|
1003
1009
|
try {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1098
|
+
diag_error(`Unable to determine file format`);
|
|
1102
1099
|
return null;
|
|
1103
1100
|
}
|
|
1104
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1230
|
+
diag_error(`OPTIMADE JSON position/species count mismatch`);
|
|
1163
1231
|
return null;
|
|
1164
1232
|
}
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
1237
|
+
diag_error(`No valid sites found in OPTIMADE JSON`);
|
|
1219
1238
|
return null;
|
|
1220
1239
|
}
|
|
1221
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1288
|
+
diag_error(`Missing required OPTIMADE structure data`);
|
|
1276
1289
|
return null;
|
|
1277
1290
|
}
|
|
1278
1291
|
try {
|
|
1279
|
-
const lattice_matrix =
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|