matterviz 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/element/data.js +1 -1
  76. package/dist/feedback/ClickFeedback.svelte +16 -5
  77. package/dist/feedback/DragOverlay.svelte +10 -2
  78. package/dist/feedback/Spinner.svelte +4 -2
  79. package/dist/feedback/StatusMessage.svelte +8 -2
  80. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  81. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  82. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  84. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  86. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  88. package/dist/fermi-surface/compute.js +16 -20
  89. package/dist/fermi-surface/parse.js +24 -14
  90. package/dist/fermi-surface/symmetry.js +2 -7
  91. package/dist/fermi-surface/types.d.ts +3 -5
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  93. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  95. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  96. package/dist/icons.js +47 -0
  97. package/dist/index.d.ts +2 -1
  98. package/dist/index.js +2 -1
  99. package/dist/io/decompress.js +1 -1
  100. package/dist/io/export.d.ts +3 -0
  101. package/dist/io/export.js +129 -143
  102. package/dist/io/is-binary.js +2 -3
  103. package/dist/io/url-drop.js +1 -2
  104. package/dist/isosurface/Isosurface.svelte +202 -148
  105. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  106. package/dist/isosurface/parse.js +34 -29
  107. package/dist/isosurface/slice.js +5 -10
  108. package/dist/isosurface/types.d.ts +2 -1
  109. package/dist/isosurface/types.js +61 -12
  110. package/dist/labels.js +11 -8
  111. package/dist/layout/FullscreenToggle.svelte +11 -2
  112. package/dist/layout/InfoCard.svelte +38 -6
  113. package/dist/layout/InfoTag.svelte +63 -32
  114. package/dist/layout/PropertyFilter.svelte +82 -37
  115. package/dist/layout/SettingsSection.svelte +85 -55
  116. package/dist/layout/SubpageGrid.svelte +10 -2
  117. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  118. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  119. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  120. package/dist/layout/json-tree/utils.js +4 -2
  121. package/dist/marching-cubes.js +25 -2
  122. package/dist/math.d.ts +13 -17
  123. package/dist/math.js +133 -67
  124. package/dist/overlays/ContextMenu.svelte +65 -40
  125. package/dist/overlays/DraggablePane.svelte +211 -139
  126. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  127. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  128. package/dist/periodic-table/PropertySelect.svelte +25 -7
  129. package/dist/periodic-table/TableInset.svelte +8 -3
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  131. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  133. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  137. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  138. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  139. package/dist/phase-diagram/build-diagram.js +9 -9
  140. package/dist/phase-diagram/colors.js +1 -3
  141. package/dist/phase-diagram/parse.js +10 -9
  142. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  143. package/dist/phase-diagram/utils.d.ts +1 -0
  144. package/dist/phase-diagram/utils.js +80 -25
  145. package/dist/plot/AxisLabel.svelte +28 -3
  146. package/dist/plot/BarPlot.svelte +1182 -734
  147. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  148. package/dist/plot/BarPlotControls.svelte +31 -5
  149. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  150. package/dist/plot/ColorBar.svelte +479 -329
  151. package/dist/plot/ColorScaleSelect.svelte +27 -6
  152. package/dist/plot/ElementScatter.svelte +36 -15
  153. package/dist/plot/FillArea.svelte +152 -95
  154. package/dist/plot/Histogram.svelte +934 -571
  155. package/dist/plot/Histogram.svelte.d.ts +1 -1
  156. package/dist/plot/HistogramControls.svelte +53 -9
  157. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  158. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  159. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  160. package/dist/plot/Line.svelte +63 -28
  161. package/dist/plot/PlotControls.svelte +157 -114
  162. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  163. package/dist/plot/PlotLegend.svelte +174 -91
  164. package/dist/plot/PlotTooltip.svelte +45 -6
  165. package/dist/plot/PortalSelect.svelte +175 -147
  166. package/dist/plot/ReferenceLine.svelte +76 -22
  167. package/dist/plot/ReferenceLine3D.svelte +132 -107
  168. package/dist/plot/ReferencePlane.svelte +146 -121
  169. package/dist/plot/ScatterPlot.svelte +1681 -1091
  170. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  171. package/dist/plot/ScatterPlot3D.svelte +256 -131
  172. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  173. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  174. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  175. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  176. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  177. package/dist/plot/ScatterPlotControls.svelte +65 -25
  178. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  179. package/dist/plot/ScatterPoint.svelte +98 -26
  180. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  181. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  182. package/dist/plot/Surface3D.svelte +159 -108
  183. package/dist/plot/ZeroLines.svelte +55 -3
  184. package/dist/plot/ZoomRect.svelte +4 -2
  185. package/dist/plot/axis-utils.js +1 -3
  186. package/dist/plot/data-cleaning.js +12 -28
  187. package/dist/plot/data-transform.js +2 -1
  188. package/dist/plot/fill-utils.js +2 -0
  189. package/dist/plot/layout.d.ts +4 -1
  190. package/dist/plot/layout.js +33 -14
  191. package/dist/plot/reference-line.d.ts +2 -2
  192. package/dist/plot/reference-line.js +7 -5
  193. package/dist/plot/scales.js +24 -36
  194. package/dist/plot/types.d.ts +11 -23
  195. package/dist/plot/types.js +6 -11
  196. package/dist/plot/utils/label-placement.d.ts +32 -15
  197. package/dist/plot/utils/label-placement.js +227 -66
  198. package/dist/plot/utils/series-visibility.js +2 -3
  199. package/dist/rdf/RdfPlot.svelte +143 -91
  200. package/dist/rdf/calc-rdf.js +4 -5
  201. package/dist/sanitize.d.ts +4 -0
  202. package/dist/sanitize.js +107 -0
  203. package/dist/settings.d.ts +18 -6
  204. package/dist/settings.js +46 -16
  205. package/dist/spectral/Bands.svelte +632 -453
  206. package/dist/spectral/BandsAndDos.svelte +90 -49
  207. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  208. package/dist/spectral/Dos.svelte +389 -258
  209. package/dist/spectral/helpers.js +55 -43
  210. package/dist/state.svelte.d.ts +1 -1
  211. package/dist/state.svelte.js +3 -2
  212. package/dist/structure/Arrow.svelte +59 -20
  213. package/dist/structure/AtomLegend.svelte +215 -134
  214. package/dist/structure/Bond.svelte +73 -47
  215. package/dist/structure/CanvasTooltip.svelte +10 -2
  216. package/dist/structure/CellSelect.svelte +72 -45
  217. package/dist/structure/Cylinder.svelte +33 -17
  218. package/dist/structure/Lattice.svelte +88 -33
  219. package/dist/structure/Structure.svelte +1063 -797
  220. package/dist/structure/Structure.svelte.d.ts +1 -1
  221. package/dist/structure/StructureControls.svelte +349 -118
  222. package/dist/structure/StructureExportPane.svelte +124 -89
  223. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  224. package/dist/structure/StructureInfoPane.svelte +304 -237
  225. package/dist/structure/StructureScene.svelte +879 -443
  226. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  227. package/dist/structure/atom-properties.js +8 -8
  228. package/dist/structure/bonding.js +6 -7
  229. package/dist/structure/export.js +14 -29
  230. package/dist/structure/ferrox-wasm.js +1 -1
  231. package/dist/structure/index.d.ts +13 -3
  232. package/dist/structure/index.js +83 -23
  233. package/dist/structure/measure.d.ts +2 -2
  234. package/dist/structure/measure.js +4 -44
  235. package/dist/structure/parse.js +113 -141
  236. package/dist/structure/partial-occupancy.js +7 -10
  237. package/dist/structure/pbc.d.ts +1 -0
  238. package/dist/structure/pbc.js +16 -6
  239. package/dist/structure/supercell.d.ts +2 -2
  240. package/dist/structure/supercell.js +12 -22
  241. package/dist/structure/validation.js +1 -2
  242. package/dist/symmetry/SymmetryStats.svelte +84 -41
  243. package/dist/symmetry/WyckoffTable.svelte +26 -6
  244. package/dist/symmetry/cell-transform.js +5 -3
  245. package/dist/symmetry/index.js +8 -7
  246. package/dist/symmetry/spacegroups.js +148 -148
  247. package/dist/table/HeatmapTable.svelte +790 -554
  248. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  249. package/dist/table/ToggleMenu.svelte +125 -92
  250. package/dist/table/index.js +2 -4
  251. package/dist/theme/ThemeControl.svelte +21 -12
  252. package/dist/time.js +4 -1
  253. package/dist/tooltip/TooltipContent.svelte +33 -8
  254. package/dist/trajectory/Trajectory.svelte +758 -558
  255. package/dist/trajectory/TrajectoryError.svelte +14 -3
  256. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  257. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  258. package/dist/trajectory/extract.js +10 -26
  259. package/dist/trajectory/format-detect.js +5 -5
  260. package/dist/trajectory/frame-reader.d.ts +1 -1
  261. package/dist/trajectory/frame-reader.js +5 -12
  262. package/dist/trajectory/helpers.d.ts +0 -1
  263. package/dist/trajectory/helpers.js +2 -17
  264. package/dist/trajectory/index.js +14 -12
  265. package/dist/trajectory/parse/ase.js +5 -4
  266. package/dist/trajectory/parse/hdf5.js +26 -18
  267. package/dist/trajectory/parse/index.js +13 -18
  268. package/dist/trajectory/parse/lammps.js +17 -7
  269. package/dist/trajectory/parse/vasp.js +5 -2
  270. package/dist/trajectory/parse/xyz.js +8 -7
  271. package/dist/trajectory/plotting.js +13 -8
  272. package/dist/utils.d.ts +1 -0
  273. package/dist/utils.js +13 -0
  274. package/dist/xrd/XrdPlot.svelte +337 -247
  275. package/dist/xrd/broadening.js +14 -9
  276. package/dist/xrd/calc-xrd.js +12 -18
  277. package/dist/xrd/parse.d.ts +1 -1
  278. package/dist/xrd/parse.js +17 -17
  279. package/package.json +99 -103
  280. package/readme.md +1 -1
  281. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -43,6 +43,19 @@ function validate_element_symbol(symbol, index) {
43
43
  console.warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
44
44
  return fallback;
45
45
  }
