matterviz 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brillouin/BrillouinZone.svelte +68 -145
- package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -14
- package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
- package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
- package/dist/brillouin/BrillouinZoneScene.svelte +49 -203
- 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.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +100 -191
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +176 -464
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -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.js +2 -2
- package/dist/composition/FormulaFilter.svelte +6 -5
- package/dist/composition/PieChart.svelte +5 -5
- package/dist/composition/chem-sys.js +3 -2
- package/dist/composition/format.js +3 -2
- package/dist/composition/parse.d.ts +0 -1
- package/dist/composition/parse.js +17 -19
- package/dist/controls.d.ts +1 -0
- package/dist/controls.js +0 -1
- package/dist/convex-hull/ConvexHull.svelte +8 -10
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
- package/dist/convex-hull/ConvexHull2D.svelte +94 -175
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +176 -680
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +180 -680
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
- package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
- package/dist/convex-hull/ConvexHullControls.svelte +88 -7
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
- package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
- package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
- package/dist/convex-hull/ConvexHullStats.svelte +29 -168
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
- package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
- package/dist/convex-hull/barycentric-coords.d.ts +2 -4
- package/dist/convex-hull/barycentric-coords.js +6 -33
- package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
- package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
- package/dist/convex-hull/helpers.d.ts +39 -7
- package/dist/convex-hull/helpers.js +154 -69
- 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 +91 -920
- package/dist/convex-hull/types.d.ts +12 -4
- package/dist/convex-hull/types.js +12 -0
- package/dist/coordination/CoordinationBarPlot.svelte +4 -11
- package/dist/element/BohrAtom.svelte +2 -1
- package/dist/element/ElementTile.svelte.d.ts +1 -1
- package/dist/element/index.d.ts +4 -0
- package/dist/element/index.js +18 -0
- package/dist/feedback/DragOverlay.svelte +3 -1
- package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
- package/dist/feedback/StatusMessage.svelte +13 -3
- package/dist/fermi-surface/FermiSurface.svelte +67 -146
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -224
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
- package/dist/fermi-surface/compute.js +11 -10
- 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 +64 -75
- package/dist/fermi-surface/types.d.ts +2 -2
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +55 -40
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
- package/dist/heatmap-matrix/index.d.ts +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/io/ExportPane.svelte +166 -0
- package/dist/io/ExportPane.svelte.d.ts +17 -0
- package/dist/io/decompress.js +1 -2
- package/dist/io/export.d.ts +5 -1
- package/dist/io/export.js +32 -28
- package/dist/io/fetch.d.ts +2 -1
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +13 -0
- package/dist/io/index.d.ts +2 -0
- package/dist/io/index.js +10 -0
- package/dist/io/types.d.ts +13 -0
- package/dist/isosurface/parse.js +46 -44
- package/dist/labels.js +1 -1
- package/dist/layout/FullscreenButton.svelte +33 -0
- package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
- package/dist/layout/FullscreenToggle.svelte +8 -14
- package/dist/layout/ViewerChrome.svelte +116 -0
- package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
- package/dist/layout/fullscreen.d.ts +4 -0
- package/dist/layout/fullscreen.svelte.d.ts +8 -0
- package/dist/layout/fullscreen.svelte.js +37 -0
- package/dist/layout/index.d.ts +3 -0
- package/dist/layout/index.js +3 -0
- package/dist/math.d.ts +7 -3
- package/dist/math.js +18 -21
- package/dist/overlays/index.d.ts +4 -0
- package/dist/periodic-table/PeriodicTable.svelte +9 -8
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
- package/dist/phase-diagram/colors.js +1 -1
- package/dist/phase-diagram/parse.d.ts +2 -1
- package/dist/plot/bar/BarPlot.svelte +79 -316
- package/dist/plot/bar/BarPlot.svelte.d.ts +7 -15
- package/dist/plot/bar/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/bar/SpacegroupBarPlot.svelte +2 -1
- package/dist/plot/box/BoxPlot.svelte +76 -246
- package/dist/plot/box/BoxPlot.svelte.d.ts +4 -3
- package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
- package/dist/plot/box/Violin.svelte.d.ts +1 -1
- package/dist/plot/box/box-plot.d.ts +3 -2
- package/dist/plot/box/box-plot.js +5 -2
- 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 +4 -1
- package/dist/plot/core/components/ColorBar.svelte +5 -5
- package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
- package/dist/plot/core/components/Line.svelte +3 -2
- package/dist/plot/core/components/Line.svelte.d.ts +3 -2
- package/dist/plot/core/components/PlotAxis.svelte +2 -1
- package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
- package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/core/components/ReferenceLine3D.svelte +2 -2
- package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
- package/dist/plot/core/components/ReferencePlane.svelte +2 -2
- package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
- package/dist/plot/core/data-cleaning.js +18 -18
- package/dist/plot/core/fill-utils.d.ts +4 -3
- package/dist/plot/core/fill-utils.js +6 -3
- package/dist/plot/core/interactions.d.ts +5 -1
- package/dist/plot/core/interactions.js +14 -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 +6 -6
- package/dist/plot/core/scales.d.ts +17 -25
- package/dist/plot/core/scales.js +10 -8
- package/dist/plot/core/svg.d.ts +2 -1
- package/dist/plot/core/types.d.ts +18 -7
- package/dist/plot/core/utils/label-placement.d.ts +1 -1
- package/dist/plot/core/utils/label-placement.js +3 -3
- package/dist/plot/core/utils.d.ts +2 -1
- package/dist/plot/histogram/Histogram.svelte +77 -314
- package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/sankey/Sankey.svelte +2 -5
- package/dist/plot/sankey/Sankey.svelte.d.ts +1 -1
- package/dist/plot/sankey/sankey.js +3 -1
- package/dist/plot/scatter/BinnedScatterPlot.svelte +3 -5
- package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +4 -4
- package/dist/plot/scatter/ScatterPlot.svelte +160 -450
- package/dist/plot/scatter/ScatterPlot.svelte.d.ts +7 -15
- package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/scatter/binned-scatter-types.d.ts +4 -11
- package/dist/plot/scatter/index.d.ts +1 -1
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte +15 -26
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +9 -10
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +122 -121
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
- package/dist/plot/scatter-3d/Surface3D.svelte +6 -5
- package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
- package/dist/plot/sunburst/Sunburst.svelte +16 -20
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
- package/dist/plot/sunburst/SunburstControls.svelte.d.ts +1 -1
- package/dist/plot/sunburst/sunburst.js +4 -1
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/sanitize.js +13 -2
- package/dist/scene/SceneCamera.svelte +62 -0
- package/dist/scene/SceneCamera.svelte.d.ts +19 -0
- package/dist/scene/bind-renderer.svelte.d.ts +2 -0
- package/dist/scene/bind-renderer.svelte.js +14 -0
- package/dist/scene/index.d.ts +4 -0
- package/dist/scene/index.js +5 -0
- package/dist/scene/props.js +52 -0
- package/dist/scene/types.d.ts +26 -0
- package/dist/scene/types.js +1 -0
- package/dist/settings.d.ts +14 -2
- package/dist/settings.js +59 -1
- package/dist/spectral/Bands.svelte +8 -7
- package/dist/spectral/Bands.svelte.d.ts +3 -2
- package/dist/spectral/BandsAndDos.svelte +22 -24
- package/dist/spectral/BrillouinBandsDos.svelte +3 -3
- package/dist/spectral/Dos.svelte +5 -4
- package/dist/spectral/Dos.svelte.d.ts +2 -1
- package/dist/spectral/helpers.d.ts +6 -6
- package/dist/spectral/helpers.js +43 -37
- package/dist/state.svelte.d.ts +0 -7
- package/dist/state.svelte.js +0 -6
- package/dist/structure/Arrow.svelte +2 -4
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CanvasTooltip.svelte +1 -0
- package/dist/structure/CellSelect.svelte +11 -3
- package/dist/structure/CellSelect.svelte.d.ts +2 -1
- package/dist/structure/Lattice.svelte +2 -2
- package/dist/structure/Structure.svelte +291 -355
- package/dist/structure/Structure.svelte.d.ts +5 -15
- package/dist/structure/StructureControls.svelte +217 -2
- package/dist/structure/StructureControls.svelte.d.ts +5 -3
- package/dist/structure/StructureExportPane.svelte +54 -156
- package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
- package/dist/structure/StructureInfoPane.svelte +5 -3
- package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
- package/dist/structure/StructureScene.svelte +365 -198
- package/dist/structure/StructureScene.svelte.d.ts +22 -20
- package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
- package/dist/structure/{label-placement.js → atom-label-placement.js} +12 -2
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +11 -16
- package/dist/structure/bond-order-perception.js +2 -4
- package/dist/structure/bonding.d.ts +3 -0
- package/dist/structure/bonding.js +91 -48
- package/dist/structure/export.d.ts +24 -4
- package/dist/structure/export.js +64 -122
- package/dist/structure/index.d.ts +2 -0
- package/dist/structure/index.js +2 -0
- package/dist/structure/parse.d.ts +3 -2
- package/dist/structure/parse.js +333 -370
- package/dist/structure/partial-occupancy.d.ts +0 -1
- package/dist/structure/partial-occupancy.js +1 -1
- package/dist/structure/pbc.d.ts +1 -1
- package/dist/structure/pbc.js +186 -13
- package/dist/structure/polyhedra.d.ts +41 -0
- package/dist/structure/polyhedra.js +602 -0
- package/dist/structure/site.d.ts +4 -0
- package/dist/structure/site.js +1 -0
- package/dist/structure/supercell.js +3 -2
- package/dist/structure/validation.js +5 -6
- package/dist/symmetry/SymmetryElementControls.svelte +69 -0
- package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
- package/dist/symmetry/SymmetryElements.svelte +354 -0
- package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
- package/dist/symmetry/SymmetryStats.svelte +111 -6
- package/dist/symmetry/WyckoffTable.svelte +68 -7
- package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
- package/dist/symmetry/cell-transform.js +7 -14
- package/dist/symmetry/index.d.ts +14 -4
- package/dist/symmetry/index.js +301 -80
- package/dist/symmetry/spacegroups.d.ts +5 -1
- package/dist/symmetry/spacegroups.js +15 -1
- 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 +4 -15
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/trajectory/Trajectory.svelte +58 -61
- package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
- package/dist/trajectory/TrajectoryExportPane.svelte +15 -24
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
- package/dist/trajectory/TrajectoryInfoPane.svelte +3 -2
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
- package/dist/trajectory/constants.js +6 -2
- package/dist/trajectory/extract.js +17 -37
- package/dist/trajectory/format-detect.d.ts +0 -1
- package/dist/trajectory/format-detect.js +3 -9
- package/dist/trajectory/frame-reader.d.ts +0 -1
- package/dist/trajectory/frame-reader.js +62 -128
- package/dist/trajectory/helpers.d.ts +10 -2
- package/dist/trajectory/helpers.js +56 -36
- 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/index.d.ts +1 -1
- package/dist/trajectory/parse/index.js +54 -102
- package/dist/trajectory/parse/lammps.d.ts +0 -2
- package/dist/trajectory/parse/lammps.js +8 -6
- package/dist/trajectory/parse/pymatgen.d.ts +2 -0
- package/dist/trajectory/parse/pymatgen.js +74 -0
- package/dist/trajectory/parse/vasp.js +4 -3
- package/dist/trajectory/parse/xyz.d.ts +9 -21
- package/dist/trajectory/parse/xyz.js +28 -33
- package/dist/trajectory/plotting.d.ts +0 -1
- package/dist/trajectory/plotting.js +3 -100
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +1 -1
- package/dist/xrd/XrdPlot.svelte +14 -29
- package/dist/xrd/broadening.d.ts +2 -1
- package/dist/xrd/calc-xrd.js +6 -11
- package/dist/xrd/index.d.ts +2 -2
- package/package.json +29 -16
- 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/structure/parse.js
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import { COMPRESSION_EXTENSIONS_REGEX, CONFIG_DIRS_REGEX, STRUCT_KEYWORDS_REGEX, STRUCT_KEYWORDS_STRICT_REGEX, STRUCTURE_EXTENSIONS_REGEX, TRAJ_KEYWORDS_REGEX, VASP_FILES_REGEX, XYZ_EXTXYZ_REGEX, } from '../constants';
|
|
2
|
-
import {
|
|
2
|
+
import { FALLBACK_ELEMENTS, is_elem_symbol } from '../element';
|
|
3
|
+
import { strip_compression_extensions } from '../io';
|
|
3
4
|
import * as math from '../math';
|
|
4
5
|
import { wrap_to_unit_cell } from './pbc';
|
|
5
|
-
import {
|
|
6
|
+
import { make_site } from './site';
|
|
7
|
+
import { iter_xyz_frames } from '../trajectory/helpers';
|
|
8
|
+
import { normalize_scientific_notation, to_error } from '../utils';
|
|
6
9
|
import { load as yaml_load } from 'js-yaml';
|
|
10
|
+
// === Parse error contract ===
|
|
11
|
+
// Individual format parsers (parse_poscar, parse_cif, parse_xyz, parse_phonopy_yaml,
|
|
12
|
+
// parse_optimade_json, ...) return `T | null` on failure and record failure reasons in a
|
|
13
|
+
// module-level collector (mirrored to the console). The top-level entry points
|
|
14
|
+
// parse_structure_file and parse_any_structure reset the collector on entry and THROW a
|
|
15
|
+
// descriptive Error aggregating the recorded reasons when nothing parses, so failure
|
|
16
|
+
// causes can reach the UI (callers surface error.message). Warnings (element-symbol
|
|
17
|
+
// fallbacks, skipped atoms, ...) never fail a parse and only go to the console.
|
|
18
|
+
let parse_errors = [];
|
|
19
|
+
const reset_parse_diagnostics = () => {
|
|
20
|
+
parse_errors = [];
|
|
21
|
+
};
|
|
22
|
+
// Record a failure reason; with `error` present, logs in `console.error('msg:', error)` form
|
|
23
|
+
const diag_error = (message, error) => {
|
|
24
|
+
const detail = error === undefined ? `` : `: ${to_error(error).message}`;
|
|
25
|
+
parse_errors.push(`${message}${detail}`);
|
|
26
|
+
if (error === undefined)
|
|
27
|
+
console.error(message);
|
|
28
|
+
else
|
|
29
|
+
console.error(`${message}:`, error);
|
|
30
|
+
};
|
|
31
|
+
const diag_warn = (message) => console.warn(message);
|
|
32
|
+
// Aggregate recorded failure reasons into the Error thrown by top-level entry points
|
|
33
|
+
const aggregate_parse_error = (filename) => {
|
|
34
|
+
const reasons = [...new Set(parse_errors)];
|
|
35
|
+
const detail = reasons.length ? `: ${reasons.join(`; `)}` : ``;
|
|
36
|
+
return new Error(`Failed to parse structure${filename ? ` from '${filename}'` : ``}${detail}`);
|
|
37
|
+
};
|
|
7
38
|
const cif_coords_key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
|
|
8
39
|
const cif_site_key = (element, abc, label) => `${element}|${label}|${cif_coords_key(abc)}`;
|
|
9
40
|
const clone_structure_properties = (properties) => structuredClone(properties);
|
|
10
|
-
const FALLBACK_ELEMENTS = [`H`, `He`, `Li`, `Be`, `B`, `C`, `N`, `O`, `F`, `Ne`];
|
|
11
|
-
const is_known_element_symbol = (symbol) => ELEM_SYMBOLS.includes(symbol);
|
|
12
41
|
const vec3_from_values = (values, context) => {
|
|
13
42
|
if (values?.length !== 3) {
|
|
14
43
|
throw new Error(`Invalid ${context}: expected 3 coordinates, got ${values?.length ?? 0}`);
|
|
@@ -54,11 +83,11 @@ function parse_coordinate_line(line) {
|
|
|
54
83
|
function validate_element_symbol(symbol, index) {
|
|
55
84
|
// Clean symbol (remove suffixes like _pv, /hash)
|
|
56
85
|
const clean_symbol = symbol.split(/[_/]/)[0];
|
|
57
|
-
if (
|
|
86
|
+
if (is_elem_symbol(clean_symbol))
|
|
58
87
|
return clean_symbol;
|
|
59
88
|
// Fallback to default elements by atomic number
|
|
60
89
|
const fallback = FALLBACK_ELEMENTS[index % FALLBACK_ELEMENTS.length] ?? `H`;
|
|
61
|
-
|
|
90
|
+
diag_warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
|
|
62
91
|
return fallback;
|
|
63
92
|
}
|
|
64
93
|
// Per OPTIMADE spec, species_at_sites holds species NAMES (e.g. 'Si1') resolved via the
|
|
@@ -70,7 +99,7 @@ function resolve_optimade_element(species_name, species_list, index) {
|
|
|
70
99
|
const spec = species_list?.find((entry) => entry.name === species_name);
|
|
71
100
|
let best;
|
|
72
101
|
for (const [sym_idx, symbol] of (spec?.chemical_symbols ?? []).entries()) {
|
|
73
|
-
if (!
|
|
102
|
+
if (!is_elem_symbol(symbol))
|
|
74
103
|
continue;
|
|
75
104
|
const conc = spec?.concentration?.[sym_idx] ?? 0;
|
|
76
105
|
if (!best || conc > best.conc)
|
|
@@ -81,7 +110,7 @@ function resolve_optimade_element(species_name, species_list, index) {
|
|
|
81
110
|
// Fallback: the name may be an element with a trailing atom index (e.g. 'O1');
|
|
82
111
|
// element symbols never contain digits, so stripping them is safe
|
|
83
112
|
const stripped = species_name.replace(/\d+$/, ``);
|
|
84
|
-
if (
|
|
113
|
+
if (is_elem_symbol(stripped))
|
|
85
114
|
return { symbol: stripped, sym_idx: -1 };
|
|
86
115
|
return { symbol: validate_element_symbol(species_name, index), sym_idx: -1 };
|
|
87
116
|
}
|
|
@@ -98,20 +127,39 @@ const approximate_cart_to_frac = (xyz, axis_lengths) => [
|
|
|
98
127
|
Math.abs(axis_lengths[1]) > math.EPS ? xyz[1] / axis_lengths[1] : 0,
|
|
99
128
|
Math.abs(axis_lengths[2]) > math.EPS ? xyz[2] / axis_lengths[2] : 0,
|
|
100
129
|
];
|
|
101
|
-
//
|
|
130
|
+
// Build a 3x3 matrix from 3 row vectors; error context is suffixed with the 1-based row index
|
|
131
|
+
const matrix3x3_from_rows = (rows, context) => [
|
|
132
|
+
vec3_from_values(rows[0], `${context} 1`),
|
|
133
|
+
vec3_from_values(rows[1], `${context} 2`),
|
|
134
|
+
vec3_from_values(rows[2], `${context} 3`),
|
|
135
|
+
];
|
|
136
|
+
// cart→frac converter that falls back to per-axis-length division for singular lattices.
|
|
137
|
+
// axis_lengths defaults to the row norms of the lattice matrix.
|
|
138
|
+
const cart_to_frac_with_fallback = (matrix, axis_lengths) => {
|
|
139
|
+
const exact_converter = try_create_cart_to_frac(matrix);
|
|
140
|
+
if (exact_converter)
|
|
141
|
+
return { convert: exact_converter, exact: true };
|
|
142
|
+
const lengths = axis_lengths ?? [
|
|
143
|
+
Math.hypot(...matrix[0]),
|
|
144
|
+
Math.hypot(...matrix[1]),
|
|
145
|
+
Math.hypot(...matrix[2]),
|
|
146
|
+
];
|
|
147
|
+
return { convert: (xyz) => approximate_cart_to_frac(xyz, lengths), exact: false };
|
|
148
|
+
};
|
|
149
|
+
// @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse VASP POSCAR.
|
|
102
150
|
export function parse_poscar(content) {
|
|
103
151
|
try {
|
|
104
152
|
// Strip only horizontal whitespace: a blank first (comment) line is valid POSCAR
|
|
105
153
|
const lines = content.replace(/^[ \t]+/, ``).split(/\r?\n/);
|
|
106
154
|
if (lines.length < 8) {
|
|
107
|
-
|
|
155
|
+
diag_error(`POSCAR file too short`);
|
|
108
156
|
return null;
|
|
109
157
|
}
|
|
110
158
|
// Scale line: one value (negative = target volume) or three per-axis Cartesian factors
|
|
111
159
|
const scale_tokens = lines[1].trim().split(/\s+/).map(parseFloat);
|
|
112
160
|
let scale_factor = scale_tokens[0];
|
|
113
161
|
if (isNaN(scale_factor)) {
|
|
114
|
-
|
|
162
|
+
diag_error(`Invalid scaling factor in POSCAR`);
|
|
115
163
|
return null;
|
|
116
164
|
}
|
|
117
165
|
const scale_vec = scale_tokens.slice(0, 3);
|
|
@@ -129,6 +177,10 @@ export function parse_poscar(content) {
|
|
|
129
177
|
// Handle negative scale factor (volume-based scaling, single-factor form only)
|
|
130
178
|
if (!per_axis_scale && scale_factor < 0) {
|
|
131
179
|
const volume = Math.abs(math.det_3x3(lattice_vecs));
|
|
180
|
+
if (volume < math.EPS) {
|
|
181
|
+
diag_error(`POSCAR target-volume scaling requires a non-singular lattice`);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
132
184
|
scale_factor = (-scale_factor / volume) ** (1 / 3);
|
|
133
185
|
}
|
|
134
186
|
// Scale lattice vectors (per-axis factors multiply Cartesian components)
|
|
@@ -183,136 +235,104 @@ export function parse_poscar(content) {
|
|
|
183
235
|
line_index += 1;
|
|
184
236
|
}
|
|
185
237
|
if (element_symbols.length !== atom_counts.length) {
|
|
186
|
-
|
|
238
|
+
diag_error(`Mismatch between element symbols and atom counts`);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (line_index >= lines.length) {
|
|
242
|
+
diag_error(`Missing coordinate mode line in POSCAR`);
|
|
187
243
|
return null;
|
|
188
244
|
}
|
|
189
245
|
// Check for selective dynamics
|
|
190
246
|
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
|
-
}
|
|
247
|
+
let coordinate_mode = lines[line_index].trim().toUpperCase();
|
|
248
|
+
if (coordinate_mode.startsWith(`S`)) {
|
|
249
|
+
has_selective_dynamics = true;
|
|
250
|
+
line_index += 1;
|
|
251
|
+
if (line_index < lines.length) {
|
|
252
|
+
coordinate_mode = lines[line_index].trim().toUpperCase();
|
|
203
253
|
}
|
|
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}`);
|
|
254
|
+
else {
|
|
255
|
+
diag_error(`Missing coordinate mode after selective dynamics`);
|
|
209
256
|
return null;
|
|
210
257
|
}
|
|
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
|
-
|
|
258
|
+
}
|
|
259
|
+
// Determine coordinate mode
|
|
260
|
+
const is_direct = coordinate_mode.startsWith(`D`);
|
|
261
|
+
const is_cartesian = coordinate_mode.startsWith(`C`) || coordinate_mode.startsWith(`K`);
|
|
262
|
+
if (!is_direct && !is_cartesian) {
|
|
263
|
+
diag_error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
// Parse atomic positions
|
|
267
|
+
const poscar_frac_to_cart = math.create_frac_to_cart(scaled_lattice);
|
|
268
|
+
const poscar_cart_to_frac = cart_to_frac_with_fallback(scaled_lattice);
|
|
269
|
+
if (!is_direct && !poscar_cart_to_frac.exact) {
|
|
270
|
+
diag_warn(`POSCAR: singular lattice, using axis-length fallback for cart→frac`);
|
|
271
|
+
}
|
|
272
|
+
const sites = [];
|
|
273
|
+
let atom_index = 0;
|
|
274
|
+
for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
|
|
275
|
+
const element = validate_element_symbol(element_symbols[elem_idx], elem_idx);
|
|
276
|
+
const count = atom_counts[elem_idx];
|
|
277
|
+
for (let atom_count_idx = 0; atom_count_idx < count; atom_count_idx++) {
|
|
278
|
+
const coord_line_idx = line_index + 1 + atom_index + atom_count_idx;
|
|
279
|
+
if (coord_line_idx >= lines.length) {
|
|
280
|
+
diag_error(`Not enough coordinate lines in POSCAR`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const coords = vec3_from_values(parse_coordinate_line(lines[coord_line_idx]), `POSCAR atom coordinates on line ${coord_line_idx + 1}`);
|
|
284
|
+
// Parse selective dynamics if present
|
|
285
|
+
let selective_dynamics;
|
|
286
|
+
if (has_selective_dynamics) {
|
|
287
|
+
const tokens = lines[coord_line_idx].trim().split(/\s+/);
|
|
288
|
+
if (tokens.length >= 6) {
|
|
289
|
+
selective_dynamics = [tokens[3] === `T`, tokens[4] === `T`, tokens[5] === `T`];
|
|
241
290
|
}
|
|
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
291
|
}
|
|
261
|
-
|
|
292
|
+
// Cartesian input is scaled then converted to fractional (axis-length fallback
|
|
293
|
+
// for singular lattices); abc wraps to [0, 1) and xyz is recomputed from it so
|
|
294
|
+
// both stay consistent (singular Cartesian keeps the scaled input as xyz)
|
|
295
|
+
const cart = is_direct ? null : apply_axis_scale(coords);
|
|
296
|
+
const raw_abc = cart ? poscar_cart_to_frac.convert(cart) : coords;
|
|
297
|
+
const abc = wrap_to_unit_cell(raw_abc);
|
|
298
|
+
const xyz = cart && !poscar_cart_to_frac.exact ? cart : poscar_frac_to_cart(abc);
|
|
299
|
+
sites.push(make_site(element, abc, xyz, `${element}${atom_index + atom_count_idx + 1}`, selective_dynamics ? { selective_dynamics } : {}));
|
|
262
300
|
}
|
|
263
|
-
|
|
264
|
-
const structure = {
|
|
265
|
-
sites,
|
|
266
|
-
lattice: { matrix: scaled_lattice, ...lattice_params },
|
|
267
|
-
};
|
|
268
|
-
return structure;
|
|
301
|
+
atom_index += count;
|
|
269
302
|
}
|
|
270
|
-
|
|
271
|
-
return
|
|
303
|
+
const lattice_params = math.calc_lattice_params(scaled_lattice);
|
|
304
|
+
return { sites, lattice: { matrix: scaled_lattice, ...lattice_params } };
|
|
272
305
|
}
|
|
273
306
|
catch (error) {
|
|
274
|
-
|
|
307
|
+
diag_error(`Error parsing POSCAR file`, error);
|
|
275
308
|
return null;
|
|
276
309
|
}
|
|
277
310
|
}
|
|
278
|
-
//
|
|
311
|
+
// @internal parser exported for tests + trajectory parser; public entry points: parse_structure_file/parse_any_structure. Parse standard/extended XYZ (multi-frame).
|
|
279
312
|
export function parse_xyz(content) {
|
|
280
313
|
try {
|
|
281
314
|
const normalized_content = content.trim();
|
|
282
315
|
if (!normalized_content) {
|
|
283
|
-
|
|
316
|
+
diag_error(`Empty XYZ file`);
|
|
284
317
|
return null;
|
|
285
318
|
}
|
|
286
|
-
//
|
|
319
|
+
// Walk frames by reading atom counts; multi-frame XYZ parses only the last frame
|
|
287
320
|
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/);
|
|
321
|
+
let last_frame = null;
|
|
322
|
+
for (const frame of iter_xyz_frames(all_lines))
|
|
323
|
+
last_frame = frame;
|
|
324
|
+
// If no complete frame found, fall back to parsing the whole content as one frame
|
|
325
|
+
const lines = last_frame
|
|
326
|
+
? all_lines.slice(last_frame.start, last_frame.start + last_frame.num_atoms + 2)
|
|
327
|
+
: all_lines;
|
|
308
328
|
if (lines.length < 2) {
|
|
309
|
-
|
|
329
|
+
diag_error(`XYZ frame too short`);
|
|
310
330
|
return null;
|
|
311
331
|
}
|
|
312
332
|
// Parse number of atoms (line 1)
|
|
313
333
|
const num_atoms = parseInt(lines[0].trim(), 10);
|
|
314
334
|
if (isNaN(num_atoms) || num_atoms <= 0) {
|
|
315
|
-
|
|
335
|
+
diag_error(`Invalid number of atoms in XYZ file`);
|
|
316
336
|
return null;
|
|
317
337
|
}
|
|
318
338
|
// Parse comment line (line 2) - may contain lattice info for extended XYZ
|
|
@@ -323,61 +343,54 @@ export function parse_xyz(content) {
|
|
|
323
343
|
if (lattice_match) {
|
|
324
344
|
const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
|
|
325
345
|
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
|
-
];
|
|
346
|
+
const lattice_vectors = matrix3x3_from_rows([lattice_values.slice(0, 3), lattice_values.slice(3, 6), lattice_values.slice(6, 9)], `XYZ lattice vector`);
|
|
331
347
|
const lattice_params = math.calc_lattice_params(lattice_vectors);
|
|
332
348
|
lattice = { matrix: lattice_vectors, ...lattice_params };
|
|
333
349
|
}
|
|
334
350
|
}
|
|
335
351
|
// Parse atomic coordinates (starting from line 3)
|
|
336
|
-
const xyz_axis_lengths = lattice ? [lattice.a, lattice.b, lattice.c] : null;
|
|
337
352
|
let xyz_frac_to_cart = null;
|
|
338
353
|
let xyz_cart_to_frac = null;
|
|
339
354
|
if (lattice) {
|
|
340
355
|
xyz_frac_to_cart = math.create_frac_to_cart(lattice.matrix);
|
|
341
|
-
xyz_cart_to_frac =
|
|
356
|
+
xyz_cart_to_frac = cart_to_frac_with_fallback(lattice.matrix, [
|
|
357
|
+
lattice.a,
|
|
358
|
+
lattice.b,
|
|
359
|
+
lattice.c,
|
|
360
|
+
]).convert;
|
|
342
361
|
}
|
|
343
362
|
const sites = [];
|
|
344
363
|
for (let atom_idx = 0; atom_idx < num_atoms; atom_idx++) {
|
|
345
364
|
const line_idx = atom_idx + 2;
|
|
346
365
|
if (line_idx >= lines.length) {
|
|
347
|
-
|
|
366
|
+
diag_error(`Not enough coordinate lines in XYZ file`);
|
|
348
367
|
return null;
|
|
349
368
|
}
|
|
350
369
|
const parts = lines[line_idx].trim().split(/\s+/);
|
|
351
370
|
if (parts.length < 4) {
|
|
352
|
-
|
|
371
|
+
diag_error(`Invalid coordinate line in XYZ file`);
|
|
353
372
|
return null;
|
|
354
373
|
}
|
|
355
374
|
const element = validate_element_symbol(parts[0], atom_idx);
|
|
356
375
|
const xyz = vec3_from_values(parts.slice(1, 4).map(parse_coordinate), `XYZ atom position ${atom_idx + 1}`);
|
|
357
376
|
// Calculate fractional coordinates if lattice is available
|
|
358
377
|
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);
|
|
378
|
+
if (lattice && xyz_frac_to_cart && xyz_cart_to_frac) {
|
|
363
379
|
// Ensure fractional coordinates are wrapped into [0, 1) for consistency
|
|
364
|
-
abc = wrap_to_unit_cell(
|
|
380
|
+
abc = wrap_to_unit_cell(xyz_cart_to_frac(xyz));
|
|
365
381
|
// Keep rendered atoms inside primary unit cell by recomputing xyz
|
|
366
382
|
const wrapped_xyz = xyz_frac_to_cart(abc);
|
|
367
383
|
xyz[0] = wrapped_xyz[0];
|
|
368
384
|
xyz[1] = wrapped_xyz[1];
|
|
369
385
|
xyz[2] = wrapped_xyz[2];
|
|
370
386
|
}
|
|
371
|
-
|
|
372
|
-
const label = `${element}${atom_idx + 1}`;
|
|
373
|
-
const site = { species, abc, xyz, label, properties: {} };
|
|
374
|
-
sites.push(site);
|
|
387
|
+
sites.push(make_site(element, abc, xyz, `${element}${atom_idx + 1}`));
|
|
375
388
|
}
|
|
376
389
|
const structure = { sites, ...(lattice && { lattice }) };
|
|
377
390
|
return structure;
|
|
378
391
|
}
|
|
379
392
|
catch (error) {
|
|
380
|
-
|
|
393
|
+
diag_error(`Error parsing XYZ file`, error);
|
|
381
394
|
return null;
|
|
382
395
|
}
|
|
383
396
|
}
|
|
@@ -538,6 +551,23 @@ const build_cif_atom_site_header_indices = (headers) => {
|
|
|
538
551
|
});
|
|
539
552
|
return indices;
|
|
540
553
|
};
|
|
554
|
+
// Walk CIF loop_ blocks: yields each loop's header tags plus the index of its first data line
|
|
555
|
+
function* iter_cif_loops(lines) {
|
|
556
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
557
|
+
if (lines[idx].trim() !== `loop_`)
|
|
558
|
+
continue;
|
|
559
|
+
const headers = [];
|
|
560
|
+
let jj = idx + 1;
|
|
561
|
+
while (jj < lines.length && lines[jj].trim().startsWith(`_`)) {
|
|
562
|
+
headers.push(lines[jj].trim());
|
|
563
|
+
jj++;
|
|
564
|
+
}
|
|
565
|
+
yield { headers, data_start: jj };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Split a CIF data line into whitespace-separated tokens, keeping quoted multi-word
|
|
569
|
+
// values as single tokens and stripping the quotes
|
|
570
|
+
const split_cif_tokens = (line) => (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []).map((token) => token.replaceAll(/['"]/g, ``));
|
|
541
571
|
// Parse atom data from CIF with robust error handling
|
|
542
572
|
const parse_cif_atom_data = (raw_data, indices, coords_type) => {
|
|
543
573
|
const { label = 0, symbol = -1, occupancy = -1 } = indices;
|
|
@@ -574,12 +604,12 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
|
|
|
574
604
|
occupancy: occu,
|
|
575
605
|
};
|
|
576
606
|
};
|
|
577
|
-
// Parse CIF (Crystallographic Information File)
|
|
607
|
+
// @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse CIF (Crystallographic Information File).
|
|
578
608
|
export function parse_cif(content, wrap_fractional_coords = true, strict = true) {
|
|
579
609
|
try {
|
|
580
610
|
const text = content.trim();
|
|
581
611
|
if (!text) {
|
|
582
|
-
|
|
612
|
+
diag_error(`CIF file is empty`);
|
|
583
613
|
return null;
|
|
584
614
|
}
|
|
585
615
|
// Find atom site loop that actually contains coordinates (fract or Cartn)
|
|
@@ -587,16 +617,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
587
617
|
let atom_headers = [];
|
|
588
618
|
const atom_data_lines = [];
|
|
589
619
|
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
|
-
}
|
|
620
|
+
for (const { headers, data_start } of iter_cif_loops(lines)) {
|
|
621
|
+
let jj = data_start;
|
|
600
622
|
// Check if this is a symmetry operations loop
|
|
601
623
|
if (headers.some((header) => header.includes(`_symmetry_equiv_pos_as_xyz`) ||
|
|
602
624
|
header.includes(`_space_group_symop_operation_xyz`))) {
|
|
@@ -623,10 +645,8 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
623
645
|
(indices_preview.cart_x !== undefined &&
|
|
624
646
|
indices_preview.cart_y !== undefined &&
|
|
625
647
|
indices_preview.cart_z !== undefined);
|
|
626
|
-
if (!has_coords)
|
|
627
|
-
ii = jj - 1;
|
|
648
|
+
if (!has_coords)
|
|
628
649
|
continue;
|
|
629
|
-
}
|
|
630
650
|
// This is the desired atom-site loop with coordinates: collect data lines
|
|
631
651
|
atom_headers = headers;
|
|
632
652
|
while (jj < lines.length) {
|
|
@@ -653,7 +673,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
653
673
|
break;
|
|
654
674
|
}
|
|
655
675
|
if (atom_headers.length === 0 || atom_data_lines.length === 0) {
|
|
656
|
-
|
|
676
|
+
diag_error(`No valid atom site loop found in CIF file`);
|
|
657
677
|
return null;
|
|
658
678
|
}
|
|
659
679
|
// Parse atom data with error handling
|
|
@@ -669,7 +689,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
669
689
|
? `cart`
|
|
670
690
|
: null;
|
|
671
691
|
if (!coords_type) {
|
|
672
|
-
|
|
692
|
+
diag_error(`CIF atom site loop missing coordinates (fract or Cartn)`);
|
|
673
693
|
return null;
|
|
674
694
|
}
|
|
675
695
|
// Collect required coordinate indices
|
|
@@ -677,12 +697,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
677
697
|
? [header_indices.x, header_indices.y, header_indices.z]
|
|
678
698
|
: [header_indices.cart_x, header_indices.cart_y, header_indices.cart_z];
|
|
679
699
|
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
|
-
})
|
|
700
|
+
.map(split_cif_tokens)
|
|
686
701
|
.filter((tokens) => {
|
|
687
702
|
const { disorder } = header_indices;
|
|
688
703
|
const max_required_idx = Math.max(...required_indices);
|
|
@@ -694,20 +709,20 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
694
709
|
return parse_cif_atom_data(tokens, header_indices, coords_type);
|
|
695
710
|
}
|
|
696
711
|
catch (error) {
|
|
697
|
-
|
|
712
|
+
diag_warn(`Skipping invalid atom data: ${error}`);
|
|
698
713
|
return null;
|
|
699
714
|
}
|
|
700
715
|
})
|
|
701
716
|
.filter((atom) => atom !== null);
|
|
702
717
|
if (atoms.length === 0) {
|
|
703
|
-
|
|
718
|
+
diag_error(`No valid atoms found in CIF file`);
|
|
704
719
|
return null;
|
|
705
720
|
}
|
|
706
721
|
// Extract cell parameters and build lattice
|
|
707
722
|
const lengths = extract_cif_cell_parameters(text, `cell_length`, strict);
|
|
708
723
|
const angles = extract_cif_cell_parameters(text, `cell_angle`, strict);
|
|
709
724
|
if (lengths.length < 3 || angles.length < 3) {
|
|
710
|
-
|
|
725
|
+
diag_error(`Insufficient cell parameters in CIF file`);
|
|
711
726
|
return null;
|
|
712
727
|
}
|
|
713
728
|
// Build lattice and create sites
|
|
@@ -716,9 +731,9 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
716
731
|
const lattice_matrix = math.cell_to_lattice_matrix(a, b, c, alpha, beta, gamma);
|
|
717
732
|
const lattice_params = math.calc_lattice_params(lattice_matrix);
|
|
718
733
|
const frac_to_cart = math.create_frac_to_cart(lattice_matrix);
|
|
719
|
-
const cart_to_frac =
|
|
734
|
+
const cart_to_frac = cart_to_frac_with_fallback(lattice_matrix, [a, b, c]).convert;
|
|
720
735
|
// Create sites with coordinate conversion and symmetry operations
|
|
721
|
-
const wrap_vec3 = (
|
|
736
|
+
const wrap_vec3 = (vec) => wrap_fractional_coords ? wrap_to_unit_cell(vec) : vec;
|
|
722
737
|
// Apply symmetry operations to generate all equivalent positions
|
|
723
738
|
const all_sites = [];
|
|
724
739
|
// Normalize symmetry operations (trim/strip quotes) but preserve duplicates; we deduplicate positions later
|
|
@@ -728,48 +743,32 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
728
743
|
// Rely on symmetry operations list for all centering/translations to avoid double-counting
|
|
729
744
|
// TODO: Support conventional cells with centering by discovering centering from space group metadata
|
|
730
745
|
// when present (e.g. P, I, F, C, R centering types)
|
|
731
|
-
const centering_vectors = [[0, 0, 0]];
|
|
732
746
|
// 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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
hdrs.push(text_lines[lj].trim().toLowerCase());
|
|
744
|
-
lj++;
|
|
745
|
-
}
|
|
746
|
-
const sym_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_symbol`));
|
|
747
|
-
const num_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_number_in_cell`));
|
|
748
|
-
if (sym_idx !== -1 && num_idx !== -1) {
|
|
749
|
-
while (lj < text_lines.length) {
|
|
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++;
|
|
767
|
-
}
|
|
747
|
+
const atom_type_counts = {};
|
|
748
|
+
for (const { headers, data_start } of iter_cif_loops(lines)) {
|
|
749
|
+
const hdrs = headers.map((hdr) => hdr.toLowerCase());
|
|
750
|
+
const sym_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_symbol`));
|
|
751
|
+
const num_idx = hdrs.findIndex((hdr) => hdr.endsWith(`_atom_type_number_in_cell`));
|
|
752
|
+
if (sym_idx === -1 || num_idx === -1)
|
|
753
|
+
continue;
|
|
754
|
+
for (let lj = data_start; lj < lines.length; lj++) {
|
|
755
|
+
const line = lines[lj].trim();
|
|
756
|
+
if (!line || line === `loop_` || line.startsWith(`data_`))
|
|
768
757
|
break;
|
|
758
|
+
if (line.startsWith(`#`))
|
|
759
|
+
continue;
|
|
760
|
+
const toks = split_cif_tokens(line);
|
|
761
|
+
if (toks.length > Math.max(sym_idx, num_idx)) {
|
|
762
|
+
// Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
|
|
763
|
+
const match = /^([A-Z][a-z]*)/.exec(toks[sym_idx]);
|
|
764
|
+
const sym = match ? match[1] : toks[sym_idx];
|
|
765
|
+
const num = parseInt(toks[num_idx], 10);
|
|
766
|
+
if (sym && !Number.isNaN(num))
|
|
767
|
+
atom_type_counts[sym] = num;
|
|
769
768
|
}
|
|
770
769
|
}
|
|
771
|
-
|
|
772
|
-
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
773
772
|
const observed_counts = {};
|
|
774
773
|
for (const atom of atoms) {
|
|
775
774
|
observed_counts[atom.element] = (observed_counts[atom.element] || 0) + 1;
|
|
@@ -794,41 +793,26 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
794
793
|
}
|
|
795
794
|
else {
|
|
796
795
|
const xyz_base = [atom.coords[0], atom.coords[1], atom.coords[2]];
|
|
797
|
-
const atom_abc = wrap_vec3(cart_to_frac
|
|
798
|
-
? cart_to_frac(xyz_base)
|
|
799
|
-
: approximate_cart_to_frac(xyz_base, [a, b, c]));
|
|
796
|
+
const atom_abc = wrap_vec3(cart_to_frac(xyz_base));
|
|
800
797
|
fractional_atom = { ...atom, coords: atom_abc, coords_type: `fract` };
|
|
801
798
|
}
|
|
802
799
|
// First apply symmetry operations in fractional space
|
|
803
800
|
const equiv_atoms = apply_symmetry_ops(fractional_atom, ops_to_use, wrap_fractional_coords);
|
|
804
|
-
// Then apply lattice centering shifts to each equivalent position
|
|
805
801
|
for (const equiv_atom of equiv_atoms) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (seen_site_keys.has(key))
|
|
814
|
-
continue;
|
|
815
|
-
seen_site_keys.add(key);
|
|
816
|
-
const xyz = frac_to_cart(abc);
|
|
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
|
-
});
|
|
824
|
-
}
|
|
802
|
+
const abc = wrap_vec3(equiv_atom.coords);
|
|
803
|
+
const key = cif_site_key(element, abc, equiv_atom.id);
|
|
804
|
+
if (seen_site_keys.has(key))
|
|
805
|
+
continue;
|
|
806
|
+
seen_site_keys.add(key);
|
|
807
|
+
const xyz = frac_to_cart(abc);
|
|
808
|
+
all_sites.push(make_site(element, abc, xyz, equiv_atom.id, {}, equiv_atom.occupancy));
|
|
825
809
|
}
|
|
826
810
|
}
|
|
827
811
|
const sites = all_sites;
|
|
828
812
|
return { sites, lattice: { matrix: lattice_matrix, ...lattice_params } };
|
|
829
813
|
}
|
|
830
814
|
catch (error) {
|
|
831
|
-
|
|
815
|
+
diag_error(`Error parsing CIF file`, error);
|
|
832
816
|
return null;
|
|
833
817
|
}
|
|
834
818
|
}
|
|
@@ -836,11 +820,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
|
|
|
836
820
|
function convert_phonopy_cell(cell) {
|
|
837
821
|
const sites = [];
|
|
838
822
|
// 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
|
-
];
|
|
823
|
+
const lattice_matrix = matrix3x3_from_rows(cell.lattice, `phonopy lattice vector`);
|
|
844
824
|
// Process each atomic site
|
|
845
825
|
const phonopy_frac_to_cart = math.create_frac_to_cart(lattice_matrix);
|
|
846
826
|
for (const point of cell.points) {
|
|
@@ -851,9 +831,7 @@ function convert_phonopy_cell(cell) {
|
|
|
851
831
|
mass: point.mass,
|
|
852
832
|
...(point.reduced_to !== undefined && { reduced_to: point.reduced_to }),
|
|
853
833
|
};
|
|
854
|
-
|
|
855
|
-
const site = { species, abc, xyz, label: point.symbol, properties };
|
|
856
|
-
sites.push(site);
|
|
834
|
+
sites.push(make_site(element, abc, xyz, point.symbol, properties));
|
|
857
835
|
}
|
|
858
836
|
// Calculate lattice parameters
|
|
859
837
|
const calculated_lattice_params = math.calc_lattice_params(lattice_matrix);
|
|
@@ -872,7 +850,7 @@ const get_phonopy_cell = (data, cell_type) => {
|
|
|
872
850
|
const cell = Reflect.get(data, cell_type);
|
|
873
851
|
return is_phonopy_cell(cell) ? cell : undefined;
|
|
874
852
|
};
|
|
875
|
-
// Parse phonopy YAML
|
|
853
|
+
// @internal parser exported for tests; public entry points: parse_structure_file/parse_any_structure. Parse phonopy YAML, returns requested cell type (or preferred single structure).
|
|
876
854
|
export function parse_phonopy_yaml(content, cell_type) {
|
|
877
855
|
try {
|
|
878
856
|
// Parse YAML content but exclude large phonon_displacements array for performance
|
|
@@ -899,7 +877,7 @@ export function parse_phonopy_yaml(content, cell_type) {
|
|
|
899
877
|
const filtered_content = filtered_lines.join(`\n`);
|
|
900
878
|
const data = yaml_load(filtered_content);
|
|
901
879
|
if (!data) {
|
|
902
|
-
|
|
880
|
+
diag_error(`Failed to parse phonopy YAML`);
|
|
903
881
|
return null;
|
|
904
882
|
}
|
|
905
883
|
// If specific cell type requested, parse only that one
|
|
@@ -907,7 +885,7 @@ export function parse_phonopy_yaml(content, cell_type) {
|
|
|
907
885
|
const cell = get_phonopy_cell(data, cell_type);
|
|
908
886
|
if (cell)
|
|
909
887
|
return convert_phonopy_cell(cell);
|
|
910
|
-
|
|
888
|
+
diag_error(`Requested cell type '${cell_type}' not found in phonopy YAML`);
|
|
911
889
|
return null;
|
|
912
890
|
}
|
|
913
891
|
// Auto mode: return preferred structure in order of preference
|
|
@@ -923,11 +901,11 @@ export function parse_phonopy_yaml(content, cell_type) {
|
|
|
923
901
|
get_phonopy_cell(data, `primitive_cell`);
|
|
924
902
|
if (auto_cell)
|
|
925
903
|
return convert_phonopy_cell(auto_cell);
|
|
926
|
-
|
|
904
|
+
diag_error(`No valid cells found in phonopy YAML`);
|
|
927
905
|
return null;
|
|
928
906
|
}
|
|
929
907
|
catch (error) {
|
|
930
|
-
|
|
908
|
+
diag_error(`Error parsing phonopy YAML`, error);
|
|
931
909
|
return null;
|
|
932
910
|
}
|
|
933
911
|
}
|
|
@@ -961,8 +939,8 @@ function find_structure_in_json(obj, visited = new WeakSet()) {
|
|
|
961
939
|
}
|
|
962
940
|
return null;
|
|
963
941
|
}
|
|
964
|
-
// Type guard to validate structure-like objects
|
|
965
|
-
function is_parsed_structure(obj) {
|
|
942
|
+
// Type guard to validate structure-like objects (sites array with species + coordinates)
|
|
943
|
+
export function is_parsed_structure(obj) {
|
|
966
944
|
if (!obj || typeof obj !== `object`)
|
|
967
945
|
return false;
|
|
968
946
|
const sites = `sites` in obj ? obj.sites : undefined;
|
|
@@ -1013,15 +991,12 @@ const detect_json_structure = (content) => {
|
|
|
1013
991
|
const structure = find_structure_in_json(parsed);
|
|
1014
992
|
return structure ? normalize_fractional_coords(structure) : null;
|
|
1015
993
|
};
|
|
1016
|
-
//
|
|
1017
|
-
|
|
994
|
+
// Internal: auto-detect file format, returns null on failure after recording reasons (see parse error contract at top)
|
|
995
|
+
function parse_structure_file_impl(content, filename) {
|
|
1018
996
|
// If a filename is provided, try to detect format by file extension first
|
|
1019
997
|
if (filename) {
|
|
1020
998
|
// 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
|
-
}
|
|
999
|
+
const base_filename = strip_compression_extensions(filename);
|
|
1025
1000
|
const ext = base_filename.split(`.`).pop();
|
|
1026
1001
|
// Try to detect format by file extension
|
|
1027
1002
|
if (ext === `xyz` || ext === `extxyz`)
|
|
@@ -1035,10 +1010,10 @@ export function parse_structure_file(content, filename) {
|
|
|
1035
1010
|
const result = detect_json_structure(content);
|
|
1036
1011
|
if (result)
|
|
1037
1012
|
return result;
|
|
1038
|
-
|
|
1013
|
+
diag_error(`JSON file does not contain a valid structure format`);
|
|
1039
1014
|
}
|
|
1040
1015
|
catch (error) {
|
|
1041
|
-
|
|
1016
|
+
diag_error(`Error parsing JSON file`, error);
|
|
1042
1017
|
}
|
|
1043
1018
|
return null;
|
|
1044
1019
|
}
|
|
@@ -1053,17 +1028,24 @@ export function parse_structure_file(content, filename) {
|
|
|
1053
1028
|
// Try to auto-detect based on content.
|
|
1054
1029
|
// JSON detection must come before the line-count guard: minified JSON
|
|
1055
1030
|
// (e.g. fetched via extensionless blob: object URLs) is a single line.
|
|
1031
|
+
const content_start = content.trimStart();
|
|
1032
|
+
const looks_like_json = content_start.startsWith(`{`) || content_start.startsWith(`[`);
|
|
1056
1033
|
try {
|
|
1057
1034
|
const result = detect_json_structure(content);
|
|
1058
1035
|
if (result)
|
|
1059
1036
|
return result;
|
|
1037
|
+
if (looks_like_json)
|
|
1038
|
+
diag_error(`JSON content does not contain a valid structure format`);
|
|
1060
1039
|
}
|
|
1061
|
-
catch {
|
|
1062
|
-
//
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
// Only swallow silently when content doesn't even look like JSON; otherwise the
|
|
1042
|
+
// syntax error is the most useful failure reason and must be surfaced
|
|
1043
|
+
if (looks_like_json)
|
|
1044
|
+
diag_error(`Invalid JSON`, error);
|
|
1063
1045
|
}
|
|
1064
1046
|
const lines = content.trim().split(/\r?\n/);
|
|
1065
1047
|
if (lines.length < 2) {
|
|
1066
|
-
|
|
1048
|
+
diag_error(`File too short to determine format`);
|
|
1067
1049
|
return null;
|
|
1068
1050
|
}
|
|
1069
1051
|
// XYZ format detection: first line should be a number, second line is comment
|
|
@@ -1113,11 +1095,20 @@ export function parse_structure_file(content, filename) {
|
|
|
1113
1095
|
line.includes(`phonon_supercell:`));
|
|
1114
1096
|
if (has_phonopy_keywords)
|
|
1115
1097
|
return parse_phonopy_yaml(content);
|
|
1116
|
-
|
|
1098
|
+
diag_error(`Unable to determine file format`);
|
|
1117
1099
|
return null;
|
|
1118
1100
|
}
|
|
1119
|
-
//
|
|
1101
|
+
// Auto-detect file format and parse; throws an Error aggregating per-format failure reasons when nothing parses
|
|
1102
|
+
export function parse_structure_file(content, filename) {
|
|
1103
|
+
reset_parse_diagnostics();
|
|
1104
|
+
const structure = parse_structure_file_impl(content, filename);
|
|
1105
|
+
if (structure)
|
|
1106
|
+
return structure;
|
|
1107
|
+
throw aggregate_parse_error(filename);
|
|
1108
|
+
}
|
|
1109
|
+
// Universal parser for JSON and structure files; throws an Error aggregating per-format failure reasons when nothing parses
|
|
1120
1110
|
export function parse_any_structure(content, filename) {
|
|
1111
|
+
reset_parse_diagnostics();
|
|
1121
1112
|
const finalize_structure = (structure) => ({
|
|
1122
1113
|
sites: structure.sites,
|
|
1123
1114
|
charge: 0,
|
|
@@ -1128,23 +1119,21 @@ export function parse_any_structure(content, filename) {
|
|
|
1128
1119
|
lattice: { ...structure.lattice, pbc: [true, true, true] },
|
|
1129
1120
|
}),
|
|
1130
1121
|
});
|
|
1131
|
-
//
|
|
1122
|
+
// Fast path: content is already a serialized structure object
|
|
1132
1123
|
try {
|
|
1133
1124
|
const parsed = JSON.parse(content);
|
|
1134
|
-
// Check if it's already a valid structure using proper type guard
|
|
1135
1125
|
if (is_parsed_structure(parsed)) {
|
|
1136
1126
|
// Normalize coordinates (wrap fractional to [0,1) and recompute Cartesian)
|
|
1137
1127
|
return finalize_structure(normalize_fractional_coords(parsed));
|
|
1138
1128
|
}
|
|
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
1129
|
}
|
|
1143
1130
|
catch {
|
|
1144
|
-
//
|
|
1145
|
-
const parsed = parse_structure_file(content, filename);
|
|
1146
|
-
return parsed ? finalize_structure(parsed) : null;
|
|
1131
|
+
// Not plain JSON — fall through to format detection, which records failure reasons
|
|
1147
1132
|
}
|
|
1133
|
+
const structure = parse_structure_file_impl(content, filename);
|
|
1134
|
+
if (structure)
|
|
1135
|
+
return finalize_structure(structure);
|
|
1136
|
+
throw aggregate_parse_error(filename);
|
|
1148
1137
|
}
|
|
1149
1138
|
// Parse OPTIMADE JSON format
|
|
1150
1139
|
export function parse_optimade_json(content) {
|
|
@@ -1153,16 +1142,80 @@ export function parse_optimade_json(content) {
|
|
|
1153
1142
|
return parse_optimade_from_raw(raw);
|
|
1154
1143
|
}
|
|
1155
1144
|
catch (error) {
|
|
1156
|
-
|
|
1145
|
+
diag_error(`Error parsing OPTIMADE JSON`, error);
|
|
1157
1146
|
return null;
|
|
1158
1147
|
}
|
|
1159
1148
|
}
|
|
1149
|
+
// Build sites + lattice shared by parse_optimade_from_raw and optimade_to_crystal.
|
|
1150
|
+
// on_invalid controls whether invalid positions are skipped with a warning or throw;
|
|
1151
|
+
// site_props extracts per-site mass/concentration from the species list.
|
|
1152
|
+
function build_optimade_sites(attrs, opts) {
|
|
1153
|
+
const positions = attrs.cartesian_site_positions ?? [];
|
|
1154
|
+
const species_at_sites = attrs.species_at_sites ?? [];
|
|
1155
|
+
const species_list = Array.isArray(attrs.species) ? attrs.species : undefined;
|
|
1156
|
+
// OPTIMADE stores lattice vectors as rows, so use as-is
|
|
1157
|
+
const lattice_matrix = attrs.lattice_vectors
|
|
1158
|
+
? matrix3x3_from_rows(attrs.lattice_vectors, `OPTIMADE lattice vector`)
|
|
1159
|
+
: undefined;
|
|
1160
|
+
const lattice_params = lattice_matrix ? math.calc_lattice_params(lattice_matrix) : null;
|
|
1161
|
+
let cart_to_frac = null;
|
|
1162
|
+
if (lattice_matrix && lattice_params) {
|
|
1163
|
+
const converter = cart_to_frac_with_fallback(lattice_matrix, [
|
|
1164
|
+
lattice_params.a,
|
|
1165
|
+
lattice_params.b,
|
|
1166
|
+
lattice_params.c,
|
|
1167
|
+
]);
|
|
1168
|
+
if (!converter.exact) {
|
|
1169
|
+
diag_warn(`Failed to create exact coordinate converter for OPTIMADE structure`);
|
|
1170
|
+
}
|
|
1171
|
+
cart_to_frac = converter.convert;
|
|
1172
|
+
}
|
|
1173
|
+
const sites = [];
|
|
1174
|
+
for (let idx = 0; idx < positions.length; idx++) {
|
|
1175
|
+
const species_name = species_at_sites[idx];
|
|
1176
|
+
if (!species_name) {
|
|
1177
|
+
if (opts.on_invalid === `throw`)
|
|
1178
|
+
throw new Error(`Missing species for site ${idx}`);
|
|
1179
|
+
diag_warn(`Missing species for site ${idx}, skipping`);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
let xyz;
|
|
1183
|
+
try {
|
|
1184
|
+
xyz = vec3_from_values(positions[idx], `OPTIMADE atom position ${idx + 1}`);
|
|
1185
|
+
}
|
|
1186
|
+
catch (error) {
|
|
1187
|
+
if (opts.on_invalid === `throw`)
|
|
1188
|
+
throw error;
|
|
1189
|
+
diag_warn(`Invalid position data at site ${idx}: ${error}`);
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const { symbol: element, sym_idx } = resolve_optimade_element(species_name, species_list, idx);
|
|
1193
|
+
// Calculate fractional coordinates if lattice is available
|
|
1194
|
+
const abc = cart_to_frac ? cart_to_frac(xyz) : [0, 0, 0];
|
|
1195
|
+
const site_props = {};
|
|
1196
|
+
if (opts.site_props) {
|
|
1197
|
+
// Extract mass/concentration for the chosen element. sym_idx indexes the (parallel)
|
|
1198
|
+
// chemical_symbols/mass/concentration arrays; -1 (name resolved directly, no
|
|
1199
|
+
// chemical_symbols) falls back to index 0 — the single-element entry.
|
|
1200
|
+
const spec = species_list?.find((entry) => entry.name === species_name);
|
|
1201
|
+
const spec_idx = Math.max(sym_idx, 0);
|
|
1202
|
+
if (spec?.mass?.[spec_idx] !== undefined)
|
|
1203
|
+
site_props.mass = spec.mass[spec_idx];
|
|
1204
|
+
if (spec?.concentration?.[spec_idx] !== undefined &&
|
|
1205
|
+
spec.concentration[spec_idx] !== 1) {
|
|
1206
|
+
site_props.concentration = spec.concentration[spec_idx];
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
sites.push(make_site(element, abc, xyz, `${element}${idx + 1}`, site_props));
|
|
1210
|
+
}
|
|
1211
|
+
return { sites, lattice_matrix, lattice_params };
|
|
1212
|
+
}
|
|
1160
1213
|
// Parse OPTIMADE from already-parsed JSON
|
|
1161
1214
|
export function parse_optimade_from_raw(raw) {
|
|
1162
1215
|
try {
|
|
1163
1216
|
const structure = extract_optimade_structure_from_raw(raw);
|
|
1164
1217
|
if (!structure) {
|
|
1165
|
-
|
|
1218
|
+
diag_error(`No valid OPTIMADE structure found in JSON`);
|
|
1166
1219
|
return null;
|
|
1167
1220
|
}
|
|
1168
1221
|
const attrs = structure.attributes;
|
|
@@ -1170,83 +1223,28 @@ export function parse_optimade_from_raw(raw) {
|
|
|
1170
1223
|
const positions_raw = attrs.cartesian_site_positions;
|
|
1171
1224
|
const species_raw = attrs.species_at_sites;
|
|
1172
1225
|
if (!(Array.isArray(positions_raw) && Array.isArray(species_raw))) {
|
|
1173
|
-
|
|
1226
|
+
diag_error(`OPTIMADE JSON missing required position or species data`);
|
|
1174
1227
|
return null;
|
|
1175
1228
|
}
|
|
1176
1229
|
if (positions_raw.length !== species_raw.length) {
|
|
1177
|
-
|
|
1230
|
+
diag_error(`OPTIMADE JSON position/species count mismatch`);
|
|
1178
1231
|
return null;
|
|
1179
1232
|
}
|
|
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
|
-
}
|
|
1233
|
+
const { sites, lattice_matrix, lattice_params } = build_optimade_sites(attrs, {
|
|
1234
|
+
on_invalid: `skip`,
|
|
1235
|
+
});
|
|
1233
1236
|
if (sites.length === 0) {
|
|
1234
|
-
|
|
1237
|
+
diag_error(`No valid sites found in OPTIMADE JSON`);
|
|
1235
1238
|
return null;
|
|
1236
1239
|
}
|
|
1237
|
-
|
|
1238
|
-
let lattice;
|
|
1239
|
-
if (lattice_matrix && optimade_lattice_params) {
|
|
1240
|
-
lattice = { matrix: lattice_matrix, ...optimade_lattice_params };
|
|
1241
|
-
}
|
|
1242
|
-
const structure_result = {
|
|
1240
|
+
return {
|
|
1243
1241
|
sites,
|
|
1244
|
-
...(
|
|
1242
|
+
...(lattice_matrix &&
|
|
1243
|
+
lattice_params && { lattice: { matrix: lattice_matrix, ...lattice_params } }),
|
|
1245
1244
|
};
|
|
1246
|
-
return structure_result;
|
|
1247
1245
|
}
|
|
1248
1246
|
catch (error) {
|
|
1249
|
-
|
|
1247
|
+
diag_error(`Error parsing OPTIMADE JSON`, error);
|
|
1250
1248
|
return null;
|
|
1251
1249
|
}
|
|
1252
1250
|
}
|
|
@@ -1268,9 +1266,7 @@ function extract_optimade_structure_from_raw(raw) {
|
|
|
1268
1266
|
const candidate = Array.isArray(payload) ? payload[0] : payload;
|
|
1269
1267
|
return is_optimade_structure_object(candidate) ? candidate : null;
|
|
1270
1268
|
}
|
|
1271
|
-
const unwrap_data = (value) => value && typeof value === `object` && `data` in value
|
|
1272
|
-
? value.data
|
|
1273
|
-
: value;
|
|
1269
|
+
const unwrap_data = (value) => value && typeof value === `object` && `data` in value ? value.data : value;
|
|
1274
1270
|
// Type guard: verify minimal OPTIMADE structure shape
|
|
1275
1271
|
function is_optimade_structure_object(value) {
|
|
1276
1272
|
if (!value || typeof value !== `object`)
|
|
@@ -1286,47 +1282,18 @@ function is_optimade_structure_object(value) {
|
|
|
1286
1282
|
}
|
|
1287
1283
|
// Convert OPTIMADE structure to Crystal format
|
|
1288
1284
|
export function optimade_to_crystal(optimade_structure) {
|
|
1289
|
-
const { lattice_vectors, cartesian_site_positions, species_at_sites, species,
|
|
1285
|
+
const { lattice_vectors, cartesian_site_positions, species_at_sites, species: _species, // excluded from the properties rest
|
|
1286
|
+
...properties } = optimade_structure.attributes;
|
|
1290
1287
|
if (!lattice_vectors || !cartesian_site_positions || !species_at_sites) {
|
|
1291
|
-
|
|
1288
|
+
diag_error(`Missing required OPTIMADE structure data`);
|
|
1292
1289
|
return null;
|
|
1293
1290
|
}
|
|
1294
1291
|
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
|
-
});
|
|
1292
|
+
const { sites, lattice_matrix, lattice_params } = build_optimade_sites(optimade_structure.attributes, { on_invalid: `throw`, site_props: true });
|
|
1293
|
+
if (!lattice_matrix || !lattice_params) {
|
|
1294
|
+
diag_error(`Missing required OPTIMADE structure data`);
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1330
1297
|
return {
|
|
1331
1298
|
sites,
|
|
1332
1299
|
lattice: { matrix: lattice_matrix, ...lattice_params, pbc: [true, true, true] },
|
|
@@ -1335,7 +1302,7 @@ export function optimade_to_crystal(optimade_structure) {
|
|
|
1335
1302
|
};
|
|
1336
1303
|
}
|
|
1337
1304
|
catch (err) {
|
|
1338
|
-
|
|
1305
|
+
diag_error(`Error converting OPTIMADE to Crystal format`, err);
|
|
1339
1306
|
return null;
|
|
1340
1307
|
}
|
|
1341
1308
|
}
|
|
@@ -1369,12 +1336,8 @@ export function is_structure_file(filename) {
|
|
|
1369
1336
|
return false;
|
|
1370
1337
|
}
|
|
1371
1338
|
export const detect_structure_type = (filename, content) => {
|
|
1372
|
-
const lower_filename = filename.toLowerCase();
|
|
1373
1339
|
// 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
|
-
}
|
|
1340
|
+
const name_to_check = strip_compression_extensions(filename);
|
|
1378
1341
|
if (name_to_check.endsWith(`.json`)) {
|
|
1379
1342
|
try {
|
|
1380
1343
|
const parsed = JSON.parse(content);
|