matterviz 0.4.0 → 0.4.2
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/EmptyState.svelte +10 -4
- package/dist/FilePicker.svelte +12 -14
- package/dist/Icon.svelte +7 -4
- package/dist/MillerIndexInput.svelte +2 -2
- package/dist/app.css +53 -0
- package/dist/brillouin/BrillouinZone.svelte +132 -219
- package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
- package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
- package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
- package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
- package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
- 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 +80 -77
- 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 +33 -38
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
- package/dist/chempot-diagram/async-compute.svelte.js +1 -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 +3 -3
- package/dist/chempot-diagram/compute.js +3 -1
- 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.d.ts +0 -4
- package/dist/colors/index.js +21 -22
- package/dist/composition/BarChart.svelte +37 -31
- package/dist/composition/BubbleChart.svelte +17 -17
- package/dist/composition/Composition.svelte +7 -11
- package/dist/composition/Formula.svelte +18 -25
- package/dist/composition/FormulaFilter.svelte +96 -98
- package/dist/composition/PieChart.svelte +60 -46
- package/dist/composition/chem-sys.js +3 -2
- package/dist/composition/format.js +4 -3
- package/dist/composition/index.d.ts +2 -0
- package/dist/composition/index.js +14 -0
- package/dist/composition/parse.d.ts +2 -3
- package/dist/composition/parse.js +27 -28
- package/dist/constants.js +11 -9
- package/dist/controls.d.ts +1 -0
- package/dist/controls.js +0 -1
- package/dist/convex-hull/ConvexHull.svelte +20 -25
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
- package/dist/convex-hull/ConvexHull2D.svelte +176 -256
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +269 -804
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +236 -755
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
- package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
- package/dist/convex-hull/ConvexHullControls.svelte +128 -49
- 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 +210 -351
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
- package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
- package/dist/convex-hull/GasPressureControls.svelte +5 -12
- package/dist/convex-hull/StructurePopup.svelte +17 -21
- package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
- package/dist/convex-hull/TemperatureSlider.svelte +4 -10
- 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 +279 -0
- package/dist/convex-hull/helpers.d.ts +47 -17
- package/dist/convex-hull/helpers.js +186 -101
- 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 +9 -7
- package/dist/convex-hull/index.js +7 -2
- package/dist/convex-hull/thermodynamics.js +109 -937
- package/dist/convex-hull/types.d.ts +12 -4
- package/dist/convex-hull/types.js +12 -0
- package/dist/coordination/CoordinationBarPlot.svelte +41 -43
- package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
- package/dist/coordination/calc-coordination.d.ts +1 -1
- package/dist/coordination/calc-coordination.js +28 -25
- package/dist/element/BohrAtom.svelte +6 -8
- package/dist/element/ElementHeading.svelte +4 -1
- package/dist/element/ElementPhoto.svelte +11 -9
- package/dist/element/ElementStats.svelte +12 -10
- package/dist/element/ElementTile.svelte +24 -29
- package/dist/element/ElementTile.svelte.d.ts +1 -1
- package/dist/element/Nucleus.svelte +4 -2
- package/dist/element/data.d.ts +1 -2
- package/dist/element/helpers.d.ts +4 -0
- package/dist/element/helpers.js +18 -0
- package/dist/element/index.d.ts +1 -0
- package/dist/element/index.js +3 -0
- package/dist/feedback/DragOverlay.svelte +5 -3
- package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
- package/dist/feedback/StatusMessage.svelte +20 -7
- package/dist/fermi-surface/FermiSlice.svelte +21 -18
- package/dist/fermi-surface/FermiSurface.svelte +78 -165
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
- package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
- package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
- package/dist/fermi-surface/compute.js +14 -13
- package/dist/fermi-surface/export.js +4 -15
- 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 +84 -97
- package/dist/fermi-surface/types.d.ts +2 -2
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
- package/dist/heatmap-matrix/index.d.ts +8 -2
- package/dist/icons.d.ts +4 -0
- package/dist/icons.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/io/ExportPane.svelte +170 -0
- package/dist/io/ExportPane.svelte.d.ts +17 -0
- package/dist/io/decompress.d.ts +2 -1
- package/dist/io/decompress.js +22 -36
- package/dist/io/export.d.ts +9 -1
- package/dist/io/export.js +37 -29
- package/dist/io/fetch.d.ts +2 -4
- package/dist/io/fetch.js +0 -11
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +13 -0
- package/dist/io/index.d.ts +1 -0
- package/dist/io/index.js +3 -0
- package/dist/io/is-binary.d.ts +9 -0
- package/dist/io/is-binary.js +45 -1
- package/dist/io/types.d.ts +13 -0
- package/dist/io/url-drop.js +13 -25
- package/dist/isosurface/Isosurface.svelte +17 -23
- package/dist/isosurface/IsosurfaceControls.svelte +37 -42
- package/dist/isosurface/parse-vaspwave.d.ts +3 -0
- package/dist/isosurface/parse-vaspwave.js +138 -0
- package/dist/isosurface/parse.js +50 -48
- package/dist/labels.d.ts +6 -2
- package/dist/labels.js +44 -14
- package/dist/layout/FullscreenButton.svelte +34 -0
- package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
- package/dist/layout/FullscreenToggle.svelte +12 -12
- package/dist/layout/InfoCard.svelte +7 -18
- package/dist/layout/InfoTag.svelte +1 -3
- package/dist/layout/NumberRangeInput.svelte +46 -0
- package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
- package/dist/layout/PropertyFilter.svelte +2 -6
- package/dist/layout/SettingsSection.svelte +21 -17
- package/dist/layout/SubpageGrid.svelte +3 -1
- package/dist/layout/ViewerChrome.svelte +115 -0
- package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
- package/dist/layout/fullscreen.d.ts +4 -0
- package/dist/layout/fullscreen.js +3 -5
- package/dist/layout/fullscreen.svelte.d.ts +8 -0
- package/dist/layout/fullscreen.svelte.js +36 -0
- package/dist/layout/index.d.ts +4 -0
- package/dist/layout/index.js +4 -0
- package/dist/layout/json-tree/JsonNode.svelte +8 -11
- package/dist/layout/json-tree/JsonTree.svelte +53 -77
- package/dist/layout/json-tree/JsonValue.svelte +7 -15
- package/dist/layout/json-tree/utils.js +14 -23
- package/dist/math.d.ts +9 -3
- package/dist/math.js +37 -21
- package/dist/overlays/ContextMenu.svelte +4 -2
- package/dist/overlays/CopyButton.svelte +11 -5
- package/dist/overlays/DragControlTab.svelte +6 -1
- package/dist/overlays/DraggablePane.svelte +81 -26
- package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
- package/dist/overlays/GlassChip.svelte +30 -0
- package/dist/overlays/GlassChip.svelte.d.ts +8 -0
- package/dist/overlays/InfoPaneCards.svelte +11 -23
- package/dist/overlays/index.d.ts +11 -0
- package/dist/overlays/index.js +26 -0
- package/dist/overlays/portal.d.ts +2 -0
- package/dist/overlays/portal.js +14 -0
- package/dist/periodic-table/PeriodicTable.svelte +115 -146
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
- package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
- package/dist/periodic-table/TableInset.svelte +9 -4
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
- package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
- 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 +49 -134
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
- package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
- package/dist/phase-diagram/build-diagram.js +3 -3
- package/dist/phase-diagram/colors.js +1 -1
- package/dist/phase-diagram/diagram-input.d.ts +1 -2
- package/dist/phase-diagram/parse.d.ts +2 -1
- package/dist/phase-diagram/parse.js +15 -15
- package/dist/phase-diagram/svg-to-diagram.js +21 -15
- package/dist/phase-diagram/utils.js +2 -3
- package/dist/plot/bar/BarPlot.svelte +494 -692
- package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
- package/dist/plot/bar/BarPlotControls.svelte +1 -3
- package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
- package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
- package/dist/plot/bar/data.d.ts +5 -4
- package/dist/plot/bar/data.js +2 -1
- package/dist/plot/box/BoxPlot.svelte +361 -439
- package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
- package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
- package/dist/plot/box/Violin.svelte.d.ts +2 -1
- package/dist/plot/box/box-plot.d.ts +3 -2
- package/dist/plot/box/box-plot.js +6 -3
- package/dist/plot/box/kde.d.ts +2 -1
- package/dist/plot/box/kde.js +4 -4
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +5 -2
- package/dist/plot/core/axis-utils.d.ts +12 -7
- package/dist/plot/core/axis-utils.js +48 -48
- package/dist/plot/core/components/AxisLabel.svelte +82 -20
- package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
- package/dist/plot/core/components/ColorBar.svelte +106 -111
- package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
- package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
- package/dist/plot/core/components/FillArea.svelte +10 -17
- package/dist/plot/core/components/HierarchyControls.svelte +249 -0
- package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
- package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
- package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/core/components/Line.svelte +21 -11
- package/dist/plot/core/components/Line.svelte.d.ts +5 -2
- package/dist/plot/core/components/PlotAxis.svelte +42 -18
- package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
- package/dist/plot/core/components/PlotControls.svelte +65 -41
- package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/core/components/PlotLegend.svelte +31 -35
- package/dist/plot/core/components/PlotMarginals.svelte +616 -0
- package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
- package/dist/plot/core/components/PlotTooltip.svelte +1 -1
- package/dist/plot/core/components/PortalSelect.svelte +4 -8
- package/dist/plot/core/components/ReferenceLine.svelte +27 -24
- package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
- package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
- package/dist/plot/core/components/ReferencePlane.svelte +9 -10
- package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
- package/dist/plot/core/components/ZeroLines.svelte +4 -8
- package/dist/plot/core/components/ZoomRect.svelte +1 -2
- package/dist/plot/core/components/index.d.ts +1 -0
- package/dist/plot/core/components/index.js +1 -0
- package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
- package/dist/plot/core/data-cleaning-signal.js +467 -0
- package/dist/plot/core/data-cleaning.d.ts +2 -42
- package/dist/plot/core/data-cleaning.js +29 -476
- package/dist/plot/core/data-transform.js +9 -5
- package/dist/plot/core/fill-utils.d.ts +7 -4
- package/dist/plot/core/fill-utils.js +34 -9
- package/dist/plot/core/index.d.ts +1 -0
- package/dist/plot/core/index.js +1 -0
- package/dist/plot/core/interactions.d.ts +5 -1
- package/dist/plot/core/interactions.js +14 -2
- package/dist/plot/core/layout.d.ts +3 -1
- package/dist/plot/core/layout.js +22 -13
- package/dist/plot/core/marginals.d.ts +170 -0
- package/dist/plot/core/marginals.js +436 -0
- 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/core/reference-line.d.ts +10 -10
- package/dist/plot/core/reference-line.js +7 -7
- package/dist/plot/core/scales.d.ts +40 -25
- package/dist/plot/core/scales.js +49 -21
- package/dist/plot/core/svg.d.ts +2 -1
- package/dist/plot/core/types/fills.d.ts +101 -0
- package/dist/plot/core/types/fills.js +15 -0
- package/dist/plot/core/types/plot-3d.d.ts +87 -0
- package/dist/plot/core/types/plot-3d.js +4 -0
- package/dist/plot/core/types/reference-lines.d.ts +140 -0
- package/dist/plot/core/types/reference-lines.js +9 -0
- package/dist/plot/core/types.d.ts +47 -331
- package/dist/plot/core/types.js +23 -20
- package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
- package/dist/plot/core/utils/hierarchy-chart.js +179 -0
- package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
- package/dist/plot/core/utils/hierarchy-labels.js +99 -0
- package/dist/plot/core/utils/label-placement.d.ts +1 -1
- package/dist/plot/core/utils/label-placement.js +5 -5
- package/dist/plot/core/utils.d.ts +2 -1
- package/dist/plot/histogram/Histogram.svelte +373 -631
- package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
- package/dist/plot/histogram/HistogramControls.svelte +30 -28
- package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/histogram/histogram.d.ts +31 -0
- package/dist/plot/histogram/histogram.js +70 -0
- package/dist/plot/histogram/index.d.ts +1 -0
- package/dist/plot/histogram/index.js +1 -0
- package/dist/plot/index.d.ts +1 -0
- package/dist/plot/index.js +1 -0
- package/dist/plot/sankey/Sankey.svelte +79 -73
- package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
- package/dist/plot/sankey/SankeyControls.svelte +22 -16
- package/dist/plot/sankey/sankey.js +3 -1
- package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
- package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
- package/dist/plot/scatter/ElementScatter.svelte +4 -1
- package/dist/plot/scatter/ScatterPlot.svelte +623 -821
- package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
- package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
- package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/scatter/ScatterPoint.svelte +16 -9
- package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
- package/dist/plot/scatter/binned-scatter-types.js +6 -1
- package/dist/plot/scatter/index.d.ts +1 -1
- package/dist/plot/scatter/scatter-data.d.ts +1 -1
- package/dist/plot/scatter/scatter-data.js +8 -7
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
- package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
- package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
- package/dist/plot/sunburst/Sunburst.svelte +256 -338
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
- package/dist/plot/sunburst/index.d.ts +1 -1
- package/dist/plot/sunburst/index.js +2 -1
- package/dist/plot/sunburst/render.d.ts +2 -2
- package/dist/plot/sunburst/render.js +47 -22
- package/dist/plot/sunburst/sunburst.d.ts +5 -1
- package/dist/plot/sunburst/sunburst.js +14 -4
- package/dist/plot/treemap/Treemap.svelte +907 -0
- package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
- package/dist/plot/treemap/index.d.ts +3 -0
- package/dist/plot/treemap/index.js +4 -0
- package/dist/plot/treemap/treemap.d.ts +26 -0
- package/dist/plot/treemap/treemap.js +99 -0
- package/dist/rdf/RdfPlot.svelte +22 -26
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/calc-rdf.js +4 -8
- 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 +22 -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 +33 -11
- package/dist/settings.js +136 -51
- package/dist/spectral/Bands.svelte +231 -237
- package/dist/spectral/Bands.svelte.d.ts +3 -2
- package/dist/spectral/BandsAndDos.svelte +29 -29
- package/dist/spectral/BrillouinBandsDos.svelte +21 -29
- package/dist/spectral/Dos.svelte +75 -77
- package/dist/spectral/Dos.svelte.d.ts +2 -1
- package/dist/spectral/helpers.d.ts +9 -9
- package/dist/spectral/helpers.js +66 -70
- package/dist/state.svelte.d.ts +0 -7
- package/dist/state.svelte.js +5 -24
- package/dist/structure/Arrow.svelte +6 -21
- package/dist/structure/ArrowInstances.svelte +160 -0
- package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
- package/dist/structure/AtomLegend.svelte +115 -75
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Bond.svelte +31 -23
- package/dist/structure/CanvasTooltip.svelte +16 -3
- package/dist/structure/CellSelect.svelte +22 -29
- package/dist/structure/CellSelect.svelte.d.ts +2 -1
- package/dist/structure/Cylinder.svelte +24 -40
- package/dist/structure/InstancedAtoms.svelte +114 -0
- package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
- package/dist/structure/Lattice.svelte +85 -149
- package/dist/structure/SiteLabels.svelte +122 -0
- package/dist/structure/SiteLabels.svelte.d.ts +18 -0
- package/dist/structure/Structure.svelte +725 -735
- package/dist/structure/Structure.svelte.d.ts +8 -16
- package/dist/structure/StructureCarousel.svelte +630 -0
- package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
- package/dist/structure/StructureControls.svelte +298 -325
- package/dist/structure/StructureControls.svelte.d.ts +5 -3
- package/dist/structure/StructureExportPane.svelte +62 -168
- package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
- package/dist/structure/StructureInfoPane.svelte +77 -69
- package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
- package/dist/structure/StructureScene.svelte +758 -600
- package/dist/structure/StructureScene.svelte.d.ts +24 -20
- package/dist/structure/StructureViewport.svelte +386 -0
- package/dist/structure/StructureViewport.svelte.d.ts +63 -0
- package/dist/structure/atom-label-placement.d.ts +18 -0
- package/dist/structure/atom-label-placement.js +115 -0
- package/dist/structure/atom-properties.d.ts +3 -1
- package/dist/structure/atom-properties.js +115 -54
- package/dist/structure/bond-order-perception.js +2 -4
- package/dist/structure/bonding.d.ts +12 -2
- package/dist/structure/bonding.js +172 -73
- package/dist/structure/edit-history.d.ts +6 -0
- package/dist/structure/edit-history.js +27 -0
- package/dist/structure/export.d.ts +25 -4
- package/dist/structure/export.js +103 -153
- package/dist/structure/format-detect.d.ts +1 -0
- package/dist/structure/format-detect.js +22 -0
- package/dist/structure/geometry.d.ts +9 -0
- package/dist/structure/geometry.js +27 -0
- package/dist/structure/index.d.ts +18 -1
- package/dist/structure/index.js +15 -2
- package/dist/structure/measure.d.ts +1 -1
- package/dist/structure/measure.js +6 -12
- package/dist/structure/parse.d.ts +5 -3
- package/dist/structure/parse.js +551 -567
- package/dist/structure/partial-occupancy.d.ts +0 -1
- package/dist/structure/partial-occupancy.js +5 -3
- package/dist/structure/pbc.d.ts +1 -1
- package/dist/structure/pbc.js +204 -51
- package/dist/structure/polyhedra.d.ts +40 -0
- package/dist/structure/polyhedra.js +587 -0
- package/dist/structure/site.d.ts +4 -0
- package/dist/structure/site.js +1 -0
- package/dist/structure/supercell.d.ts +1 -2
- package/dist/structure/supercell.js +10 -15
- 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 +350 -0
- package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
- package/dist/symmetry/SymmetryStats.svelte +142 -49
- package/dist/symmetry/WyckoffTable.svelte +58 -16
- package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
- package/dist/symmetry/cell-transform.js +8 -14
- package/dist/symmetry/index.d.ts +15 -4
- package/dist/symmetry/index.js +308 -81
- package/dist/symmetry/spacegroups.d.ts +5 -1
- package/dist/symmetry/spacegroups.js +18 -3
- 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 +1104 -378
- package/dist/table/HeatmapTable.svelte.d.ts +12 -2
- package/dist/table/ToggleMenu.svelte +5 -12
- package/dist/table/index.d.ts +21 -6
- package/dist/table/index.js +34 -21
- package/dist/theme/ThemeControl.svelte +16 -8
- package/dist/theme/index.d.ts +1 -5
- package/dist/theme/index.js +4 -10
- package/dist/theme/themes.mjs +14 -7
- package/dist/tooltip/KCoords.svelte +4 -1
- package/dist/tooltip/TooltipContent.svelte +2 -5
- package/dist/trajectory/Trajectory.svelte +556 -352
- package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
- package/dist/trajectory/TrajectoryError.svelte +11 -8
- package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
- package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
- package/dist/trajectory/constants.js +6 -2
- package/dist/trajectory/extract.js +73 -71
- package/dist/trajectory/format-detect.d.ts +0 -1
- package/dist/trajectory/format-detect.js +13 -17
- package/dist/trajectory/frame-reader.d.ts +2 -1
- package/dist/trajectory/frame-reader.js +74 -136
- package/dist/trajectory/helpers.d.ts +12 -2
- package/dist/trajectory/helpers.js +68 -37
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +3 -0
- 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/h5-utils.d.ts +13 -0
- package/dist/trajectory/parse/h5-utils.js +109 -0
- package/dist/trajectory/parse/hdf5.d.ts +1 -1
- package/dist/trajectory/parse/hdf5.js +116 -106
- package/dist/trajectory/parse/index.d.ts +3 -4
- package/dist/trajectory/parse/index.js +68 -119
- package/dist/trajectory/parse/lammps.d.ts +0 -2
- package/dist/trajectory/parse/lammps.js +14 -16
- package/dist/trajectory/parse/pymatgen.d.ts +2 -0
- package/dist/trajectory/parse/pymatgen.js +74 -0
- package/dist/trajectory/parse/vasp.js +13 -9
- package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
- package/dist/trajectory/parse/vaspout-electronic.js +207 -0
- package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
- package/dist/trajectory/parse/vaspout-h5.js +284 -0
- package/dist/trajectory/parse/xyz.d.ts +9 -21
- package/dist/trajectory/parse/xyz.js +38 -39
- package/dist/trajectory/plotting.d.ts +8 -10
- package/dist/trajectory/plotting.js +102 -127
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +8 -1
- package/dist/xrd/XrdPlot.svelte +56 -85
- package/dist/xrd/broadening.d.ts +2 -1
- package/dist/xrd/broadening.js +2 -1
- package/dist/xrd/calc-xrd.js +8 -15
- package/dist/xrd/index.d.ts +2 -2
- package/dist/xrd/parse.d.ts +0 -1
- package/dist/xrd/parse.js +56 -73
- package/package.json +77 -36
- package/dist/api/mp.d.ts +0 -6
- package/dist/api/mp.js +0 -22
- 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/core/hover-lock.svelte.d.ts +0 -14
- package/dist/plot/core/hover-lock.svelte.js +0 -45
- package/dist/plot/sunburst/SunburstControls.svelte +0 -200
- package/dist/structure/label-placement.d.ts +0 -14
- package/dist/structure/label-placement.js +0 -72
package/dist/structure/parse.js
CHANGED
|
@@ -1,14 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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';
|
|
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 { 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';
|
|
6
9
|
import { load as yaml_load } from 'js-yaml';
|
|
10
|
+
export { is_structure_file } from './format-detect';
|
|
11
|
+
// === Parse error contract ===
|
|
12
|
+
// Individual format parsers (parse_poscar, parse_cif, parse_xyz, parse_phonopy_yaml,
|
|
13
|
+
// parse_optimade_json, ...) return `T | null` on failure and record failure reasons in a
|
|
14
|
+
// module-level collector (mirrored to the console). The top-level entry points
|
|
15
|
+
// parse_structure_file and parse_any_structure reset the collector on entry and THROW a
|
|
16
|
+
// descriptive Error aggregating the recorded reasons when nothing parses, so failure
|
|
17
|
+
// causes can reach the UI (callers surface error.message). Warnings (element-symbol
|
|
18
|
+
// fallbacks, skipped atoms, ...) never fail a parse and only go to the console.
|
|
19
|
+
let parse_errors = [];
|
|
20
|
+
const reset_parse_diagnostics = () => {
|
|
21
|
+
parse_errors = [];
|
|
22
|
+
};
|
|
23
|
+
// Record a failure reason; with `error` present, logs in `console.error('msg:', error)` form
|
|
24
|
+
const diag_error = (message, error) => {
|
|
25
|
+
const detail = error === undefined ? `` : `: ${to_error(error).message}`;
|
|
26
|
+
parse_errors.push(`${message}${detail}`);
|
|
27
|
+
if (error === undefined)
|
|
28
|
+
console.error(message);
|
|
29
|
+
else
|
|
30
|
+
console.error(`${message}:`, error);
|
|
31
|
+
};
|
|
32
|
+
const diag_warn = (message) => console.warn(message);
|
|
33
|
+
// Aggregate recorded failure reasons into the Error thrown by top-level entry points
|
|
34
|
+
const aggregate_parse_error = (filename) => {
|
|
35
|
+
const reasons = [...new Set(parse_errors)];
|
|
36
|
+
const detail = reasons.length ? `: ${reasons.join(`; `)}` : ``;
|
|
37
|
+
return new Error(`Failed to parse structure${filename ? ` from '${filename}'` : ``}${detail}`);
|
|
38
|
+
};
|
|
7
39
|
const cif_coords_key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
|
|
8
40
|
const cif_site_key = (element, abc, label) => `${element}|${label}|${cif_coords_key(abc)}`;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
41
|
+
// Bravais lattice centering translations (excluding the identity) keyed by the
|
|
42
|
+
// leading letter of a space-group Hermann-Mauguin symbol. R is the obverse
|
|
43
|
+
// hexagonal setting.
|
|
44
|
+
const CENTERING_VECTORS = {
|
|
45
|
+
P: [],
|
|
46
|
+
I: [[0.5, 0.5, 0.5]],
|
|
47
|
+
F: [
|
|
48
|
+
[0, 0.5, 0.5],
|
|
49
|
+
[0.5, 0, 0.5],
|
|
50
|
+
[0.5, 0.5, 0],
|
|
51
|
+
],
|
|
52
|
+
A: [[0, 0.5, 0.5]],
|
|
53
|
+
B: [[0.5, 0, 0.5]],
|
|
54
|
+
C: [[0.5, 0.5, 0]],
|
|
55
|
+
R: [
|
|
56
|
+
[2 / 3, 1 / 3, 1 / 3],
|
|
57
|
+
[1 / 3, 2 / 3, 2 / 3],
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
// Detect the centering letter from a CIF's space-group H-M symbol, if present.
|
|
61
|
+
const extract_cif_centering = (text) => {
|
|
62
|
+
for (const line of text.split(`\n`)) {
|
|
63
|
+
const match = /^_(?:symmetry_space_group_name_h-m|space_group_name_h-m(?:_alt)?)\s+(?<symbol>.+)/i.exec(line.trim());
|
|
64
|
+
const letter = match?.groups?.symbol.replaceAll(/['"]/g, ``).trim()[0]?.toUpperCase();
|
|
65
|
+
if (letter && letter in CENTERING_VECTORS)
|
|
66
|
+
return letter;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
12
70
|
const vec3_from_values = (values, context) => {
|
|
13
71
|
if (values?.length !== 3) {
|
|
14
72
|
throw new Error(`Invalid ${context}: expected 3 coordinates, got ${values?.length ?? 0}`);
|
|
@@ -27,7 +85,7 @@ const vec3_from_values = (values, context) => {
|
|
|
27
85
|
// Parse a coordinate value that might be in various scientific notation formats
|
|
28
86
|
function parse_coordinate(str) {
|
|
29
87
|
const normalized = normalize_scientific_notation(str.trim());
|
|
30
|
-
const value =
|
|
88
|
+
const value = Number(normalized);
|
|
31
89
|
if (isNaN(value))
|
|
32
90
|
throw new Error(`Invalid coordinate value: ${str}`);
|
|
33
91
|
return value;
|
|
@@ -41,9 +99,9 @@ function parse_coordinate_line(line) {
|
|
|
41
99
|
const sanitized = line
|
|
42
100
|
.trim()
|
|
43
101
|
// Add space when '-' follows a digit and precedes a digit or dot
|
|
44
|
-
.replaceAll(/(
|
|
102
|
+
.replaceAll(/(?<digit>\d)-(?=[\d.])/g, `$1 -`)
|
|
45
103
|
// Revert accidental spaces after exponent markers
|
|
46
|
-
.replaceAll(/([eE])\s-\s/g, `$1-`);
|
|
104
|
+
.replaceAll(/(?<exp_marker>[eE])\s-\s/g, `$1-`);
|
|
47
105
|
tokens = sanitized.split(/\s+/);
|
|
48
106
|
}
|
|
49
107
|
if (tokens.length < 3)
|
|
@@ -54,11 +112,11 @@ function parse_coordinate_line(line) {
|
|
|
54
112
|
function validate_element_symbol(symbol, index) {
|
|
55
113
|
// Clean symbol (remove suffixes like _pv, /hash)
|
|
56
114
|
const clean_symbol = symbol.split(/[_/]/)[0];
|
|
57
|
-
if (
|
|
115
|
+
if (is_elem_symbol(clean_symbol))
|
|
58
116
|
return clean_symbol;
|
|
59
117
|
// Fallback to default elements by atomic number
|
|
60
118
|
const fallback = FALLBACK_ELEMENTS[index % FALLBACK_ELEMENTS.length] ?? `H`;
|
|
61
|
-
|
|
119
|
+
diag_warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
|
|
62
120
|
return fallback;
|
|
63
121
|
}
|
|
64
122
|
// Per OPTIMADE spec, species_at_sites holds species NAMES (e.g. 'Si1') resolved via the
|
|
@@ -70,7 +128,7 @@ function resolve_optimade_element(species_name, species_list, index) {
|
|
|
70
128
|
const spec = species_list?.find((entry) => entry.name === species_name);
|
|
71
129
|
let best;
|
|
72
130
|
for (const [sym_idx, symbol] of (spec?.chemical_symbols ?? []).entries()) {
|
|
73
|
-
if (!
|
|
131
|
+
if (!is_elem_symbol(symbol))
|
|
74
132
|
continue;
|
|
75
133
|
const conc = spec?.concentration?.[sym_idx] ?? 0;
|
|
76
134
|
if (!best || conc > best.conc)
|
|
@@ -81,37 +139,51 @@ function resolve_optimade_element(species_name, species_list, index) {
|
|
|
81
139
|
// Fallback: the name may be an element with a trailing atom index (e.g. 'O1');
|
|
82
140
|
// element symbols never contain digits, so stripping them is safe
|
|
83
141
|
const stripped = species_name.replace(/\d+$/, ``);
|
|
84
|
-
if (
|
|
142
|
+
if (is_elem_symbol(stripped))
|
|
85
143
|
return { symbol: stripped, sym_idx: -1 };
|
|
86
144
|
return { symbol: validate_element_symbol(species_name, index), sym_idx: -1 };
|
|
87
145
|
}
|
|
88
|
-
const try_create_cart_to_frac = (lattice_matrix) => {
|
|
89
|
-
try {
|
|
90
|
-
return math.create_cart_to_frac(lattice_matrix);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
146
|
const approximate_cart_to_frac = (xyz, axis_lengths) => [
|
|
97
147
|
Math.abs(axis_lengths[0]) > math.EPS ? xyz[0] / axis_lengths[0] : 0,
|
|
98
148
|
Math.abs(axis_lengths[1]) > math.EPS ? xyz[1] / axis_lengths[1] : 0,
|
|
99
149
|
Math.abs(axis_lengths[2]) > math.EPS ? xyz[2] / axis_lengths[2] : 0,
|
|
100
150
|
];
|
|
101
|
-
//
|
|
151
|
+
// Build a 3x3 matrix from 3 row vectors; error context is suffixed with the 1-based row index
|
|
152
|
+
const matrix3x3_from_rows = (rows, context) => [
|
|
153
|
+
vec3_from_values(rows[0], `${context} 1`),
|
|
154
|
+
vec3_from_values(rows[1], `${context} 2`),
|
|
155
|
+
vec3_from_values(rows[2], `${context} 3`),
|
|
156
|
+
];
|
|
157
|
+
// cart→frac converter that falls back to per-axis-length division for singular lattices.
|
|
158
|
+
// axis_lengths defaults to the row norms of the lattice matrix.
|
|
159
|
+
const cart_to_frac_with_fallback = (matrix, axis_lengths) => {
|
|
160
|
+
try {
|
|
161
|
+
return { convert: math.create_cart_to_frac(matrix), exact: true };
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// fall through to the per-axis-length approximation below
|
|
165
|
+
}
|
|
166
|
+
const lengths = axis_lengths ?? [
|
|
167
|
+
Math.hypot(...matrix[0]),
|
|
168
|
+
Math.hypot(...matrix[1]),
|
|
169
|
+
Math.hypot(...matrix[2]),
|
|
170
|
+
];
|
|
171
|
+
return { convert: (xyz) => approximate_cart_to_frac(xyz, lengths), exact: false };
|
|
172
|
+
};
|
|
173
|
+
// @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse VASP POSCAR.
|
|
102
174
|
export function parse_poscar(content) {
|
|
103
175
|
try {
|
|
104
176
|
// Strip only horizontal whitespace: a blank first (comment) line is valid POSCAR
|
|
105
177
|
const lines = content.replace(/^[ \t]+/, ``).split(/\r?\n/);
|
|
106
178
|
if (lines.length < 8) {
|
|
107
|
-
|
|
179
|
+
diag_error(`POSCAR file too short`);
|
|
108
180
|
return null;
|
|
109
181
|
}
|
|
110
182
|
// Scale line: one value (negative = target volume) or three per-axis Cartesian factors
|
|
111
183
|
const scale_tokens = lines[1].trim().split(/\s+/).map(parseFloat);
|
|
112
184
|
let scale_factor = scale_tokens[0];
|
|
113
185
|
if (isNaN(scale_factor)) {
|
|
114
|
-
|
|
186
|
+
diag_error(`Invalid scaling factor in POSCAR`);
|
|
115
187
|
return null;
|
|
116
188
|
}
|
|
117
189
|
const scale_vec = scale_tokens.slice(0, 3);
|
|
@@ -129,6 +201,10 @@ export function parse_poscar(content) {
|
|
|
129
201
|
// Handle negative scale factor (volume-based scaling, single-factor form only)
|
|
130
202
|
if (!per_axis_scale && scale_factor < 0) {
|
|
131
203
|
const volume = Math.abs(math.det_3x3(lattice_vecs));
|
|
204
|
+
if (volume < math.EPS) {
|
|
205
|
+
diag_error(`POSCAR target-volume scaling requires a non-singular lattice`);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
132
208
|
scale_factor = (-scale_factor / volume) ** (1 / 3);
|
|
133
209
|
}
|
|
134
210
|
// Scale lattice vectors (per-axis factors multiply Cartesian components)
|
|
@@ -141,9 +217,7 @@ export function parse_poscar(content) {
|
|
|
141
217
|
let atom_counts = [];
|
|
142
218
|
// Detect if this is VASP 5+ format (has element symbols)
|
|
143
219
|
// Try to parse the first token as a number - if it succeeds, it's VASP 4 format
|
|
144
|
-
const
|
|
145
|
-
const first_token_as_number = parseInt(first_token, 10);
|
|
146
|
-
const has_element_symbols = isNaN(first_token_as_number);
|
|
220
|
+
const has_element_symbols = isNaN(parse_leading_num(lines[line_index]));
|
|
147
221
|
if (has_element_symbols) {
|
|
148
222
|
// VASP 5+ format - parse element symbols (may span multiple lines)
|
|
149
223
|
let symbol_lines = 1;
|
|
@@ -151,9 +225,7 @@ export function parse_poscar(content) {
|
|
|
151
225
|
for (let lookahead_idx = 1; lookahead_idx < 10; lookahead_idx++) {
|
|
152
226
|
if (line_index + lookahead_idx >= lines.length)
|
|
153
227
|
break;
|
|
154
|
-
|
|
155
|
-
const next_token_as_number = parseInt(next_line_first_token, 10);
|
|
156
|
-
if (!isNaN(next_token_as_number)) {
|
|
228
|
+
if (!isNaN(parse_leading_num(lines[line_index + lookahead_idx]))) {
|
|
157
229
|
symbol_lines = lookahead_idx;
|
|
158
230
|
break;
|
|
159
231
|
}
|
|
@@ -183,201 +255,163 @@ export function parse_poscar(content) {
|
|
|
183
255
|
line_index += 1;
|
|
184
256
|
}
|
|
185
257
|
if (element_symbols.length !== atom_counts.length) {
|
|
186
|
-
|
|
258
|
+
diag_error(`Mismatch between element symbols and atom counts`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (line_index >= lines.length) {
|
|
262
|
+
diag_error(`Missing coordinate mode line in POSCAR`);
|
|
187
263
|
return null;
|
|
188
264
|
}
|
|
189
265
|
// Check for selective dynamics
|
|
190
266
|
let has_selective_dynamics = false;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
coordinate_mode = lines[line_index].trim().toUpperCase();
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
console.error(`Missing coordinate mode after selective dynamics`);
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
267
|
+
let coordinate_mode = lines[line_index].trim().toUpperCase();
|
|
268
|
+
if (coordinate_mode.startsWith(`S`)) {
|
|
269
|
+
has_selective_dynamics = true;
|
|
270
|
+
line_index += 1;
|
|
271
|
+
if (line_index < lines.length) {
|
|
272
|
+
coordinate_mode = lines[line_index].trim().toUpperCase();
|
|
203
273
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const is_cartesian = coordinate_mode.startsWith(`C`) || coordinate_mode.startsWith(`K`);
|
|
207
|
-
if (!is_direct && !is_cartesian) {
|
|
208
|
-
console.error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
|
|
274
|
+
else {
|
|
275
|
+
diag_error(`Missing coordinate mode after selective dynamics`);
|
|
209
276
|
return null;
|
|
210
277
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
278
|
+
}
|
|
279
|
+
// Determine coordinate mode
|
|
280
|
+
const is_direct = coordinate_mode.startsWith(`D`);
|
|
281
|
+
const is_cartesian = coordinate_mode.startsWith(`C`) || coordinate_mode.startsWith(`K`);
|
|
282
|
+
if (!is_direct && !is_cartesian) {
|
|
283
|
+
diag_error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
// Parse atomic positions
|
|
287
|
+
const poscar_frac_to_cart = math.create_frac_to_cart(scaled_lattice);
|
|
288
|
+
const poscar_cart_to_frac = cart_to_frac_with_fallback(scaled_lattice);
|
|
289
|
+
if (!is_direct && !poscar_cart_to_frac.exact) {
|
|
290
|
+
diag_warn(`POSCAR: singular lattice, using axis-length fallback for cart→frac`);
|
|
291
|
+
}
|
|
292
|
+
const sites = [];
|
|
293
|
+
let atom_index = 0;
|
|
294
|
+
for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
|
|
295
|
+
const element = validate_element_symbol(element_symbols[elem_idx], elem_idx);
|
|
296
|
+
const count = atom_counts[elem_idx];
|
|
297
|
+
for (let atom_count_idx = 0; atom_count_idx < count; atom_count_idx++) {
|
|
298
|
+
const coord_line_idx = line_index + 1 + atom_index + atom_count_idx;
|
|
299
|
+
if (coord_line_idx >= lines.length) {
|
|
300
|
+
diag_error(`Not enough coordinate lines in POSCAR`);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const coords = vec3_from_values(parse_coordinate_line(lines[coord_line_idx]), `POSCAR atom coordinates on line ${coord_line_idx + 1}`);
|
|
304
|
+
// Parse selective dynamics if present
|
|
305
|
+
let selective_dynamics;
|
|
306
|
+
if (has_selective_dynamics) {
|
|
307
|
+
const tokens = lines[coord_line_idx].trim().split(/\s+/);
|
|
308
|
+
if (tokens.length >= 6) {
|
|
309
|
+
selective_dynamics = [tokens[3] === `T`, tokens[4] === `T`, tokens[5] === `T`];
|
|
241
310
|
}
|
|
242
|
-
// Cartesian input is scaled then converted to fractional (axis-length fallback
|
|
243
|
-
// for singular lattices); abc wraps to [0, 1) and xyz is recomputed from it so
|
|
244
|
-
// both stay consistent (singular Cartesian keeps the scaled input as xyz)
|
|
245
|
-
const cart = is_direct ? null : apply_axis_scale(coords);
|
|
246
|
-
const raw_abc = cart
|
|
247
|
-
? (poscar_cart_to_frac?.(cart) ??
|
|
248
|
-
approximate_cart_to_frac(cart, poscar_axis_lengths))
|
|
249
|
-
: coords;
|
|
250
|
-
const abc = wrap_to_unit_cell(raw_abc);
|
|
251
|
-
const xyz = cart && !poscar_cart_to_frac ? cart : poscar_frac_to_cart(abc);
|
|
252
|
-
const site = {
|
|
253
|
-
species: [{ element, occu: 1, oxidation_state: 0 }],
|
|
254
|
-
abc,
|
|
255
|
-
xyz,
|
|
256
|
-
label: `${element}${atom_index + atom_count_idx + 1}`,
|
|
257
|
-
properties: selective_dynamics ? { selective_dynamics } : {},
|
|
258
|
-
};
|
|
259
|
-
sites.push(site);
|
|
260
311
|
}
|
|
261
|
-
|
|
312
|
+
// Cartesian input is scaled then converted to fractional (axis-length fallback
|
|
313
|
+
// for singular lattices); abc wraps to [0, 1) and xyz is recomputed from it so
|
|
314
|
+
// both stay consistent (singular Cartesian keeps the scaled input as xyz)
|
|
315
|
+
const cart = is_direct ? null : apply_axis_scale(coords);
|
|
316
|
+
const raw_abc = cart ? poscar_cart_to_frac.convert(cart) : coords;
|
|
317
|
+
const abc = wrap_to_unit_cell(raw_abc);
|
|
318
|
+
const xyz = cart && !poscar_cart_to_frac.exact ? cart : poscar_frac_to_cart(abc);
|
|
319
|
+
sites.push(make_site(element, abc, xyz, `${element}${atom_index + atom_count_idx + 1}`, selective_dynamics ? { selective_dynamics } : {}));
|
|
262
320
|
}
|
|
263
|
-
|
|
264
|
-
const structure = {
|
|
265
|
-
sites,
|
|
266
|
-
lattice: { matrix: scaled_lattice, ...lattice_params },
|
|
267
|
-
};
|
|
268
|
-
return structure;
|
|
321
|
+
atom_index += count;
|
|
269
322
|
}
|
|
270
|
-
|
|
271
|
-
return
|
|
323
|
+
const lattice_params = math.calc_lattice_params(scaled_lattice);
|
|
324
|
+
return { sites, lattice: { matrix: scaled_lattice, ...lattice_params } };
|
|
272
325
|
}
|
|
273
326
|
catch (error) {
|
|
274
|
-
|
|
327
|
+
diag_error(`Error parsing POSCAR file`, error);
|
|
275
328
|
return null;
|
|
276
329
|
}
|
|
277
330
|
}
|
|
278
|
-
//
|
|
331
|
+
// @internal parser exported for tests + trajectory parser; public entry points: parse_structure_file/parse_any_structure. Parse standard/extended XYZ (multi-frame).
|
|
279
332
|
export function parse_xyz(content) {
|
|
280
333
|
try {
|
|
281
334
|
const normalized_content = content.trim();
|
|
282
335
|
if (!normalized_content) {
|
|
283
|
-
|
|
336
|
+
diag_error(`Empty XYZ file`);
|
|
284
337
|
return null;
|
|
285
338
|
}
|
|
286
|
-
//
|
|
339
|
+
// Walk frames by reading atom counts; multi-frame XYZ parses only the last frame
|
|
287
340
|
const all_lines = normalized_content.split(/\r?\n/);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const frameLines = all_lines.slice(frame_line_idx, frame_line_idx + numAtoms + 2);
|
|
296
|
-
frames.push(frameLines.join(`\n`));
|
|
297
|
-
frame_line_idx += numAtoms + 2;
|
|
298
|
-
}
|
|
299
|
-
else
|
|
300
|
-
frame_line_idx++;
|
|
301
|
-
}
|
|
302
|
-
// If no frames found, try simple parsing
|
|
303
|
-
if (frames.length === 0)
|
|
304
|
-
frames.push(normalized_content);
|
|
305
|
-
// Parse the last frame (or only frame)
|
|
306
|
-
const frame_content = frames.at(-1) ?? ``;
|
|
307
|
-
const lines = frame_content.trim().split(/\r?\n/);
|
|
341
|
+
let last_frame = null;
|
|
342
|
+
for (const frame of iter_xyz_frames(all_lines))
|
|
343
|
+
last_frame = frame;
|
|
344
|
+
// If no complete frame found, fall back to parsing the whole content as one frame
|
|
345
|
+
const lines = last_frame
|
|
346
|
+
? all_lines.slice(last_frame.start, last_frame.start + last_frame.num_atoms + 2)
|
|
347
|
+
: all_lines;
|
|
308
348
|
if (lines.length < 2) {
|
|
309
|
-
|
|
349
|
+
diag_error(`XYZ frame too short`);
|
|
310
350
|
return null;
|
|
311
351
|
}
|
|
312
|
-
// Parse number of atoms (line 1)
|
|
313
|
-
|
|
352
|
+
// Parse number of atoms (line 1). Only the first token counts: Tinker-style
|
|
353
|
+
// XYZ files put a title after the count (e.g. `6 methane`)
|
|
354
|
+
const num_atoms = Math.trunc(parse_leading_num(lines[0]));
|
|
314
355
|
if (isNaN(num_atoms) || num_atoms <= 0) {
|
|
315
|
-
|
|
356
|
+
diag_error(`Invalid number of atoms in XYZ file`);
|
|
316
357
|
return null;
|
|
317
358
|
}
|
|
318
359
|
// Parse comment line (line 2) - may contain lattice info for extended XYZ
|
|
319
360
|
const comment_line = lines[1];
|
|
320
361
|
let lattice;
|
|
321
362
|
// Check for extended XYZ lattice information in comment line
|
|
322
|
-
const lattice_match = /Lattice="([^"]+)"/.exec(comment_line);
|
|
363
|
+
const lattice_match = /Lattice="(?<lattice>[^"]+)"/.exec(comment_line);
|
|
323
364
|
if (lattice_match) {
|
|
324
365
|
const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
|
|
325
366
|
if (lattice_values.length === 9) {
|
|
326
|
-
const lattice_vectors = [
|
|
327
|
-
vec3_from_values(lattice_values.slice(0, 3), `XYZ lattice vector 1`),
|
|
328
|
-
vec3_from_values(lattice_values.slice(3, 6), `XYZ lattice vector 2`),
|
|
329
|
-
vec3_from_values(lattice_values.slice(6, 9), `XYZ lattice vector 3`),
|
|
330
|
-
];
|
|
367
|
+
const lattice_vectors = matrix3x3_from_rows([lattice_values.slice(0, 3), lattice_values.slice(3, 6), lattice_values.slice(6, 9)], `XYZ lattice vector`);
|
|
331
368
|
const lattice_params = math.calc_lattice_params(lattice_vectors);
|
|
332
369
|
lattice = { matrix: lattice_vectors, ...lattice_params };
|
|
333
370
|
}
|
|
334
371
|
}
|
|
335
372
|
// Parse atomic coordinates (starting from line 3)
|
|
336
|
-
const xyz_axis_lengths = lattice ? [lattice.a, lattice.b, lattice.c] : null;
|
|
337
373
|
let xyz_frac_to_cart = null;
|
|
338
374
|
let xyz_cart_to_frac = null;
|
|
339
375
|
if (lattice) {
|
|
340
376
|
xyz_frac_to_cart = math.create_frac_to_cart(lattice.matrix);
|
|
341
|
-
xyz_cart_to_frac =
|
|
377
|
+
xyz_cart_to_frac = cart_to_frac_with_fallback(lattice.matrix, [
|
|
378
|
+
lattice.a,
|
|
379
|
+
lattice.b,
|
|
380
|
+
lattice.c,
|
|
381
|
+
]).convert;
|
|
342
382
|
}
|
|
343
383
|
const sites = [];
|
|
344
384
|
for (let atom_idx = 0; atom_idx < num_atoms; atom_idx++) {
|
|
345
385
|
const line_idx = atom_idx + 2;
|
|
346
386
|
if (line_idx >= lines.length) {
|
|
347
|
-
|
|
387
|
+
diag_error(`Not enough coordinate lines in XYZ file`);
|
|
348
388
|
return null;
|
|
349
389
|
}
|
|
350
390
|
const parts = lines[line_idx].trim().split(/\s+/);
|
|
351
391
|
if (parts.length < 4) {
|
|
352
|
-
|
|
392
|
+
diag_error(`Invalid coordinate line in XYZ file`);
|
|
353
393
|
return null;
|
|
354
394
|
}
|
|
355
395
|
const element = validate_element_symbol(parts[0], atom_idx);
|
|
356
396
|
const xyz = vec3_from_values(parts.slice(1, 4).map(parse_coordinate), `XYZ atom position ${atom_idx + 1}`);
|
|
357
397
|
// Calculate fractional coordinates if lattice is available
|
|
358
398
|
let abc = [0, 0, 0];
|
|
359
|
-
if (lattice && xyz_frac_to_cart &&
|
|
360
|
-
abc = xyz_cart_to_frac
|
|
361
|
-
? xyz_cart_to_frac(xyz)
|
|
362
|
-
: approximate_cart_to_frac(xyz, xyz_axis_lengths);
|
|
399
|
+
if (lattice && xyz_frac_to_cart && xyz_cart_to_frac) {
|
|
363
400
|
// Ensure fractional coordinates are wrapped into [0, 1) for consistency
|
|
364
|
-
abc = wrap_to_unit_cell(
|
|
401
|
+
abc = wrap_to_unit_cell(xyz_cart_to_frac(xyz));
|
|
365
402
|
// Keep rendered atoms inside primary unit cell by recomputing xyz
|
|
366
403
|
const wrapped_xyz = xyz_frac_to_cart(abc);
|
|
367
404
|
xyz[0] = wrapped_xyz[0];
|
|
368
405
|
xyz[1] = wrapped_xyz[1];
|
|
369
406
|
xyz[2] = wrapped_xyz[2];
|
|
370
407
|
}
|
|
371
|
-
|
|
372
|
-
const label = `${element}${atom_idx + 1}`;
|
|
373
|
-
const site = { species, abc, xyz, label, properties: {} };
|
|
374
|
-
sites.push(site);
|
|
408
|
+
sites.push(make_site(element, abc, xyz, `${element}${atom_idx + 1}`));
|
|
375
409
|
}
|
|
376
410
|
const structure = { sites, ...(lattice && { lattice }) };
|
|
377
411
|
return structure;
|
|
378
412
|
}
|
|
379
413
|
catch (error) {
|
|
380
|
-
|
|
414
|
+
diag_error(`Error parsing XYZ file`, error);
|
|
381
415
|
return null;
|
|
382
416
|
}
|
|
383
417
|
}
|
|
@@ -394,8 +428,7 @@ const parse_symmetry_expression = (expr_input) => {
|
|
|
394
428
|
// E.g., "x-y+1/3" → ["x", "-y", "+1/3"] or "-x+y" → ["-x", "+y"]
|
|
395
429
|
const tokens = [];
|
|
396
430
|
let current_token = ``;
|
|
397
|
-
for (
|
|
398
|
-
const char = expr[idx];
|
|
431
|
+
for (const char of expr) {
|
|
399
432
|
if ((char === `+` || char === `-`) && current_token.length > 0) {
|
|
400
433
|
tokens.push(current_token);
|
|
401
434
|
current_token = char;
|
|
@@ -408,7 +441,7 @@ const parse_symmetry_expression = (expr_input) => {
|
|
|
408
441
|
tokens.push(current_token);
|
|
409
442
|
for (const token of tokens) {
|
|
410
443
|
// Check if this token is a variable term (x, y, or z with optional sign)
|
|
411
|
-
const var_match = /^([+-]?)([xyz])$/.exec(token);
|
|
444
|
+
const var_match = /^(?<sign>[+-]?)(?<axis>[xyz])$/.exec(token);
|
|
412
445
|
if (var_match) {
|
|
413
446
|
const sign = var_match[1] === `-` ? -1 : 1;
|
|
414
447
|
const var_char = var_match[2];
|
|
@@ -434,8 +467,8 @@ const parse_symmetry_expression = (expr_input) => {
|
|
|
434
467
|
// Fraction
|
|
435
468
|
const parts = num_str.split(`/`);
|
|
436
469
|
if (parts.length === 2) {
|
|
437
|
-
const numerator =
|
|
438
|
-
const denominator =
|
|
470
|
+
const numerator = Number(parts[0]);
|
|
471
|
+
const denominator = Number(parts[1]);
|
|
439
472
|
if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {
|
|
440
473
|
translation += sign * (numerator / denominator);
|
|
441
474
|
}
|
|
@@ -443,7 +476,7 @@ const parse_symmetry_expression = (expr_input) => {
|
|
|
443
476
|
}
|
|
444
477
|
else {
|
|
445
478
|
// Integer or decimal
|
|
446
|
-
const val =
|
|
479
|
+
const val = Number(num_str);
|
|
447
480
|
if (!isNaN(val)) {
|
|
448
481
|
translation += sign * val;
|
|
449
482
|
}
|
|
@@ -451,49 +484,50 @@ const parse_symmetry_expression = (expr_input) => {
|
|
|
451
484
|
}
|
|
452
485
|
return { coefficients, translation };
|
|
453
486
|
};
|
|
454
|
-
// Apply symmetry operations
|
|
455
|
-
|
|
456
|
-
|
|
487
|
+
// Apply symmetry operations (and optional lattice-centering translations) to
|
|
488
|
+
// generate all equivalent positions. Deduplication uses 6 decimal places to
|
|
489
|
+
// absorb floating point error from compound ops like x-y, -x+y.
|
|
490
|
+
const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords, centering = []) => {
|
|
491
|
+
if (symmetry_ops.length === 0 && centering.length === 0)
|
|
457
492
|
return [atom];
|
|
458
493
|
const equivalent_atoms = [];
|
|
459
494
|
const seen = new Set();
|
|
460
495
|
const wrap = (coords) => wrap_fractional_coords ? wrap_to_unit_cell(coords) : coords;
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
//
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
496
|
+
// Every generated position is also offset by each centering translation
|
|
497
|
+
const shifts = [[0, 0, 0], ...centering];
|
|
498
|
+
// Record a position plus its centering images, deduplicating on wrapped coords
|
|
499
|
+
const add_position = (coords) => {
|
|
500
|
+
for (const [dx, dy, dz] of shifts) {
|
|
501
|
+
const wrapped = wrap([coords[0] + dx, coords[1] + dy, coords[2] + dz]);
|
|
502
|
+
const key = cif_coords_key(wrapped);
|
|
503
|
+
if (seen.has(key))
|
|
504
|
+
continue;
|
|
505
|
+
seen.add(key);
|
|
506
|
+
const id = equivalent_atoms.length === 0 ? atom.id : `${atom.id}_${equivalent_atoms.length}`;
|
|
507
|
+
equivalent_atoms.push({ ...atom, coords: wrapped, id });
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
add_position(atom.coords); // base atom (+ centering images)
|
|
511
|
+
// ops arrive pre-normalized (quotes + whitespace already stripped, see normalized_ops)
|
|
467
512
|
for (const operation of symmetry_ops) {
|
|
468
|
-
const
|
|
469
|
-
const expr_str = operation_match ? operation_match[1] : operation.trim();
|
|
470
|
-
const parts = expr_str.split(`,`).map((part) => part.trim());
|
|
513
|
+
const parts = operation.split(`,`);
|
|
471
514
|
if (parts.length !== 3)
|
|
472
515
|
continue;
|
|
473
516
|
const new_coords = [0, 0, 0];
|
|
474
517
|
for (let dim = 0; dim < 3; dim++) {
|
|
475
518
|
const { coefficients, translation } = parse_symmetry_expression(parts[dim]);
|
|
476
|
-
//
|
|
477
|
-
new_coords[dim] =
|
|
478
|
-
coefficients[0] * atom.coords[0] +
|
|
479
|
-
coefficients[1] * atom.coords[1] +
|
|
480
|
-
coefficients[2] * atom.coords[2] +
|
|
481
|
-
translation;
|
|
519
|
+
// new_coord = coeff_x * x + coeff_y * y + coeff_z * z + translation
|
|
520
|
+
new_coords[dim] = math.dot(coefficients, atom.coords) + translation;
|
|
482
521
|
}
|
|
483
|
-
|
|
484
|
-
const wrapped = wrap(new_coords);
|
|
485
|
-
const cache_key = cif_coords_key(wrapped);
|
|
486
|
-
if (seen.has(cache_key))
|
|
487
|
-
continue;
|
|
488
|
-
seen.add(cache_key);
|
|
489
|
-
equivalent_atoms.push({
|
|
490
|
-
...atom,
|
|
491
|
-
coords: wrapped,
|
|
492
|
-
id: `${atom.id}_${equivalent_atoms.length}`,
|
|
493
|
-
});
|
|
522
|
+
add_position(new_coords);
|
|
494
523
|
}
|
|
495
524
|
return equivalent_atoms;
|
|
496
525
|
};
|
|
526
|
+
// Parse a CIF numeric token, stripping a trailing uncertainty like "1.234(5)"
|
|
527
|
+
const parse_cif_uncertain_number = (token) => {
|
|
528
|
+
const value = parse_num_token(token.split(`(`)[0]);
|
|
529
|
+
return isNaN(value) ? null : value;
|
|
530
|
+
};
|
|
497
531
|
const extract_cif_cell_parameters = (text, type, strict = true) => text
|
|
498
532
|
.split(`\n`)
|
|
499
533
|
.filter((line) => line.startsWith(`_${type}`))
|
|
@@ -506,11 +540,9 @@ const extract_cif_cell_parameters = (text, type, strict = true) => text
|
|
|
506
540
|
throw new Error(`Invalid CIF cell parameter line format: ${line}`);
|
|
507
541
|
return null;
|
|
508
542
|
}
|
|
509
|
-
const value =
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
throw new Error(`Invalid CIF cell parameter in line: ${line}`);
|
|
513
|
-
return null; // Return null for invalid values in non-strict mode
|
|
543
|
+
const value = parse_cif_uncertain_number(tokens[1]);
|
|
544
|
+
if (value === null && strict) {
|
|
545
|
+
throw new Error(`Invalid CIF cell parameter in line: ${line}`);
|
|
514
546
|
}
|
|
515
547
|
return value;
|
|
516
548
|
})
|
|
@@ -538,31 +570,63 @@ const build_cif_atom_site_header_indices = (headers) => {
|
|
|
538
570
|
});
|
|
539
571
|
return indices;
|
|
540
572
|
};
|
|
573
|
+
// Which coordinate triple a CIF atom-site loop provides (fractional preferred), or null
|
|
574
|
+
const cif_coords_type = (indices) => {
|
|
575
|
+
if (indices.x !== undefined && indices.y !== undefined && indices.z !== undefined) {
|
|
576
|
+
return `fract`;
|
|
577
|
+
}
|
|
578
|
+
if (indices.cart_x !== undefined &&
|
|
579
|
+
indices.cart_y !== undefined &&
|
|
580
|
+
indices.cart_z !== undefined) {
|
|
581
|
+
return `cart`;
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
};
|
|
585
|
+
// The 3 column indices for the requested coordinate type
|
|
586
|
+
const cif_coord_indices = (indices, coords_type) => coords_type === `fract`
|
|
587
|
+
? [indices.x, indices.y, indices.z]
|
|
588
|
+
: [indices.cart_x, indices.cart_y, indices.cart_z];
|
|
589
|
+
// Walk CIF loop_ blocks: yields each loop's header tags plus the index of its first data line
|
|
590
|
+
function* iter_cif_loops(lines) {
|
|
591
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
592
|
+
if (lines[idx].trim() !== `loop_`)
|
|
593
|
+
continue;
|
|
594
|
+
const headers = [];
|
|
595
|
+
let jj = idx + 1;
|
|
596
|
+
while (jj < lines.length && lines[jj].trim().startsWith(`_`)) {
|
|
597
|
+
headers.push(lines[jj].trim());
|
|
598
|
+
jj++;
|
|
599
|
+
}
|
|
600
|
+
yield { headers, data_start: jj };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Split a CIF data line into whitespace-separated tokens, keeping quoted multi-word
|
|
604
|
+
// values as single tokens and stripping the quotes
|
|
605
|
+
const split_cif_tokens = (line) => (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []).map((token) => token.replaceAll(/['"]/g, ``));
|
|
541
606
|
// Parse atom data from CIF with robust error handling
|
|
542
607
|
const parse_cif_atom_data = (raw_data, indices, coords_type) => {
|
|
543
608
|
const { label = 0, symbol = -1, occupancy = -1 } = indices;
|
|
544
|
-
const coord_indices = coords_type
|
|
545
|
-
? [indices.x, indices.y, indices.z]
|
|
546
|
-
: [indices.cart_x, indices.cart_y, indices.cart_z];
|
|
609
|
+
const coord_indices = cif_coord_indices(indices, coords_type);
|
|
547
610
|
if (coord_indices.some((idx) => idx === undefined)) {
|
|
548
611
|
throw new Error(`Missing coordinate indices`);
|
|
549
612
|
}
|
|
550
613
|
const coords_triplet = vec3_from_values(coord_indices.map((idx) => {
|
|
551
|
-
|
|
552
|
-
throw new Error(`Invalid coordinate index`);
|
|
614
|
+
// idx cannot be undefined: the `.some` guard above already threw
|
|
553
615
|
const coord_str = raw_data[idx];
|
|
554
616
|
if (!coord_str)
|
|
555
617
|
throw new Error(`Missing coordinate at index ${idx}`);
|
|
556
|
-
const coord =
|
|
557
|
-
if (
|
|
618
|
+
const coord = parse_cif_uncertain_number(coord_str);
|
|
619
|
+
if (coord === null)
|
|
558
620
|
throw new Error(`Invalid coordinate: ${coord_str}`);
|
|
559
621
|
return coord;
|
|
560
622
|
}), `CIF atom coordinates`);
|
|
561
|
-
const
|
|
562
|
-
?
|
|
563
|
-
:
|
|
564
|
-
|
|
565
|
-
const
|
|
623
|
+
const raw_occu = occupancy >= 0 && raw_data[occupancy]
|
|
624
|
+
? parse_cif_uncertain_number(raw_data[occupancy])
|
|
625
|
+
: null;
|
|
626
|
+
// invalid or zero occupancy defaults to fully occupied
|
|
627
|
+
const occu = raw_occu == null || raw_occu === 0 ? 1.0 : raw_occu;
|
|
628
|
+
const from_symbol = symbol >= 0 ? /^(?<element>[A-Z][a-z]*)/.exec(raw_data[symbol])?.[1] : undefined;
|
|
629
|
+
const element_symbol = from_symbol ?? raw_data[label]?.match(/(?:[A-Z][a-z]*)/g)?.[0];
|
|
566
630
|
if (!element_symbol) {
|
|
567
631
|
throw new Error(`Could not extract element symbol from: ${raw_data.join(` `)}`);
|
|
568
632
|
}
|
|
@@ -574,12 +638,12 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
|
|
|
574
638
|
occupancy: occu,
|
|
575
639
|
};
|
|
576
640
|
};
|
|
577
|
-
// Parse CIF (Crystallographic Information File)
|
|
641
|
+
// @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse CIF (Crystallographic Information File).
|
|
578
642
|
export function parse_cif(content, wrap_fractional_coords = true, strict = true) {
|
|
579
643
|
try {
|
|
580
644
|
const text = content.trim();
|
|
581
645
|
if (!text) {
|
|
582
|
-
|
|
646
|
+
diag_error(`CIF file is empty`);
|
|
583
647
|
return null;
|
|
584
648
|
}
|
|
585
649
|
// Find atom site loop that actually contains coordinates (fract or Cartn)
|
|
@@ -587,16 +651,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
587
651
|
let atom_headers = [];
|
|
588
652
|
const atom_data_lines = [];
|
|
589
653
|
const symmetry_ops = [];
|
|
590
|
-
for (
|
|
591
|
-
|
|
592
|
-
continue;
|
|
593
|
-
let jj = ii + 1;
|
|
594
|
-
const headers = [];
|
|
595
|
-
// Collect headers for this loop
|
|
596
|
-
while (jj < lines.length && lines[jj].trim().startsWith(`_`)) {
|
|
597
|
-
headers.push(lines[jj].trim());
|
|
598
|
-
jj++;
|
|
599
|
-
}
|
|
654
|
+
for (const { headers, data_start } of iter_cif_loops(lines)) {
|
|
655
|
+
let jj = data_start;
|
|
600
656
|
// Check if this is a symmetry operations loop
|
|
601
657
|
if (headers.some((header) => header.includes(`_symmetry_equiv_pos_as_xyz`) ||
|
|
602
658
|
header.includes(`_space_group_symop_operation_xyz`))) {
|
|
@@ -617,16 +673,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
617
673
|
continue;
|
|
618
674
|
// Check if this loop contains coordinate headers
|
|
619
675
|
const indices_preview = build_cif_atom_site_header_indices(headers);
|
|
620
|
-
|
|
621
|
-
indices_preview.y !== undefined &&
|
|
622
|
-
indices_preview.z !== undefined) ||
|
|
623
|
-
(indices_preview.cart_x !== undefined &&
|
|
624
|
-
indices_preview.cart_y !== undefined &&
|
|
625
|
-
indices_preview.cart_z !== undefined);
|
|
626
|
-
if (!has_coords) {
|
|
627
|
-
ii = jj - 1;
|
|
676
|
+
if (cif_coords_type(indices_preview) === null)
|
|
628
677
|
continue;
|
|
629
|
-
}
|
|
630
678
|
// This is the desired atom-site loop with coordinates: collect data lines
|
|
631
679
|
atom_headers = headers;
|
|
632
680
|
while (jj < lines.length) {
|
|
@@ -653,36 +701,21 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
653
701
|
break;
|
|
654
702
|
}
|
|
655
703
|
if (atom_headers.length === 0 || atom_data_lines.length === 0) {
|
|
656
|
-
|
|
704
|
+
diag_error(`No valid atom site loop found in CIF file`);
|
|
657
705
|
return null;
|
|
658
706
|
}
|
|
659
707
|
// Parse atom data with error handling
|
|
660
708
|
const header_indices = build_cif_atom_site_header_indices(atom_headers);
|
|
661
709
|
// Determine available coordinate type
|
|
662
|
-
const coords_type = header_indices
|
|
663
|
-
header_indices.y !== undefined &&
|
|
664
|
-
header_indices.z !== undefined
|
|
665
|
-
? `fract`
|
|
666
|
-
: header_indices.cart_x !== undefined &&
|
|
667
|
-
header_indices.cart_y !== undefined &&
|
|
668
|
-
header_indices.cart_z !== undefined
|
|
669
|
-
? `cart`
|
|
670
|
-
: null;
|
|
710
|
+
const coords_type = cif_coords_type(header_indices);
|
|
671
711
|
if (!coords_type) {
|
|
672
|
-
|
|
712
|
+
diag_error(`CIF atom site loop missing coordinates (fract or Cartn)`);
|
|
673
713
|
return null;
|
|
674
714
|
}
|
|
675
715
|
// Collect required coordinate indices
|
|
676
|
-
const required_indices = coords_type
|
|
677
|
-
? [header_indices.x, header_indices.y, header_indices.z]
|
|
678
|
-
: [header_indices.cart_x, header_indices.cart_y, header_indices.cart_z];
|
|
716
|
+
const required_indices = cif_coord_indices(header_indices, coords_type);
|
|
679
717
|
const atoms = atom_data_lines
|
|
680
|
-
.map(
|
|
681
|
-
// Handle quoted multi-word values by splitting only on whitespace
|
|
682
|
-
// that is not inside quotes.
|
|
683
|
-
const tokens = line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
684
|
-
return tokens.map((token) => token.replaceAll(/['"]/g, ``));
|
|
685
|
-
})
|
|
718
|
+
.map(split_cif_tokens)
|
|
686
719
|
.filter((tokens) => {
|
|
687
720
|
const { disorder } = header_indices;
|
|
688
721
|
const max_required_idx = Math.max(...required_indices);
|
|
@@ -694,20 +727,20 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
694
727
|
return parse_cif_atom_data(tokens, header_indices, coords_type);
|
|
695
728
|
}
|
|
696
729
|
catch (error) {
|
|
697
|
-
|
|
730
|
+
diag_warn(`Skipping invalid atom data: ${error}`);
|
|
698
731
|
return null;
|
|
699
732
|
}
|
|
700
733
|
})
|
|
701
734
|
.filter((atom) => atom !== null);
|
|
702
735
|
if (atoms.length === 0) {
|
|
703
|
-
|
|
736
|
+
diag_error(`No valid atoms found in CIF file`);
|
|
704
737
|
return null;
|
|
705
738
|
}
|
|
706
739
|
// Extract cell parameters and build lattice
|
|
707
740
|
const lengths = extract_cif_cell_parameters(text, `cell_length`, strict);
|
|
708
741
|
const angles = extract_cif_cell_parameters(text, `cell_angle`, strict);
|
|
709
742
|
if (lengths.length < 3 || angles.length < 3) {
|
|
710
|
-
|
|
743
|
+
diag_error(`Insufficient cell parameters in CIF file`);
|
|
711
744
|
return null;
|
|
712
745
|
}
|
|
713
746
|
// Build lattice and create sites
|
|
@@ -716,60 +749,42 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
716
749
|
const lattice_matrix = math.cell_to_lattice_matrix(a, b, c, alpha, beta, gamma);
|
|
717
750
|
const lattice_params = math.calc_lattice_params(lattice_matrix);
|
|
718
751
|
const frac_to_cart = math.create_frac_to_cart(lattice_matrix);
|
|
719
|
-
const cart_to_frac =
|
|
752
|
+
const cart_to_frac = cart_to_frac_with_fallback(lattice_matrix, [a, b, c]).convert;
|
|
720
753
|
// Create sites with coordinate conversion and symmetry operations
|
|
721
|
-
const wrap_vec3 = (
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const normalized_ops = symmetry_ops
|
|
726
|
-
.map((op) => /['"]([^'"]+)['"]/.exec(op)?.[1] ?? op.trim())
|
|
727
|
-
.map((op) => op.replaceAll(/\s+/g, ``));
|
|
728
|
-
// Rely on symmetry operations list for all centering/translations to avoid double-counting
|
|
729
|
-
// TODO: Support conventional cells with centering by discovering centering from space group metadata
|
|
730
|
-
// when present (e.g. P, I, F, C, R centering types)
|
|
731
|
-
const centering_vectors = [[0, 0, 0]];
|
|
754
|
+
const wrap_vec3 = (vec) => wrap_fractional_coords ? wrap_to_unit_cell(vec) : vec;
|
|
755
|
+
// Strip surrounding quotes and all whitespace (preserving duplicates; positions
|
|
756
|
+
// are deduplicated later). Leaves ops as bare `x,y,z`-style expressions.
|
|
757
|
+
const normalized_ops = symmetry_ops.map((op) => (/['"](?<expr>[^'"]+)['"]/.exec(op)?.groups?.expr ?? op).replaceAll(/\s+/g, ``));
|
|
732
758
|
// Inspect optional _atom_type_number_in_cell loop to see if atom sites are already expanded
|
|
733
|
-
const atom_type_counts =
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
759
|
+
const atom_type_counts = {};
|
|
760
|
+
for (const { headers, data_start } of iter_cif_loops(lines)) {
|
|
761
|
+
const hdrs = headers.map((hdr) => hdr.toLowerCase());
|
|
762
|
+
const sym_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_symbol`));
|
|
763
|
+
const num_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_number_in_cell`));
|
|
764
|
+
if (sym_idx === -1 || num_idx === -1)
|
|
765
|
+
continue;
|
|
766
|
+
for (let lj = data_start; lj < lines.length; lj++) {
|
|
767
|
+
const line = lines[lj].trim();
|
|
768
|
+
if (!line || line === `loop_` || line.startsWith(`data_`))
|
|
769
|
+
break;
|
|
770
|
+
if (line.startsWith(`#`))
|
|
738
771
|
continue;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const line = text_lines[lj].trim();
|
|
751
|
-
if (!line || line === `loop_` || line.startsWith(`data_`))
|
|
752
|
-
break;
|
|
753
|
-
if (line.startsWith(`#`)) {
|
|
754
|
-
lj++;
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
const toks = (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []).map((tok) => tok.replaceAll(/['"]/g, ``));
|
|
758
|
-
if (toks.length > Math.max(sym_idx, num_idx)) {
|
|
759
|
-
// Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
|
|
760
|
-
const match = /^([A-Z][a-z]*)/.exec(toks[sym_idx]);
|
|
761
|
-
const sym = match ? match[1] : toks[sym_idx];
|
|
762
|
-
const num = parseInt(toks[num_idx], 10);
|
|
763
|
-
if (sym && !Number.isNaN(num))
|
|
764
|
-
map[sym] = num;
|
|
765
|
-
}
|
|
766
|
-
lj++;
|
|
772
|
+
const toks = split_cif_tokens(line);
|
|
773
|
+
if (toks.length > Math.max(sym_idx, num_idx)) {
|
|
774
|
+
// Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
|
|
775
|
+
const match = /^(?<element>[A-Z][a-z]*)/.exec(toks[sym_idx]);
|
|
776
|
+
const sym = match ? match[1] : toks[sym_idx];
|
|
777
|
+
// Strip standard-uncertainty parentheses (`8(0)` -> `8`) like other CIF
|
|
778
|
+
// readers; empty prefixes like `(8)` parse as NaN and get skipped
|
|
779
|
+
const num = Math.trunc(parse_num_token(toks[num_idx].split(`(`)[0]));
|
|
780
|
+
// sum rows that normalize to the same element (e.g. Fe2+ and Fe3+ → Fe)
|
|
781
|
+
if (sym && !Number.isNaN(num)) {
|
|
782
|
+
atom_type_counts[sym] = (atom_type_counts[sym] ?? 0) + num;
|
|
767
783
|
}
|
|
768
|
-
break;
|
|
769
784
|
}
|
|
770
785
|
}
|
|
771
|
-
|
|
772
|
-
}
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
773
788
|
const observed_counts = {};
|
|
774
789
|
for (const atom of atoms) {
|
|
775
790
|
observed_counts[atom.element] = (observed_counts[atom.element] || 0) + 1;
|
|
@@ -778,57 +793,65 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
778
793
|
const already_enumerated = has_expected_counts &&
|
|
779
794
|
Object.entries(atom_type_counts).every(([el, exp]) => (observed_counts[el] || 0) >= exp);
|
|
780
795
|
const ops_to_use = already_enumerated ? [] : normalized_ops;
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
const
|
|
798
|
-
?
|
|
799
|
-
:
|
|
800
|
-
fractional_atom = { ...atom, coords
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
// Then apply lattice centering shifts to each equivalent position
|
|
805
|
-
for (const equiv_atom of equiv_atoms) {
|
|
806
|
-
for (const cv of centering_vectors) {
|
|
807
|
-
const abc = wrap_vec3([
|
|
808
|
-
equiv_atom.coords[0] + cv[0],
|
|
809
|
-
equiv_atom.coords[1] + cv[1],
|
|
810
|
-
equiv_atom.coords[2] + cv[2],
|
|
811
|
-
]);
|
|
796
|
+
// Candidate lattice-centering translations from the space-group symbol (R
|
|
797
|
+
// only valid in the hexagonal setting, α≈β≈90°, γ≈120°). Whether to actually
|
|
798
|
+
// apply them is decided below by reconciling against _atom_type_number_in_cell.
|
|
799
|
+
const centering_letter = extract_cif_centering(text);
|
|
800
|
+
const is_hexagonal_setting = Math.abs(alpha - 90) <= 1 && Math.abs(beta - 90) <= 1 && Math.abs(gamma - 120) <= 1;
|
|
801
|
+
const centering = centering_letter && (centering_letter !== `R` || is_hexagonal_setting)
|
|
802
|
+
? CENTERING_VECTORS[centering_letter]
|
|
803
|
+
: [];
|
|
804
|
+
// Build all sites by expanding each atom via the symmetry ops (+ optional
|
|
805
|
+
// centering). Deduplicate globally on element + coordinates + label (6 dp to
|
|
806
|
+
// absorb floating point error from compound ops).
|
|
807
|
+
const build_sites = (extra_centering) => {
|
|
808
|
+
const sites = [];
|
|
809
|
+
const seen_site_keys = new Set();
|
|
810
|
+
for (const atom of atoms) {
|
|
811
|
+
const element = validate_element_symbol(atom.element, sites.length);
|
|
812
|
+
const coords = atom.coords_type === `fract`
|
|
813
|
+
? wrap_vec3(atom.coords)
|
|
814
|
+
: wrap_vec3(cart_to_frac([atom.coords[0], atom.coords[1], atom.coords[2]]));
|
|
815
|
+
const fractional_atom = { ...atom, coords, coords_type: `fract` };
|
|
816
|
+
const equiv_atoms = apply_symmetry_ops(fractional_atom, ops_to_use, wrap_fractional_coords, extra_centering);
|
|
817
|
+
for (const equiv_atom of equiv_atoms) {
|
|
818
|
+
const abc = wrap_vec3(equiv_atom.coords);
|
|
812
819
|
const key = cif_site_key(element, abc, equiv_atom.id);
|
|
813
820
|
if (seen_site_keys.has(key))
|
|
814
821
|
continue;
|
|
815
822
|
seen_site_keys.add(key);
|
|
816
|
-
|
|
817
|
-
all_sites.push({
|
|
818
|
-
species: [{ element, occu: equiv_atom.occupancy, oxidation_state: 0 }],
|
|
819
|
-
abc,
|
|
820
|
-
xyz,
|
|
821
|
-
label: equiv_atom.id,
|
|
822
|
-
properties: {},
|
|
823
|
-
});
|
|
823
|
+
sites.push(make_site(element, abc, frac_to_cart(abc), equiv_atom.id, {}, equiv_atom.occupancy));
|
|
824
824
|
}
|
|
825
825
|
}
|
|
826
|
+
return sites;
|
|
827
|
+
};
|
|
828
|
+
// Expand with point-group ops first. If the space group is centered and the
|
|
829
|
+
// result falls short of _atom_type_number_in_cell, retry with centering and
|
|
830
|
+
// adopt it only when it reconciles the expected total exactly — this fixes
|
|
831
|
+
// CIFs listing point-only ops for the asymmetric unit while avoiding
|
|
832
|
+
// double-counting CIFs whose atom list already embeds centering (e.g. C2/c
|
|
833
|
+
// COD 7008984, where listed ops + atoms already total the cell contents).
|
|
834
|
+
let sites = build_sites([]);
|
|
835
|
+
const expected_total = Object.values(atom_type_counts).reduce((sum, num) => sum + num, 0);
|
|
836
|
+
if (centering.length > 0 && expected_total > sites.length) {
|
|
837
|
+
const centered_sites = build_sites(centering);
|
|
838
|
+
// Adopt centering only when per-element counts reconcile exactly. Checking
|
|
839
|
+
// the total alone is insufficient: it can coincide while individual element
|
|
840
|
+
// counts are wrong (e.g. expected Fe 1 / O 3 but centering yields Fe 2 / O 2).
|
|
841
|
+
const counts = {};
|
|
842
|
+
for (const site of centered_sites) {
|
|
843
|
+
const element = site.species[0].element;
|
|
844
|
+
counts[element] = (counts[element] ?? 0) + 1;
|
|
845
|
+
}
|
|
846
|
+
const reconciles = centered_sites.length === expected_total &&
|
|
847
|
+
Object.entries(atom_type_counts).every(([element, exp]) => counts[element] === exp);
|
|
848
|
+
if (reconciles)
|
|
849
|
+
sites = centered_sites;
|
|
826
850
|
}
|
|
827
|
-
const sites = all_sites;
|
|
828
851
|
return { sites, lattice: { matrix: lattice_matrix, ...lattice_params } };
|
|
829
852
|
}
|
|
830
853
|
catch (error) {
|
|
831
|
-
|
|
854
|
+
diag_error(`Error parsing CIF file`, error);
|
|
832
855
|
return null;
|
|
833
856
|
}
|
|
834
857
|
}
|
|
@@ -836,11 +859,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
836
859
|
function convert_phonopy_cell(cell) {
|
|
837
860
|
const sites = [];
|
|
838
861
|
// Phonopy stores lattice vectors as rows, use them directly
|
|
839
|
-
const lattice_matrix =
|
|
840
|
-
vec3_from_values(cell.lattice[0], `phonopy lattice vector 1`),
|
|
841
|
-
vec3_from_values(cell.lattice[1], `phonopy lattice vector 2`),
|
|
842
|
-
vec3_from_values(cell.lattice[2], `phonopy lattice vector 3`),
|
|
843
|
-
];
|
|
862
|
+
const lattice_matrix = matrix3x3_from_rows(cell.lattice, `phonopy lattice vector`);
|
|
844
863
|
// Process each atomic site
|
|
845
864
|
const phonopy_frac_to_cart = math.create_frac_to_cart(lattice_matrix);
|
|
846
865
|
for (const point of cell.points) {
|
|
@@ -851,9 +870,7 @@ function convert_phonopy_cell(cell) {
|
|
|
851
870
|
mass: point.mass,
|
|
852
871
|
...(point.reduced_to !== undefined && { reduced_to: point.reduced_to }),
|
|
853
872
|
};
|
|
854
|
-
|
|
855
|
-
const site = { species, abc, xyz, label: point.symbol, properties };
|
|
856
|
-
sites.push(site);
|
|
873
|
+
sites.push(make_site(element, abc, xyz, point.symbol, properties));
|
|
857
874
|
}
|
|
858
875
|
// Calculate lattice parameters
|
|
859
876
|
const calculated_lattice_params = math.calc_lattice_params(lattice_matrix);
|
|
@@ -872,7 +889,7 @@ const get_phonopy_cell = (data, cell_type) => {
|
|
|
872
889
|
const cell = Reflect.get(data, cell_type);
|
|
873
890
|
return is_phonopy_cell(cell) ? cell : undefined;
|
|
874
891
|
};
|
|
875
|
-
// Parse phonopy YAML
|
|
892
|
+
// @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).
|
|
876
893
|
export function parse_phonopy_yaml(content, cell_type) {
|
|
877
894
|
try {
|
|
878
895
|
// Parse YAML content but exclude large phonon_displacements array for performance
|
|
@@ -887,7 +904,7 @@ export function parse_phonopy_yaml(content, cell_type) {
|
|
|
887
904
|
}
|
|
888
905
|
// Check if we're still in the phonon_displacements section
|
|
889
906
|
if (skip_displacements) {
|
|
890
|
-
if (/^[a-zA-Z_]/.
|
|
907
|
+
if (/^[a-zA-Z_]/.test(line)) {
|
|
891
908
|
// New top-level key, stop skipping
|
|
892
909
|
skip_displacements = false;
|
|
893
910
|
}
|
|
@@ -899,7 +916,7 @@ export function parse_phonopy_yaml(content, cell_type) {
|
|
|
899
916
|
const filtered_content = filtered_lines.join(`\n`);
|
|
900
917
|
const data = yaml_load(filtered_content);
|
|
901
918
|
if (!data) {
|
|
902
|
-
|
|
919
|
+
diag_error(`Failed to parse phonopy YAML`);
|
|
903
920
|
return null;
|
|
904
921
|
}
|
|
905
922
|
// If specific cell type requested, parse only that one
|
|
@@ -907,27 +924,26 @@ export function parse_phonopy_yaml(content, cell_type) {
|
|
|
907
924
|
const cell = get_phonopy_cell(data, cell_type);
|
|
908
925
|
if (cell)
|
|
909
926
|
return convert_phonopy_cell(cell);
|
|
910
|
-
|
|
927
|
+
diag_error(`Requested cell type '${cell_type}' not found in phonopy YAML`);
|
|
911
928
|
return null;
|
|
912
929
|
}
|
|
913
|
-
// Auto mode: return
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
get_phonopy_cell(data,
|
|
922
|
-
|
|
923
|
-
get_phonopy_cell(data, `primitive_cell`);
|
|
930
|
+
// Auto mode: return first available cell, most detailed first
|
|
931
|
+
const auto_cell = [
|
|
932
|
+
`supercell`,
|
|
933
|
+
`phonon_supercell`,
|
|
934
|
+
`unit_cell`,
|
|
935
|
+
`phonon_primitive_cell`,
|
|
936
|
+
`primitive_cell`,
|
|
937
|
+
]
|
|
938
|
+
.map((kind) => get_phonopy_cell(data, kind))
|
|
939
|
+
.find(Boolean);
|
|
924
940
|
if (auto_cell)
|
|
925
941
|
return convert_phonopy_cell(auto_cell);
|
|
926
|
-
|
|
942
|
+
diag_error(`No valid cells found in phonopy YAML`);
|
|
927
943
|
return null;
|
|
928
944
|
}
|
|
929
945
|
catch (error) {
|
|
930
|
-
|
|
946
|
+
diag_error(`Error parsing phonopy YAML`, error);
|
|
931
947
|
return null;
|
|
932
948
|
}
|
|
933
949
|
}
|
|
@@ -961,8 +977,8 @@ function find_structure_in_json(obj, visited = new WeakSet()) {
|
|
|
961
977
|
}
|
|
962
978
|
return null;
|
|
963
979
|
}
|
|
964
|
-
// Type guard to validate structure-like objects
|
|
965
|
-
function is_parsed_structure(obj) {
|
|
980
|
+
// Type guard to validate structure-like objects (sites array with species + coordinates)
|
|
981
|
+
export function is_parsed_structure(obj) {
|
|
966
982
|
if (!obj || typeof obj !== `object`)
|
|
967
983
|
return false;
|
|
968
984
|
const sites = `sites` in obj ? obj.sites : undefined;
|
|
@@ -978,6 +994,31 @@ function is_parsed_structure(obj) {
|
|
|
978
994
|
const has_coords = Array.isArray(abc) || Array.isArray(xyz);
|
|
979
995
|
return has_species && has_coords;
|
|
980
996
|
}
|
|
997
|
+
// Structure JSON serialized by pymatgen (default verbosity) stores only the lattice
|
|
998
|
+
// matrix + pbc; derive the missing scalar params (a/b/c/angles/volume) from the matrix
|
|
999
|
+
// so downstream consumers (camera auto-fit, density, export) never see NaN.
|
|
1000
|
+
export function ensure_lattice_params(structure) {
|
|
1001
|
+
const lattice = structure.lattice;
|
|
1002
|
+
if (!lattice?.matrix)
|
|
1003
|
+
return structure;
|
|
1004
|
+
const params = [
|
|
1005
|
+
lattice.a,
|
|
1006
|
+
lattice.b,
|
|
1007
|
+
lattice.c,
|
|
1008
|
+
lattice.alpha,
|
|
1009
|
+
lattice.beta,
|
|
1010
|
+
lattice.gamma,
|
|
1011
|
+
lattice.volume,
|
|
1012
|
+
];
|
|
1013
|
+
if (params.every(Number.isFinite))
|
|
1014
|
+
return structure;
|
|
1015
|
+
// The matrix is authoritative: recompute all params from it rather than
|
|
1016
|
+
// trusting a partially-populated (or non-numeric) set of values.
|
|
1017
|
+
return {
|
|
1018
|
+
...structure,
|
|
1019
|
+
lattice: { ...lattice, ...math.calc_lattice_params(lattice.matrix) },
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
981
1022
|
// Normalize structure coordinates: wrap fractional coords to [0,1) and recompute Cartesian
|
|
982
1023
|
// Only normalizes when lattice matrix is available to ensure abc/xyz stay consistent
|
|
983
1024
|
export function normalize_fractional_coords(structure) {
|
|
@@ -1011,17 +1052,14 @@ const detect_json_structure = (content) => {
|
|
|
1011
1052
|
}
|
|
1012
1053
|
// Otherwise try parsing as pymatgen/nested structure JSON
|
|
1013
1054
|
const structure = find_structure_in_json(parsed);
|
|
1014
|
-
return structure ? normalize_fractional_coords(structure) : null;
|
|
1055
|
+
return structure ? ensure_lattice_params(normalize_fractional_coords(structure)) : null;
|
|
1015
1056
|
};
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1057
|
+
// Internal: auto-detect file format, returns null on failure after recording reasons (see parse error contract at top)
|
|
1058
|
+
function parse_structure_file_impl(content, filename) {
|
|
1018
1059
|
// If a filename is provided, try to detect format by file extension first
|
|
1019
1060
|
if (filename) {
|
|
1020
1061
|
// Handle compressed files by removing compression extensions
|
|
1021
|
-
|
|
1022
|
-
while (COMPRESSION_EXTENSIONS_REGEX.test(base_filename)) {
|
|
1023
|
-
base_filename = base_filename.replace(COMPRESSION_EXTENSIONS_REGEX, ``);
|
|
1024
|
-
}
|
|
1062
|
+
const base_filename = strip_compression_extensions(filename);
|
|
1025
1063
|
const ext = base_filename.split(`.`).pop();
|
|
1026
1064
|
// Try to detect format by file extension
|
|
1027
1065
|
if (ext === `xyz` || ext === `extxyz`)
|
|
@@ -1035,10 +1073,10 @@ export function parse_structure_file(content, filename) {
|
|
|
1035
1073
|
const result = detect_json_structure(content);
|
|
1036
1074
|
if (result)
|
|
1037
1075
|
return result;
|
|
1038
|
-
|
|
1076
|
+
diag_error(`JSON file does not contain a valid structure format`);
|
|
1039
1077
|
}
|
|
1040
1078
|
catch (error) {
|
|
1041
|
-
|
|
1079
|
+
diag_error(`Error parsing JSON file`, error);
|
|
1042
1080
|
}
|
|
1043
1081
|
return null;
|
|
1044
1082
|
}
|
|
@@ -1053,49 +1091,40 @@ export function parse_structure_file(content, filename) {
|
|
|
1053
1091
|
// Try to auto-detect based on content.
|
|
1054
1092
|
// JSON detection must come before the line-count guard: minified JSON
|
|
1055
1093
|
// (e.g. fetched via extensionless blob: object URLs) is a single line.
|
|
1094
|
+
const content_start = content.trimStart();
|
|
1095
|
+
const looks_like_json = content_start.startsWith(`{`) || content_start.startsWith(`[`);
|
|
1056
1096
|
try {
|
|
1057
1097
|
const result = detect_json_structure(content);
|
|
1058
1098
|
if (result)
|
|
1059
1099
|
return result;
|
|
1100
|
+
if (looks_like_json)
|
|
1101
|
+
diag_error(`JSON content does not contain a valid structure format`);
|
|
1060
1102
|
}
|
|
1061
|
-
catch {
|
|
1062
|
-
//
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
// Only swallow silently when content doesn't even look like JSON; otherwise the
|
|
1105
|
+
// syntax error is the most useful failure reason and must be surfaced
|
|
1106
|
+
if (looks_like_json)
|
|
1107
|
+
diag_error(`Invalid JSON`, error);
|
|
1063
1108
|
}
|
|
1064
1109
|
const lines = content.trim().split(/\r?\n/);
|
|
1065
1110
|
if (lines.length < 2) {
|
|
1066
|
-
|
|
1111
|
+
diag_error(`File too short to determine format`);
|
|
1067
1112
|
return null;
|
|
1068
1113
|
}
|
|
1069
|
-
// XYZ format detection: first line
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const parts = lines[coord_line_idx].trim().split(/\s+/);
|
|
1078
|
-
// XYZ format: element symbol followed by 3 coordinates
|
|
1079
|
-
if (parts.length >= 4) {
|
|
1080
|
-
const first_token = parts[0];
|
|
1081
|
-
const coords = parts.slice(1, 4);
|
|
1082
|
-
// Check if first token looks like an element symbol (not a number)
|
|
1083
|
-
// and the next 3 tokens look like coordinates (numbers)
|
|
1084
|
-
const is_element_symbol = isNaN(parseInt(first_token, 10)) && first_token.length <= 3;
|
|
1085
|
-
const are_coordinates = coords.every((coord) => !isNaN(parseFloat(coord)));
|
|
1086
|
-
if (is_element_symbol && are_coordinates) {
|
|
1087
|
-
// First token is likely an element symbol, likely XYZ
|
|
1088
|
-
return parse_xyz(content);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1114
|
+
// XYZ format detection: first line is a positive atom count, second line is a
|
|
1115
|
+
// comment, and the first coordinate line looks like "<element> <x> <y> <z>"
|
|
1116
|
+
const first_line_number = Math.trunc(parse_leading_num(lines[0]));
|
|
1117
|
+
if (!isNaN(first_line_number) &&
|
|
1118
|
+
first_line_number > 0 &&
|
|
1119
|
+
lines.length >= first_line_number + 2 &&
|
|
1120
|
+
is_xyz_atom_line(lines[2]?.trim().split(/\s+/)))
|
|
1121
|
+
return parse_xyz(content);
|
|
1094
1122
|
// POSCAR format detection: look for typical structure
|
|
1095
1123
|
if (lines.length >= 8) {
|
|
1096
|
-
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1124
|
+
// Second line starts with a number (scale factor), likely POSCAR. First
|
|
1125
|
+
// token only: POSCAR allows three per-axis scale factors (or trailing
|
|
1126
|
+
// comments) on line 2, and blank lines must not pass
|
|
1127
|
+
if (!isNaN(parse_leading_num(lines[1])))
|
|
1099
1128
|
return parse_poscar(content);
|
|
1100
1129
|
}
|
|
1101
1130
|
// CIF format detection: look for CIF-specific keywords
|
|
@@ -1113,38 +1142,48 @@ export function parse_structure_file(content, filename) {
|
|
|
1113
1142
|
line.includes(`phonon_supercell:`));
|
|
1114
1143
|
if (has_phonopy_keywords)
|
|
1115
1144
|
return parse_phonopy_yaml(content);
|
|
1116
|
-
|
|
1145
|
+
diag_error(`Unable to determine file format`);
|
|
1117
1146
|
return null;
|
|
1118
1147
|
}
|
|
1119
|
-
//
|
|
1148
|
+
// Auto-detect file format and parse; throws an Error aggregating per-format failure reasons when nothing parses
|
|
1149
|
+
export function parse_structure_file(content, filename) {
|
|
1150
|
+
reset_parse_diagnostics();
|
|
1151
|
+
const structure = parse_structure_file_impl(content, filename);
|
|
1152
|
+
if (structure)
|
|
1153
|
+
return structure;
|
|
1154
|
+
throw aggregate_parse_error(filename);
|
|
1155
|
+
}
|
|
1156
|
+
// Universal parser for JSON and structure files; throws an Error aggregating per-format failure reasons when nothing parses
|
|
1120
1157
|
export function parse_any_structure(content, filename) {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1158
|
+
reset_parse_diagnostics();
|
|
1159
|
+
const finalize_structure = (parsed_structure) => {
|
|
1160
|
+
const structure = ensure_lattice_params(parsed_structure);
|
|
1161
|
+
return {
|
|
1162
|
+
sites: structure.sites,
|
|
1163
|
+
charge: 0,
|
|
1164
|
+
...(structure.properties && {
|
|
1165
|
+
properties: structuredClone(structure.properties),
|
|
1166
|
+
}),
|
|
1167
|
+
...(structure.lattice && {
|
|
1168
|
+
lattice: { ...structure.lattice, pbc: [true, true, true] },
|
|
1169
|
+
}),
|
|
1170
|
+
};
|
|
1171
|
+
};
|
|
1172
|
+
// Fast path: content is already a serialized structure object
|
|
1132
1173
|
try {
|
|
1133
1174
|
const parsed = JSON.parse(content);
|
|
1134
|
-
// Check if it's already a valid structure using proper type guard
|
|
1135
1175
|
if (is_parsed_structure(parsed)) {
|
|
1136
1176
|
// Normalize coordinates (wrap fractional to [0,1) and recompute Cartesian)
|
|
1137
1177
|
return finalize_structure(normalize_fractional_coords(parsed));
|
|
1138
1178
|
}
|
|
1139
|
-
// If not, use parse_structure_file to find nested structures
|
|
1140
|
-
const structure = parse_structure_file(content, filename);
|
|
1141
|
-
return structure ? finalize_structure(structure) : null;
|
|
1142
1179
|
}
|
|
1143
1180
|
catch {
|
|
1144
|
-
//
|
|
1145
|
-
const parsed = parse_structure_file(content, filename);
|
|
1146
|
-
return parsed ? finalize_structure(parsed) : null;
|
|
1181
|
+
// Not plain JSON — fall through to format detection, which records failure reasons
|
|
1147
1182
|
}
|
|
1183
|
+
const structure = parse_structure_file_impl(content, filename);
|
|
1184
|
+
if (structure)
|
|
1185
|
+
return finalize_structure(structure);
|
|
1186
|
+
throw aggregate_parse_error(filename);
|
|
1148
1187
|
}
|
|
1149
1188
|
// Parse OPTIMADE JSON format
|
|
1150
1189
|
export function parse_optimade_json(content) {
|
|
@@ -1153,16 +1192,80 @@ export function parse_optimade_json(content) {
|
|
|
1153
1192
|
return parse_optimade_from_raw(raw);
|
|
1154
1193
|
}
|
|
1155
1194
|
catch (error) {
|
|
1156
|
-
|
|
1195
|
+
diag_error(`Error parsing OPTIMADE JSON`, error);
|
|
1157
1196
|
return null;
|
|
1158
1197
|
}
|
|
1159
1198
|
}
|
|
1199
|
+
// Build sites + lattice shared by parse_optimade_from_raw and optimade_to_crystal.
|
|
1200
|
+
// on_invalid controls whether invalid positions are skipped with a warning or throw;
|
|
1201
|
+
// site_props extracts per-site mass/concentration from the species list.
|
|
1202
|
+
function build_optimade_sites(attrs, opts) {
|
|
1203
|
+
const positions = attrs.cartesian_site_positions ?? [];
|
|
1204
|
+
const species_at_sites = attrs.species_at_sites ?? [];
|
|
1205
|
+
const species_list = Array.isArray(attrs.species) ? attrs.species : undefined;
|
|
1206
|
+
// OPTIMADE stores lattice vectors as rows, so use as-is
|
|
1207
|
+
const lattice_matrix = attrs.lattice_vectors
|
|
1208
|
+
? matrix3x3_from_rows(attrs.lattice_vectors, `OPTIMADE lattice vector`)
|
|
1209
|
+
: undefined;
|
|
1210
|
+
const lattice_params = lattice_matrix ? math.calc_lattice_params(lattice_matrix) : null;
|
|
1211
|
+
let cart_to_frac = null;
|
|
1212
|
+
if (lattice_matrix && lattice_params) {
|
|
1213
|
+
const converter = cart_to_frac_with_fallback(lattice_matrix, [
|
|
1214
|
+
lattice_params.a,
|
|
1215
|
+
lattice_params.b,
|
|
1216
|
+
lattice_params.c,
|
|
1217
|
+
]);
|
|
1218
|
+
if (!converter.exact) {
|
|
1219
|
+
diag_warn(`Failed to create exact coordinate converter for OPTIMADE structure`);
|
|
1220
|
+
}
|
|
1221
|
+
cart_to_frac = converter.convert;
|
|
1222
|
+
}
|
|
1223
|
+
const sites = [];
|
|
1224
|
+
for (let idx = 0; idx < positions.length; idx++) {
|
|
1225
|
+
const species_name = species_at_sites[idx];
|
|
1226
|
+
if (!species_name) {
|
|
1227
|
+
if (opts.on_invalid === `throw`)
|
|
1228
|
+
throw new Error(`Missing species for site ${idx}`);
|
|
1229
|
+
diag_warn(`Missing species for site ${idx}, skipping`);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
let xyz;
|
|
1233
|
+
try {
|
|
1234
|
+
xyz = vec3_from_values(positions[idx], `OPTIMADE atom position ${idx + 1}`);
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
if (opts.on_invalid === `throw`)
|
|
1238
|
+
throw error;
|
|
1239
|
+
diag_warn(`Invalid position data at site ${idx}: ${error}`);
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
const { symbol: element, sym_idx } = resolve_optimade_element(species_name, species_list, idx);
|
|
1243
|
+
// Calculate fractional coordinates if lattice is available
|
|
1244
|
+
const abc = cart_to_frac ? cart_to_frac(xyz) : [0, 0, 0];
|
|
1245
|
+
const site_props = {};
|
|
1246
|
+
if (opts.site_props) {
|
|
1247
|
+
// Extract mass/concentration for the chosen element. sym_idx indexes the (parallel)
|
|
1248
|
+
// chemical_symbols/mass/concentration arrays; -1 (name resolved directly, no
|
|
1249
|
+
// chemical_symbols) falls back to index 0 — the single-element entry.
|
|
1250
|
+
const spec = species_list?.find((entry) => entry.name === species_name);
|
|
1251
|
+
const spec_idx = Math.max(sym_idx, 0);
|
|
1252
|
+
if (spec?.mass?.[spec_idx] !== undefined)
|
|
1253
|
+
site_props.mass = spec.mass[spec_idx];
|
|
1254
|
+
if (spec?.concentration?.[spec_idx] !== undefined &&
|
|
1255
|
+
spec.concentration[spec_idx] !== 1) {
|
|
1256
|
+
site_props.concentration = spec.concentration[spec_idx];
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
sites.push(make_site(element, abc, xyz, `${element}${idx + 1}`, site_props));
|
|
1260
|
+
}
|
|
1261
|
+
return { sites, lattice_matrix, lattice_params };
|
|
1262
|
+
}
|
|
1160
1263
|
// Parse OPTIMADE from already-parsed JSON
|
|
1161
1264
|
export function parse_optimade_from_raw(raw) {
|
|
1162
1265
|
try {
|
|
1163
1266
|
const structure = extract_optimade_structure_from_raw(raw);
|
|
1164
1267
|
if (!structure) {
|
|
1165
|
-
|
|
1268
|
+
diag_error(`No valid OPTIMADE structure found in JSON`);
|
|
1166
1269
|
return null;
|
|
1167
1270
|
}
|
|
1168
1271
|
const attrs = structure.attributes;
|
|
@@ -1170,83 +1273,28 @@ export function parse_optimade_from_raw(raw) {
|
|
|
1170
1273
|
const positions_raw = attrs.cartesian_site_positions;
|
|
1171
1274
|
const species_raw = attrs.species_at_sites;
|
|
1172
1275
|
if (!(Array.isArray(positions_raw) && Array.isArray(species_raw))) {
|
|
1173
|
-
|
|
1276
|
+
diag_error(`OPTIMADE JSON missing required position or species data`);
|
|
1174
1277
|
return null;
|
|
1175
1278
|
}
|
|
1176
1279
|
if (positions_raw.length !== species_raw.length) {
|
|
1177
|
-
|
|
1280
|
+
diag_error(`OPTIMADE JSON position/species count mismatch`);
|
|
1178
1281
|
return null;
|
|
1179
1282
|
}
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const lattice_matrix = attrs.lattice_vectors
|
|
1184
|
-
? [
|
|
1185
|
-
vec3_from_values(attrs.lattice_vectors[0], `OPTIMADE lattice vector 1`),
|
|
1186
|
-
vec3_from_values(attrs.lattice_vectors[1], `OPTIMADE lattice vector 2`),
|
|
1187
|
-
vec3_from_values(attrs.lattice_vectors[2], `OPTIMADE lattice vector 3`),
|
|
1188
|
-
]
|
|
1189
|
-
: undefined;
|
|
1190
|
-
const optimade_lattice_params = lattice_matrix
|
|
1191
|
-
? math.calc_lattice_params(lattice_matrix)
|
|
1192
|
-
: null;
|
|
1193
|
-
// Parse atomic sites
|
|
1194
|
-
const optimade_exact_cart_to_frac = lattice_matrix
|
|
1195
|
-
? try_create_cart_to_frac(lattice_matrix)
|
|
1196
|
-
: null;
|
|
1197
|
-
const optimade_cart_to_frac = lattice_matrix && optimade_lattice_params
|
|
1198
|
-
? (optimade_exact_cart_to_frac ??
|
|
1199
|
-
((xyz) => approximate_cart_to_frac(xyz, [
|
|
1200
|
-
optimade_lattice_params.a,
|
|
1201
|
-
optimade_lattice_params.b,
|
|
1202
|
-
optimade_lattice_params.c,
|
|
1203
|
-
])))
|
|
1204
|
-
: null;
|
|
1205
|
-
if (lattice_matrix && !optimade_exact_cart_to_frac) {
|
|
1206
|
-
console.warn(`Failed to create exact coordinate converter for OPTIMADE structure`);
|
|
1207
|
-
}
|
|
1208
|
-
const optimade_species = Array.isArray(attrs.species) ? attrs.species : undefined;
|
|
1209
|
-
const sites = [];
|
|
1210
|
-
for (let idx = 0; idx < positions.length; idx++) {
|
|
1211
|
-
const pos = positions[idx];
|
|
1212
|
-
const element_symbol = species[idx];
|
|
1213
|
-
let xyz;
|
|
1214
|
-
try {
|
|
1215
|
-
xyz = vec3_from_values(pos, `OPTIMADE site ${idx} position`);
|
|
1216
|
-
}
|
|
1217
|
-
catch (error) {
|
|
1218
|
-
console.warn(`Invalid position data at site ${idx}: ${error}`);
|
|
1219
|
-
continue;
|
|
1220
|
-
}
|
|
1221
|
-
const { symbol: element } = resolve_optimade_element(element_symbol, optimade_species, idx);
|
|
1222
|
-
// Calculate fractional coordinates if lattice is available
|
|
1223
|
-
const abc = optimade_cart_to_frac ? optimade_cart_to_frac(xyz) : [0, 0, 0];
|
|
1224
|
-
const site = {
|
|
1225
|
-
species: [{ element, occu: 1, oxidation_state: 0 }],
|
|
1226
|
-
abc,
|
|
1227
|
-
xyz,
|
|
1228
|
-
label: `${element}${idx + 1}`,
|
|
1229
|
-
properties: {},
|
|
1230
|
-
};
|
|
1231
|
-
sites.push(site);
|
|
1232
|
-
}
|
|
1283
|
+
const { sites, lattice_matrix, lattice_params } = build_optimade_sites(attrs, {
|
|
1284
|
+
on_invalid: `skip`,
|
|
1285
|
+
});
|
|
1233
1286
|
if (sites.length === 0) {
|
|
1234
|
-
|
|
1287
|
+
diag_error(`No valid sites found in OPTIMADE JSON`);
|
|
1235
1288
|
return null;
|
|
1236
1289
|
}
|
|
1237
|
-
|
|
1238
|
-
let lattice;
|
|
1239
|
-
if (lattice_matrix && optimade_lattice_params) {
|
|
1240
|
-
lattice = { matrix: lattice_matrix, ...optimade_lattice_params };
|
|
1241
|
-
}
|
|
1242
|
-
const structure_result = {
|
|
1290
|
+
return {
|
|
1243
1291
|
sites,
|
|
1244
|
-
...(
|
|
1292
|
+
...(lattice_matrix &&
|
|
1293
|
+
lattice_params && { lattice: { matrix: lattice_matrix, ...lattice_params } }),
|
|
1245
1294
|
};
|
|
1246
|
-
return structure_result;
|
|
1247
1295
|
}
|
|
1248
1296
|
catch (error) {
|
|
1249
|
-
|
|
1297
|
+
diag_error(`Error parsing OPTIMADE JSON`, error);
|
|
1250
1298
|
return null;
|
|
1251
1299
|
}
|
|
1252
1300
|
}
|
|
@@ -1268,9 +1316,7 @@ function extract_optimade_structure_from_raw(raw) {
|
|
|
1268
1316
|
const candidate = Array.isArray(payload) ? payload[0] : payload;
|
|
1269
1317
|
return is_optimade_structure_object(candidate) ? candidate : null;
|
|
1270
1318
|
}
|
|
1271
|
-
const unwrap_data = (value) => value && typeof value === `object` && `data` in value
|
|
1272
|
-
? value.data
|
|
1273
|
-
: value;
|
|
1319
|
+
const unwrap_data = (value) => value && typeof value === `object` && `data` in value ? value.data : value;
|
|
1274
1320
|
// Type guard: verify minimal OPTIMADE structure shape
|
|
1275
1321
|
function is_optimade_structure_object(value) {
|
|
1276
1322
|
if (!value || typeof value !== `object`)
|
|
@@ -1286,47 +1332,18 @@ function is_optimade_structure_object(value) {
|
|
|
1286
1332
|
}
|
|
1287
1333
|
// Convert OPTIMADE structure to Crystal format
|
|
1288
1334
|
export function optimade_to_crystal(optimade_structure) {
|
|
1289
|
-
const { lattice_vectors, cartesian_site_positions, species_at_sites, species,
|
|
1335
|
+
const { lattice_vectors, cartesian_site_positions, species_at_sites, species: _species, // excluded from the properties rest
|
|
1336
|
+
...properties } = optimade_structure.attributes;
|
|
1290
1337
|
if (!lattice_vectors || !cartesian_site_positions || !species_at_sites) {
|
|
1291
|
-
|
|
1338
|
+
diag_error(`Missing required OPTIMADE structure data`);
|
|
1292
1339
|
return null;
|
|
1293
1340
|
}
|
|
1294
1341
|
try {
|
|
1295
|
-
const lattice_matrix =
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
const lattice_params = math.calc_lattice_params(lattice_matrix);
|
|
1301
|
-
const crystal_cart_to_frac = try_create_cart_to_frac(lattice_matrix) ??
|
|
1302
|
-
((xyz) => approximate_cart_to_frac(xyz, [lattice_params.a, lattice_params.b, lattice_params.c]));
|
|
1303
|
-
const sites = cartesian_site_positions.map((pos, idx) => {
|
|
1304
|
-
const element_symbol = species_at_sites[idx];
|
|
1305
|
-
if (!element_symbol)
|
|
1306
|
-
throw new Error(`Missing species for site ${idx}`);
|
|
1307
|
-
const { symbol: element, sym_idx } = resolve_optimade_element(element_symbol, species, idx);
|
|
1308
|
-
const xyz = vec3_from_values(pos, `OPTIMADE atom position ${idx + 1}`);
|
|
1309
|
-
const abc = crystal_cart_to_frac ? crystal_cart_to_frac(xyz) : [0, 0, 0];
|
|
1310
|
-
// Extract mass/concentration for the chosen element. sym_idx indexes the (parallel)
|
|
1311
|
-
// chemical_symbols/mass/concentration arrays; -1 (name resolved directly, no
|
|
1312
|
-
// chemical_symbols) falls back to index 0 — the single-element entry.
|
|
1313
|
-
const spec = species?.find((entry) => entry.name === element_symbol);
|
|
1314
|
-
const spec_idx = Math.max(sym_idx, 0);
|
|
1315
|
-
const site_props = {};
|
|
1316
|
-
if (spec?.mass?.[spec_idx] !== undefined)
|
|
1317
|
-
site_props.mass = spec.mass[spec_idx];
|
|
1318
|
-
if (spec?.concentration?.[spec_idx] !== undefined &&
|
|
1319
|
-
spec.concentration[spec_idx] !== 1) {
|
|
1320
|
-
site_props.concentration = spec.concentration[spec_idx];
|
|
1321
|
-
}
|
|
1322
|
-
return {
|
|
1323
|
-
species: [{ element, occu: 1, oxidation_state: 0 }],
|
|
1324
|
-
abc,
|
|
1325
|
-
xyz,
|
|
1326
|
-
label: `${element}${idx + 1}`,
|
|
1327
|
-
properties: site_props,
|
|
1328
|
-
};
|
|
1329
|
-
});
|
|
1342
|
+
const { sites, lattice_matrix, lattice_params } = build_optimade_sites(optimade_structure.attributes, { on_invalid: `throw`, site_props: true });
|
|
1343
|
+
if (!lattice_matrix || !lattice_params) {
|
|
1344
|
+
diag_error(`Missing required OPTIMADE structure data`);
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1330
1347
|
return {
|
|
1331
1348
|
sites,
|
|
1332
1349
|
lattice: { matrix: lattice_matrix, ...lattice_params, pbc: [true, true, true] },
|
|
@@ -1335,46 +1352,13 @@ export function optimade_to_crystal(optimade_structure) {
|
|
|
1335
1352
|
};
|
|
1336
1353
|
}
|
|
1337
1354
|
catch (err) {
|
|
1338
|
-
|
|
1355
|
+
diag_error(`Error converting OPTIMADE to Crystal format`, err);
|
|
1339
1356
|
return null;
|
|
1340
1357
|
}
|
|
1341
1358
|
}
|
|
1342
|
-
// Check if filename indicates a structure file
|
|
1343
|
-
export function is_structure_file(filename) {
|
|
1344
|
-
const name = filename.toLowerCase();
|
|
1345
|
-
// Trajectory-only formats (can't be structures)
|
|
1346
|
-
if (/\.(traj|xtc|h5|hdf5)$/i.test(name) || /xdatcar/i.test(name))
|
|
1347
|
-
return false;
|
|
1348
|
-
// Always structure formats
|
|
1349
|
-
if (STRUCTURE_EXTENSIONS_REGEX.test(name))
|
|
1350
|
-
return true;
|
|
1351
|
-
if (VASP_FILES_REGEX.test(name))
|
|
1352
|
-
return true;
|
|
1353
|
-
// .xyz/.extxyz files: structure unless they have trajectory keywords
|
|
1354
|
-
if (/\.(xyz|extxyz)$/i.test(name))
|
|
1355
|
-
return !TRAJ_KEYWORDS_REGEX.test(name);
|
|
1356
|
-
// Keyword-based detection for YAML/XML
|
|
1357
|
-
if (/\.(yaml|yml|xml)$/i.test(name) && STRUCT_KEYWORDS_REGEX.test(name))
|
|
1358
|
-
return true;
|
|
1359
|
-
// More restrictive keyword detection for JSON files
|
|
1360
|
-
if (/\.json$/i.test(name) &&
|
|
1361
|
-
STRUCT_KEYWORDS_STRICT_REGEX.test(name) &&
|
|
1362
|
-
!TRAJ_KEYWORDS_REGEX.test(name) &&
|
|
1363
|
-
!CONFIG_DIRS_REGEX.test(name))
|
|
1364
|
-
return true;
|
|
1365
|
-
// Compressed files - check base filename recursively
|
|
1366
|
-
if (COMPRESSION_EXTENSIONS_REGEX.test(name)) {
|
|
1367
|
-
return is_structure_file(name.replace(COMPRESSION_EXTENSIONS_REGEX, ``));
|
|
1368
|
-
}
|
|
1369
|
-
return false;
|
|
1370
|
-
}
|
|
1371
1359
|
export const detect_structure_type = (filename, content) => {
|
|
1372
|
-
const lower_filename = filename.toLowerCase();
|
|
1373
1360
|
// Normalize compressed suffixes (gz, gzip, zip, xz, bz2) for detection parity
|
|
1374
|
-
|
|
1375
|
-
while (COMPRESSION_EXTENSIONS_REGEX.test(name_to_check)) {
|
|
1376
|
-
name_to_check = name_to_check.replace(COMPRESSION_EXTENSIONS_REGEX, ``);
|
|
1377
|
-
}
|
|
1361
|
+
const name_to_check = strip_compression_extensions(filename);
|
|
1378
1362
|
if (name_to_check.endsWith(`.json`)) {
|
|
1379
1363
|
try {
|
|
1380
1364
|
const parsed = JSON.parse(content);
|