46
+ const try_create_cart_to_frac = (lattice_matrix) => {
47
+ try {
48
+ return math.create_cart_to_frac(lattice_matrix);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ };
54
+ const approximate_cart_to_frac = (xyz, axis_lengths) => [
55
+ Math.abs(axis_lengths[0]) > math.EPS ? xyz[0] / axis_lengths[0] : 0,
56
+ Math.abs(axis_lengths[1]) > math.EPS ? xyz[1] / axis_lengths[1] : 0,
57
+ Math.abs(axis_lengths[2]) > math.EPS ? xyz[2] / axis_lengths[2] : 0,
58
+ ];
46
59
  // Parse VASP POSCAR file format
47
60
  export function parse_poscar(content) {
48
61
  try {
@@ -97,9 +110,7 @@ export function parse_poscar(content) {
97
110
  for (let lookahead_idx = 1; lookahead_idx < 10; lookahead_idx++) {
98
111
  if (line_index + lookahead_idx >= lines.length)
99
112
  break;
100
- const next_line_first_token = lines[line_index + lookahead_idx]
101
- .trim()
102
- .split(/\s+/)[0];
113
+ const next_line_first_token = lines[line_index + lookahead_idx].trim().split(/\s+/)[0];
103
114
  const next_token_as_number = parseInt(next_line_first_token);
104
115
  if (!isNaN(next_token_as_number)) {
105
116
  symbol_lines = lookahead_idx;
@@ -151,13 +162,18 @@ export function parse_poscar(content) {
151
162
  }
152
163
  // Determine coordinate mode
153
164
  const is_direct = coordinate_mode.startsWith(`D`);
154
- const is_cartesian = coordinate_mode.startsWith(`C`) ||
155
- coordinate_mode.startsWith(`K`);
165
+ const is_cartesian = coordinate_mode.startsWith(`C`) || coordinate_mode.startsWith(`K`);
156
166
  if (!is_direct && !is_cartesian) {
157
167
  console.error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
158
168
  return null;
159
169
  }
160
170
  // Parse atomic positions
171
+ const poscar_axis_lengths = scaled_lattice.map((lattice_vec) => Math.hypot(...lattice_vec));
172
+ const poscar_frac_to_cart = math.create_frac_to_cart(scaled_lattice);
173
+ const poscar_cart_to_frac = try_create_cart_to_frac(scaled_lattice);
174
+ if (!is_direct && !poscar_cart_to_frac) {
175
+ console.warn(`POSCAR: singular lattice, using axis-length fallback for cart→frac`);
176
+ }
161
177
  const sites = [];
162
178
  let atom_index = 0;
163
179
  for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
@@ -175,11 +191,7 @@ export function parse_poscar(content) {
175
191
  if (has_selective_dynamics) {
176
192
  const tokens = lines[coord_line_idx].trim().split(/\s+/);
177
193
  if (tokens.length >= 6) {
178
- selective_dynamics = [
179
- tokens[3] === `T`,
180
- tokens[4] === `T`,
181
- tokens[5] === `T`,
182
- ];
194
+ selective_dynamics = [tokens[3] === `T`, tokens[4] === `T`, tokens[5] === `T`];
183
195
  }
184
196
  }
185
197
  let xyz;
@@ -187,45 +199,24 @@ export function parse_poscar(content) {
187
199
  const [x, y, z] = coords;
188
200
  if (is_direct) {
189
201
  // Store fractional coordinates, wrapping to [0, 1) range
190
- abc = [x - Math.floor(x), y - Math.floor(y), z - Math.floor(z)];
191
- // Convert fractional to Cartesian coordinates
192
- const lattice_transposed = math.transpose_3x3_matrix(scaled_lattice);
193
- xyz = math.mat3x3_vec3_multiply(lattice_transposed, abc);
202
+ abc = wrap_to_unit_cell([x, y, z]);
203
+ xyz = poscar_frac_to_cart(abc);
194
204
  }
195
- else { // Already Cartesian, scale if needed
205
+ else {
206
+ // Already Cartesian, scale if needed
196
207
  xyz = math.scale([x, y, z], scale_factor);
197
- // Calculate fractional coordinates using proper matrix inversion
198
- // Note: Our lattice matrix is stored as row vectors, but for coordinate conversion
199
- // we need column vectors, so we transpose before inversion
200
- let raw_abc;
201
- try {
202
- const lattice_transposed = math.transpose_3x3_matrix(scaled_lattice);
203
- const lattice_inv = math.matrix_inverse_3x3(lattice_transposed);
204
- raw_abc = math.mat3x3_vec3_multiply(lattice_inv, xyz);
205
- }
206
- catch {
207
- // Fallback to simplified method if matrix is singular
208
- raw_abc = [
209
- xyz[0] / scaled_lattice[0][0],
210
- xyz[1] / scaled_lattice[1][1],
211
- xyz[2] / scaled_lattice[2][2],
212
- ];
213
- }
208
+ const raw_abc = poscar_cart_to_frac
209
+ ? poscar_cart_to_frac(xyz)
210
+ : approximate_cart_to_frac(xyz, poscar_axis_lengths);
214
211
  // Wrap fractional coordinates to [0, 1) range
215
- abc = [
216
- raw_abc[0] - Math.floor(raw_abc[0]),
217
- raw_abc[1] - Math.floor(raw_abc[1]),
218
- raw_abc[2] - Math.floor(raw_abc[2]),
219
- ];
212
+ abc = wrap_to_unit_cell(raw_abc);
220
213
  }
221
214
  const site = {
222
215
  species: [{ element, occu: 1, oxidation_state: 0 }],
223
216
  abc,
224
217
  xyz,
225
218
  label: `${element}${atom_index + atom_count_idx + 1}`,
226
- properties: selective_dynamics
227
- ? { selective_dynamics: selective_dynamics }
228
- : {},
219
+ properties: selective_dynamics ? { selective_dynamics: selective_dynamics } : {},
229
220
  };
230
221
  sites.push(site);
231
222
  }
@@ -262,9 +253,7 @@ export function parse_xyz(content) {
262
253
  let line_idx = 0;
263
254
  while (line_idx < all_lines.length) {
264
255
  const numAtoms = parseInt(all_lines[line_idx].trim(), 10);
265
- if (!isNaN(numAtoms) &&
266
- numAtoms > 0 &&
267
- line_idx + numAtoms + 1 < all_lines.length) {
256
+ if (!isNaN(numAtoms) && numAtoms > 0 && line_idx + numAtoms + 1 < all_lines.length) {
268
257
  const frameLines = all_lines.slice(line_idx, line_idx + numAtoms + 2);
269
258
  frames.push(frameLines.join(`\n`));
270
259
  line_idx += numAtoms + 2;
@@ -292,7 +281,7 @@ export function parse_xyz(content) {
292
281
  const comment_line = lines[1];
293
282
  let lattice;
294
283
  // Check for extended XYZ lattice information in comment line
295
- const lattice_match = comment_line.match(/Lattice="([^"]+)"/);
284
+ const lattice_match = /Lattice="([^"]+)"/.exec(comment_line);
296
285
  if (lattice_match) {
297
286
  const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
298
287
  if (lattice_values.length === 9) {
@@ -306,6 +295,13 @@ export function parse_xyz(content) {
306
295
  }
307
296
  }
308
297
  // Parse atomic coordinates (starting from line 3)
298
+ const xyz_axis_lengths = lattice ? [lattice.a, lattice.b, lattice.c] : null;
299
+ let xyz_frac_to_cart = null;
300
+ let xyz_cart_to_frac = null;
301
+ if (lattice) {
302
+ xyz_frac_to_cart = math.create_frac_to_cart(lattice.matrix);
303
+ xyz_cart_to_frac = try_create_cart_to_frac(lattice.matrix);
304
+ }
309
305
  const sites = [];
310
306
  for (let atom_idx = 0; atom_idx < num_atoms; atom_idx++) {
311
307
  const line_idx = atom_idx + 2;
@@ -328,29 +324,14 @@ export function parse_xyz(content) {
328
324
  const xyz = [coords[0], coords[1], coords[2]];
329
325
  // Calculate fractional coordinates if lattice is available
330
326
  let abc = [0, 0, 0];
331
- if (lattice) {
332
- // Calculate fractional coordinates using proper matrix inversion
333
- // Note: Our lattice matrix is stored as row vectors, but for coordinate conversion
334
- // we need column vectors, so we transpose before inversion
335
- try {
336
- const lattice_transposed = math.transpose_3x3_matrix(lattice.matrix);
337
- const lattice_inv = math.matrix_inverse_3x3(lattice_transposed);
338
- abc = math.mat3x3_vec3_multiply(lattice_inv, xyz);
339
- }
340
- catch {
341
- // Fallback to simplified method if matrix is singular
342
- abc = [xyz[0] / lattice.a, xyz[1] / lattice.b, xyz[2] / lattice.c];
343
- }
327
+ if (lattice && xyz_frac_to_cart && xyz_axis_lengths) {
328
+ abc = xyz_cart_to_frac
329
+ ? xyz_cart_to_frac(xyz)
330
+ : approximate_cart_to_frac(xyz, xyz_axis_lengths);
344
331
  // Ensure fractional coordinates are wrapped into [0, 1) for consistency
345
- abc = [
346
- abc[0] - Math.floor(abc[0]),
347
- abc[1] - Math.floor(abc[1]),
348
- abc[2] - Math.floor(abc[2]),
349
- ];
332
+ abc = wrap_to_unit_cell(abc);
350
333
  // Keep rendered atoms inside primary unit cell by recomputing xyz
351
- // from the wrapped fractional coordinates using transpose(lattice)
352
- const lattice_transposed = math.transpose_3x3_matrix(lattice.matrix);
353
- const wrapped_xyz = math.mat3x3_vec3_multiply(lattice_transposed, abc);
334
+ const wrapped_xyz = xyz_frac_to_cart(abc);
354
335
  xyz[0] = wrapped_xyz[0];
355
336
  xyz[1] = wrapped_xyz[1];
356
337
  xyz[2] = wrapped_xyz[2];
@@ -395,7 +376,7 @@ const parse_symmetry_expression = (expr_input) => {
395
376
  tokens.push(current_token);
396
377
  for (const token of tokens) {
397
378
  // Check if this token is a variable term (x, y, or z with optional sign)
398
- const var_match = token.match(/^([+-]?)([xyz])$/);
379
+ const var_match = /^([+-]?)([xyz])$/.exec(token);
399
380
  if (var_match) {
400
381
  const sign = var_match[1] === `-` ? -1 : 1;
401
382
  const var_char = var_match[2];
@@ -444,9 +425,7 @@ const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords) => {
444
425
  return [atom];
445
426
  const equivalent_atoms = [];
446
427
  const seen = new Set();
447
- const wrap = (coords) => (wrap_fractional_coords
448
- ? coords.map((val) => val - Math.floor(val))
449
- : coords);
428
+ const wrap = (coords) => wrap_fractional_coords ? wrap_to_unit_cell(coords) : coords;
450
429
  // Use 6 decimal places for deduplication to handle floating point imprecision
451
430
  // from compound symmetry operations like x-y, -x+y which can produce small errors
452
431
  const key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
@@ -455,7 +434,7 @@ const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords) => {
455
434
  seen.add(key(base_coords));
456
435
  equivalent_atoms.push({ ...atom, coords: base_coords });
457
436
  for (const operation of symmetry_ops) {
458
- const operation_match = operation.match(/['"]([^'"]+)['"]/);
437
+ const operation_match = /['"]([^'"]+)['"]/.exec(operation);
459
438
  const expr_str = operation_match ? operation_match[1] : operation.trim();
460
439
  const parts = expr_str.split(`,`).map((part) => part.trim());
461
440
  if (parts.length !== 3)
@@ -464,17 +443,18 @@ const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords) => {
464
443
  for (let dim = 0; dim < 3; dim++) {
465
444
  const { coefficients, translation } = parse_symmetry_expression(parts[dim]);
466
445
  // Apply: new_coord = coeff_x * x + coeff_y * y + coeff_z * z + translation
467
- new_coords[dim] = coefficients[0] * atom.coords[0] +
468
- coefficients[1] * atom.coords[1] +
469
- coefficients[2] * atom.coords[2] +
470
- translation;
446
+ new_coords[dim] =
447
+ coefficients[0] * atom.coords[0] +
448
+ coefficients[1] * atom.coords[1] +
449
+ coefficients[2] * atom.coords[2] +
450
+ translation;
471
451
  }
472
452
  // Wrap and deduplicate transformed coordinates
473
453
  const wrapped = wrap(new_coords);
474
- const k = key(wrapped);
475
- if (seen.has(k))
454
+ const cache_key = key(wrapped);
455
+ if (seen.has(cache_key))
476
456
  continue;
477
- seen.add(k);
457
+ seen.add(cache_key);
478
458
  equivalent_atoms.push({
479
459
  ...atom,
480
460
  coords: wrapped,
@@ -548,7 +528,7 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
548
528
  const occu = occupancy >= 0 && raw_data[occupancy]
549
529
  ? parseFloat(raw_data[occupancy].split(`(`)[0]) || 1.0
550
530
  : 1.0;
551
- const element_symbol = (symbol >= 0 && raw_data[symbol]?.match(/^([A-Z][a-z]*)/)?.[1]) ||
531
+ const element_symbol = (symbol >= 0 && /^([A-Z][a-z]*)/.exec(raw_data[symbol])?.[1]) ||
552
532
  raw_data[label]?.match(/([A-Z][a-z]*)/g)?.[0] ||
553
533
  (() => {
554
534
  throw new Error(`Could not extract element symbol from: ${raw_data.join(` `)}`);
@@ -604,9 +584,11 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
604
584
  continue;
605
585
  // Check if this loop contains coordinate headers
606
586
  const indices_preview = build_cif_atom_site_header_indices(headers);
607
- const has_coords = (indices_preview.x !== undefined && indices_preview.y !== undefined &&
587
+ const has_coords = (indices_preview.x !== undefined &&
588
+ indices_preview.y !== undefined &&
608
589
  indices_preview.z !== undefined) ||
609
- (indices_preview.cart_x !== undefined && indices_preview.cart_y !== undefined &&
590
+ (indices_preview.cart_x !== undefined &&
591
+ indices_preview.cart_y !== undefined &&
610
592
  indices_preview.cart_z !== undefined);
611
593
  if (!has_coords) {
612
594
  ii = jj - 1;
@@ -644,10 +626,12 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
644
626
  // Parse atom data with error handling
645
627
  const header_indices = build_cif_atom_site_header_indices(atom_headers);
646
628
  // Determine available coordinate type
647
- const coords_type = header_indices.x !== undefined && header_indices.y !== undefined &&
629
+ const coords_type = header_indices.x !== undefined &&
630
+ header_indices.y !== undefined &&
648
631
  header_indices.z !== undefined
649
632
  ? `fract`
650
- : header_indices.cart_x !== undefined && header_indices.cart_y !== undefined &&
633
+ : header_indices.cart_x !== undefined &&
634
+ header_indices.cart_y !== undefined &&
651
635
  header_indices.cart_z !== undefined
652
636
  ? `cart`
653
637
  : null;
@@ -698,21 +682,15 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
698
682
  const [alpha, beta, gamma] = angles;
699
683
  const lattice_matrix = math.cell_to_lattice_matrix(a, b, c, alpha, beta, gamma);
700
684
  const lattice_params = math.calc_lattice_params(lattice_matrix);
701
- const lattice_transposed = math.transpose_3x3_matrix(lattice_matrix);
702
- let lattice_inv_transposed = null;
703
- try {
704
- lattice_inv_transposed = math.matrix_inverse_3x3(lattice_transposed);
705
- }
706
- catch {
707
- lattice_inv_transposed = null;
708
- }
685
+ const frac_to_cart = math.create_frac_to_cart(lattice_matrix);
686
+ const cart_to_frac = try_create_cart_to_frac(lattice_matrix);
709
687
  // Create sites with coordinate conversion and symmetry operations
710
- const wrap_vec3 = (v) => wrap_fractional_coords ? v.map((coord) => coord - Math.floor(coord)) : v;
688
+ const wrap_vec3 = (v) => (wrap_fractional_coords ? wrap_to_unit_cell(v) : v);
711
689
  // Apply symmetry operations to generate all equivalent positions
712
690
  const all_sites = [];
713
691
  // Normalize symmetry operations (trim/strip quotes) but preserve duplicates; we deduplicate positions later
714
692
  const normalized_ops = symmetry_ops
715
- .map((op) => op.match(/['\"]([^'\"]+)['\"]/)?.[1] || op.trim())
693
+ .map((op) => /['"]([^'"]+)['"]/.exec(op)?.[1] || op.trim())
716
694
  .map((op) => op.replace(/\s+/g, ``));
717
695
  // Rely on symmetry operations list for all centering/translations to avoid double-counting
718
696
  // TODO: Support conventional cells with centering by discovering centering from space group metadata
@@ -746,7 +724,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
746
724
  const toks = (line.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).map((tok) => tok.replace(/['"]/g, ``));
747
725
  if (toks.length > Math.max(sym_idx, num_idx)) {
748
726
  // Normalize type symbol to bare element (e.g. 'Sn2+' -> 'Sn')
749
- const match = toks[sym_idx]?.match(/^([A-Z][a-z]*)/);
727
+ const match = /^([A-Z][a-z]*)/.exec(toks[sym_idx]);
750
728
  const sym = match ? match[1] : toks[sym_idx];
751
729
  const num = parseInt(toks[num_idx]);
752
730
  if (sym && !Number.isNaN(num))
@@ -784,13 +762,9 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
784
762
  }
785
763
  else {
786
764
  const xyz_base = [atom.coords[0], atom.coords[1], atom.coords[2]];
787
- let atom_abc;
788
- if (lattice_inv_transposed) {
789
- const raw = math.mat3x3_vec3_multiply(lattice_inv_transposed, xyz_base);
790
- atom_abc = wrap_vec3(raw);
791
- }
792
- else
793
- atom_abc = wrap_vec3([xyz_base[0] / a, xyz_base[1] / b, xyz_base[2] / c]);
765
+ const atom_abc = wrap_vec3(cart_to_frac
766
+ ? cart_to_frac(xyz_base)
767
+ : approximate_cart_to_frac(xyz_base, [a, b, c]));
794
768
  fractional_atom = { ...atom, coords: atom_abc, coords_type: `fract` };
795
769
  }
796
770
  // First apply symmetry operations in fractional space
@@ -807,7 +781,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
807
781
  if (seen_site_keys.has(key))
808
782
  continue;
809
783
  seen_site_keys.add(key);
810
- const xyz = math.mat3x3_vec3_multiply(lattice_transposed, abc);
784
+ const xyz = frac_to_cart(abc);
811
785
  all_sites.push({
812
786
  species: [{ element, occu: equiv_atom.occupancy, oxidation_state: 0 }],
813
787
  abc,
@@ -832,15 +806,11 @@ function convert_phonopy_cell(cell) {
832
806
  // Phonopy stores lattice vectors as rows, use them directly
833
807
  const lattice_matrix = cell.lattice;
834
808
  // Process each atomic site
809
+ const phonopy_frac_to_cart = math.create_frac_to_cart(lattice_matrix);
835
810
  for (const point of cell.points) {
836
811
  const element = validate_element_symbol(point.symbol, sites.length);
837
- const abc = [
838
- point.coordinates[0],
839
- point.coordinates[1],
840
- point.coordinates[2],
841
- ];
842
- // Convert fractional to Cartesian coordinates
843
- const xyz = math.mat3x3_vec3_multiply(math.transpose_3x3_matrix(lattice_matrix), abc);
812
+ const abc = [point.coordinates[0], point.coordinates[1], point.coordinates[2]];
813
+ const xyz = phonopy_frac_to_cart(abc);
844
814
  const properties = {
845
815
  mass: point.mass,
846
816
  ...(point.reduced_to !== undefined && { reduced_to: point.reduced_to }),
@@ -868,7 +838,7 @@ export function parse_phonopy_yaml(content, cell_type) {
868
838
  }
869
839
  // Check if we're still in the phonon_displacements section
870
840
  if (skip_displacements) {
871
- if (line.match(/^[a-zA-Z_]/)) {
841
+ if (/^[a-zA-Z_]/.exec(line)) {
872
842
  // New top-level key, stop skipping
873
843
  skip_displacements = false;
874
844
  }
@@ -921,7 +891,7 @@ export function parse_phonopy_yaml(content, cell_type) {
921
891
  // Recursively search for a valid structure object in nested JSON
922
892
  function find_structure_in_json(obj, visited = new WeakSet()) {
923
893
  // Check if current object is null or undefined
924
- if (obj === null || obj === undefined)
894
+ if (obj == null)
925
895
  return null;
926
896
  if (typeof obj !== `object`)
927
897
  return null; // If it's not an object, skip it
@@ -1073,8 +1043,7 @@ export function parse_structure_file(content, filename) {
1073
1043
  const coords = parts.slice(1, 4);
1074
1044
  // Check if first token looks like an element symbol (not a number)
1075
1045
  // and the next 3 tokens look like coordinates (numbers)
1076
- const is_element_symbol = isNaN(parseInt(first_token)) &&
1077
- first_token.length <= 3;
1046
+ const is_element_symbol = isNaN(parseInt(first_token)) && first_token.length <= 3;
1078
1047
  const are_coordinates = coords.every((coord) => !isNaN(parseFloat(coord)));
1079
1048
  if (is_element_symbol && are_coordinates) {
1080
1049
  // First token is likely an element symbol, likely XYZ
@@ -1188,7 +1157,24 @@ export function parse_optimade_from_raw(raw) {
1188
1157
  const species = species_raw;
1189
1158
  // Optimade stores lattice vectors as rows, so use as is
1190
1159
  const lattice_matrix = attrs.lattice_vectors;
1160
+ const optimade_lattice_params = lattice_matrix
1161
+ ? math.calc_lattice_params(lattice_matrix)
1162
+ : null;
1191
1163
  // Parse atomic sites
1164
+ const optimade_exact_cart_to_frac = lattice_matrix
1165
+ ? try_create_cart_to_frac(lattice_matrix)
1166
+ : null;
1167
+ const optimade_cart_to_frac = lattice_matrix && optimade_lattice_params
1168
+ ? (optimade_exact_cart_to_frac ??
1169
+ ((xyz) => approximate_cart_to_frac(xyz, [
1170
+ optimade_lattice_params.a,
1171
+ optimade_lattice_params.b,
1172
+ optimade_lattice_params.c,
1173
+ ])))
1174
+ : null;
1175
+ if (lattice_matrix && !optimade_exact_cart_to_frac) {
1176
+ console.warn(`Failed to create exact coordinate converter for OPTIMADE structure`);
1177
+ }
1192
1178
  const sites = [];
1193
1179
  for (let idx = 0; idx < positions.length; idx++) {
1194
1180
  const pos = positions[idx];
@@ -1200,18 +1186,7 @@ export function parse_optimade_from_raw(raw) {
1200
1186
  const element = validate_element_symbol(element_symbol, idx);
1201
1187
  const xyz = [pos[0], pos[1], pos[2]];
1202
1188
  // Calculate fractional coordinates if lattice is available
1203
- let abc = [0, 0, 0];
1204
- if (lattice_matrix) {
1205
- try {
1206
- const lattice_transposed = math.transpose_3x3_matrix(lattice_matrix);
1207
- const lattice_inv = math.matrix_inverse_3x3(lattice_transposed);
1208
- abc = math.mat3x3_vec3_multiply(lattice_inv, xyz);
1209
- }
1210
- catch {
1211
- // Fallback if matrix inversion fails
1212
- console.warn(`Failed to calculate fractional coordinates for OPTIMADE structure`);
1213
- }
1214
- }
1189
+ const abc = optimade_cart_to_frac ? optimade_cart_to_frac(xyz) : [0, 0, 0];
1215
1190
  const site = {
1216
1191
  species: [{ element, occu: 1, oxidation_state: 0 }],
1217
1192
  abc,
@@ -1227,9 +1202,8 @@ export function parse_optimade_from_raw(raw) {
1227
1202
  }
1228
1203
  // Create structure object
1229
1204
  let lattice;
1230
- if (lattice_matrix) {
1231
- const lattice_params = math.calc_lattice_params(lattice_matrix);
1232
- lattice = { matrix: lattice_matrix, ...lattice_params };
1205
+ if (lattice_matrix && optimade_lattice_params) {
1206
+ lattice = { matrix: lattice_matrix, ...optimade_lattice_params };
1233
1207
  }
1234
1208
  const structure_result = {
1235
1209
  sites,
@@ -1260,7 +1234,7 @@ function extract_optimade_structure_from_raw(raw) {
1260
1234
  const candidate = Array.isArray(payload) ? payload[0] : payload;
1261
1235
  return is_optimade_structure_object(candidate) ? candidate : null;
1262
1236
  }
1263
- const unwrap_data = (value) => (value && typeof value === `object` && `data` in value)
1237
+ const unwrap_data = (value) => value && typeof value === `object` && `data` in value
1264
1238
  ? value.data
1265
1239
  : value;
1266
1240
  // Type guard: verify minimal OPTIMADE structure shape
@@ -1271,8 +1245,10 @@ function is_optimade_structure_object(value) {
1271
1245
  const type = obj.type;
1272
1246
  const id = obj.id;
1273
1247
  const attributes = obj.attributes;
1274
- return type === `structures` && typeof id === `string` &&
1275
- typeof attributes === `object` && attributes !== null;
1248
+ return (type === `structures` &&
1249
+ typeof id === `string` &&
1250
+ typeof attributes === `object` &&
1251
+ attributes !== null);
1276
1252
  }
1277
1253
  // Convert OPTIMADE structure to Crystal format
1278
1254
  export function optimade_to_crystal(optimade_structure) {
@@ -1290,21 +1266,15 @@ export function optimade_to_crystal(optimade_structure) {
1290
1266
  const lattice_params = math.calc_lattice_params(lattice_matrix);
1291
1267
  // Build species lookup for site properties (mass, concentration, etc.)
1292
1268
  const species_map = new Map(species?.map((spec) => [spec.name, spec]));
1269
+ const crystal_cart_to_frac = try_create_cart_to_frac(lattice_matrix) ??
1270
+ ((xyz) => approximate_cart_to_frac(xyz, [lattice_params.a, lattice_params.b, lattice_params.c]));
1293
1271
  const sites = cartesian_site_positions.map((pos, idx) => {
1294
1272
  const element_symbol = species_at_sites[idx];
1295
1273
  if (!element_symbol)
1296
1274
  throw new Error(`Missing species for site ${idx}`);
1297
1275
  const element = validate_element_symbol(element_symbol, idx);
1298
1276
  const xyz = [pos[0], pos[1], pos[2]];
1299
- let abc;
1300
- try {
1301
- const lattice_transposed = math.transpose_3x3_matrix(lattice_matrix);
1302
- const inv_matrix = math.matrix_inverse_3x3(lattice_transposed);
1303
- abc = math.mat3x3_vec3_multiply(inv_matrix, xyz);
1304
- }
1305
- catch {
1306
- abc = [0, 0, 0];
1307
- }
1277
+ const abc = crystal_cart_to_frac ? crystal_cart_to_frac(xyz) : [0, 0, 0];
1308
1278
  // Extract mass/concentration from species data
1309
1279
  const spec = species_map.get(element_symbol);
1310
1280
  const site_props = {};
@@ -1351,8 +1321,10 @@ export function is_structure_file(filename) {
1351
1321
  if (/\.(yaml|yml|xml)$/i.test(name) && STRUCT_KEYWORDS_REGEX.test(name))
1352
1322
  return true;
1353
1323
  // More restrictive keyword detection for JSON files
1354
- if (/\.json$/i.test(name) && STRUCT_KEYWORDS_STRICT_REGEX.test(name) &&
1355
- !TRAJ_KEYWORDS_REGEX.test(name) && !CONFIG_DIRS_REGEX.test(name))
1324
+ if (/\.json$/i.test(name) &&
1325
+ STRUCT_KEYWORDS_STRICT_REGEX.test(name) &&
1326
+ !TRAJ_KEYWORDS_REGEX.test(name) &&
1327
+ !CONFIG_DIRS_REGEX.test(name))
1356
1328
  return true;
1357
1329
  // Compressed files - check base filename recursively
1358
1330
  if (COMPRESSION_EXTENSIONS_REGEX.test(name)) {
@@ -10,9 +10,7 @@ const make_render_site = (sites, site_idx, source_site_indices, site_override) =
10
10
  is_image_atom: source_site_indices.some((source_site_idx) => is_image_atom(sites[source_site_idx])),
11
11
  source_site_indices,
12
12
  });
13
- const sq_dist = (xyz_1, xyz_2) => (xyz_1[0] - xyz_2[0]) ** 2 +
14
- (xyz_1[1] - xyz_2[1]) ** 2 +
15
- (xyz_1[2] - xyz_2[2]) ** 2;
13
+ const sq_dist = (xyz_1, xyz_2) => (xyz_1[0] - xyz_2[0]) ** 2 + (xyz_1[1] - xyz_2[1]) ** 2 + (xyz_1[2] - xyz_2[2]) ** 2;
16
14
  const is_split_partial_site = (site, hidden_elements) => {
17
15
  const visible_species = site.species.filter(({ element }) => !hidden_elements.has(element));
18
16
  const total_visible_occupancy = visible_species.reduce((occupancy_sum, { occu }) => occupancy_sum + occu, 0);
@@ -48,7 +46,10 @@ const build_render_sites = (sites, non_grouped_site_indices, grouped_site_indice
48
46
  const representative_site_idx = grouped_indices[0];
49
47
  const representative_site = sites[representative_site_idx];
50
48
  const merged_species = grouped_indices.flatMap((grouped_site_idx) => sites[grouped_site_idx].species);
51
- render_sites.push(make_render_site(sites, representative_site_idx, [...grouped_indices], { ...representative_site, species: merged_species }));
49
+ render_sites.push(make_render_site(sites, representative_site_idx, [...grouped_indices], {
50
+ ...representative_site,
51
+ species: merged_species,
52
+ }));
52
53
  }
53
54
  return render_sites;
54
55
  };
@@ -66,9 +67,7 @@ export const compute_slice_geometry = (visible_species, slice_gap_rad = PARTIAL_
66
67
  return [];
67
68
  const total_visible_occupancy = visible_species.reduce((occupancy_sum, { occu }) => occupancy_sum + occu, 0);
68
69
  // Preserve total angular coverage at one full turn for invalid overfull inputs.
69
- const occupancy_scale_factor = total_visible_occupancy > 1 + OCCUPANCY_EPS
70
- ? 1 / total_visible_occupancy
71
- : 1;
70
+ const occupancy_scale_factor = total_visible_occupancy > 1 + OCCUPANCY_EPS ? 1 / total_visible_occupancy : 1;
72
71
  const normalized_species = visible_species.map(({ element, occu }) => ({
73
72
  element,
74
73
  occu: occu * occupancy_scale_factor,
@@ -83,9 +82,7 @@ export const compute_slice_geometry = (visible_species, slice_gap_rad = PARTIAL_
83
82
  // Keep neighboring wedges from sharing the exact same plane (z-fighting).
84
83
  const phi_span_raw = Math.max(0, end_phi_raw - start_phi_raw);
85
84
  const max_safe_gap = Math.max(0, phi_span_raw - MIN_PHI_LENGTH);
86
- const desired_gap = visible_species.length > 1
87
- ? Math.min(slice_gap_rad, phi_span_raw * 0.25)
88
- : 0;
85
+ const desired_gap = visible_species.length > 1 ? Math.min(slice_gap_rad, phi_span_raw * 0.25) : 0;
89
86
  const phi_gap = Math.min(desired_gap, max_safe_gap);
90
87
  const start_phi = start_phi_raw + phi_gap / 2;
91
88
  const end_phi = end_phi_raw - phi_gap / 2;
@@ -1,6 +1,7 @@
1
1
  import type { Vec3 } from '../math';
2
2
  import type { ParsedStructure } from './parse';
3
3
  export type Pbc = readonly [boolean, boolean, boolean];
4
+ export declare const wrap_frac_coord: (coord: number) => number;
4
5
  export declare const wrap_to_unit_cell: (frac: Vec3) => Vec3;
5
6
  export declare function find_image_atoms(structure: ParsedStructure, { tolerance }?: {
6
7
  tolerance?: number;
@@ -1,9 +1,18 @@
1
1
  import * as math from '../math';
2
+ // Wrap a single fractional coordinate to [0, 1), clamping near-1 values to 0
3
+ // and rounding to 15 digits to suppress floating-point noise.
4
+ export const wrap_frac_coord = (coord) => {
5
+ const wrapped = coord - Math.floor(coord);
6
+ if (wrapped >= 1 - 1e-10)
7
+ return 0;
8
+ return Number(wrapped.toFixed(15));
9
+ };
2
10
  // Wrap fractional coordinates to [0, 1) range for periodicity.
3
- export const wrap_to_unit_cell = (frac) => frac.map((coord) => {
4
- const wrapped = ((coord % 1) + 1) % 1;
5
- return wrapped >= 1 - 1e-10 ? 0 : wrapped; // clamp near-1 to 0 for float precision
6
- });
11
+ export const wrap_to_unit_cell = (frac) => [
12
+ wrap_frac_coord(frac[0]),
13
+ wrap_frac_coord(frac[1]),
14
+ wrap_frac_coord(frac[2]),
15
+ ];
7
16
  export function find_image_atoms(structure, { tolerance } = {}) {
8
17
  // Find image atoms for PBC. Returns [atom_idx, image_xyz, image_abc] tuples.
9
18
  // Skips image generation for trajectory data with scattered atoms.
@@ -47,7 +56,7 @@ export function find_image_atoms(structure, { tolerance } = {}) {
47
56
  edge_dims.push({ dim, direction: -1 });
48
57
  }
49
58
  // Generate all translation combinations
50
- for (let mask = 1; mask < (1 << edge_dims.length); mask++) {
59
+ for (let mask = 1; mask < 1 << edge_dims.length; mask++) {
51
60
  // Track selected translation per dimension. If both +1 and -1 are selected for a dim,
52
61
  // the net shift is zero and we skip because it yields no image.
53
62
  const selected_shift = [0, 0, 0];
@@ -69,7 +78,8 @@ export function find_image_atoms(structure, { tolerance } = {}) {
69
78
  site.abc[2] + selected_shift[2],
70
79
  ];
71
80
  // If no dimension actually shifted, continue
72
- if (img_abc[0] === site.abc[0] && img_abc[1] === site.abc[1] &&
81
+ if (img_abc[0] === site.abc[0] &&
82
+ img_abc[1] === site.abc[1] &&
73
83
  img_abc[2] === site.abc[2])
74
84
  continue;
75
85
  // Compute xyz from img_abc to ensure consistency
@@ -1,8 +1,8 @@
1
1
  import type { Vec3 } from '../math';
2
- import * as math from '../math';
2
+ import { scale_lattice_matrix } from '../math';
3
3
  import type { Crystal } from './index';
4
4
  export declare function parse_supercell_scaling(scaling: string | number | Vec3): Vec3;
5
5
  export declare function generate_lattice_points(scaling_factors: Vec3): Vec3[];
6
- export declare function scale_lattice_matrix(orig_matrix: math.Matrix3x3, scaling_factors: Vec3): math.Matrix3x3;
6
+ export { scale_lattice_matrix };
7
7
  export declare function make_supercell(structure: Crystal, scaling: string | number | Vec3, to_unit_cell?: boolean): Crystal;
8
8
  export declare function is_valid_supercell_input(input: string): boolean;