matterviz 0.1.0

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 (131) hide show
  1. package/dist/BohrAtom.svelte +105 -0
  2. package/dist/BohrAtom.svelte.d.ts +21 -0
  3. package/dist/ControlPanel.svelte +158 -0
  4. package/dist/ControlPanel.svelte.d.ts +18 -0
  5. package/dist/Icon.svelte +23 -0
  6. package/dist/Icon.svelte.d.ts +8 -0
  7. package/dist/InfoCard.svelte +79 -0
  8. package/dist/InfoCard.svelte.d.ts +23 -0
  9. package/dist/Nucleus.svelte +64 -0
  10. package/dist/Nucleus.svelte.d.ts +16 -0
  11. package/dist/Spinner.svelte +44 -0
  12. package/dist/Spinner.svelte.d.ts +7 -0
  13. package/dist/api.d.ts +6 -0
  14. package/dist/api.js +30 -0
  15. package/dist/colors/alloy-colors.json +111 -0
  16. package/dist/colors/dark-mode-colors.json +111 -0
  17. package/dist/colors/index.d.ts +26 -0
  18. package/dist/colors/index.js +72 -0
  19. package/dist/colors/jmol-colors.json +111 -0
  20. package/dist/colors/muted-colors.json +111 -0
  21. package/dist/colors/pastel-colors.json +111 -0
  22. package/dist/colors/vesta-colors.json +111 -0
  23. package/dist/composition/BarChart.svelte +260 -0
  24. package/dist/composition/BarChart.svelte.d.ts +33 -0
  25. package/dist/composition/BubbleChart.svelte +166 -0
  26. package/dist/composition/BubbleChart.svelte.d.ts +30 -0
  27. package/dist/composition/Composition.svelte +73 -0
  28. package/dist/composition/Composition.svelte.d.ts +27 -0
  29. package/dist/composition/PieChart.svelte +236 -0
  30. package/dist/composition/PieChart.svelte.d.ts +36 -0
  31. package/dist/composition/index.d.ts +5 -0
  32. package/dist/composition/index.js +5 -0
  33. package/dist/composition/parse.d.ts +14 -0
  34. package/dist/composition/parse.js +307 -0
  35. package/dist/element/ElementHeading.svelte +21 -0
  36. package/dist/element/ElementHeading.svelte.d.ts +8 -0
  37. package/dist/element/ElementPhoto.svelte +56 -0
  38. package/dist/element/ElementPhoto.svelte.d.ts +9 -0
  39. package/dist/element/ElementStats.svelte +73 -0
  40. package/dist/element/ElementStats.svelte.d.ts +8 -0
  41. package/dist/element/ElementTile.svelte +449 -0
  42. package/dist/element/ElementTile.svelte.d.ts +25 -0
  43. package/dist/element/data.d.ts +4958 -0
  44. package/dist/element/data.js +5628 -0
  45. package/dist/element/index.d.ts +4 -0
  46. package/dist/element/index.js +4 -0
  47. package/dist/icons.d.ts +435 -0
  48. package/dist/icons.js +435 -0
  49. package/dist/index.d.ts +82 -0
  50. package/dist/index.js +43 -0
  51. package/dist/io/decompress.d.ts +16 -0
  52. package/dist/io/decompress.js +78 -0
  53. package/dist/io/export.d.ts +9 -0
  54. package/dist/io/export.js +205 -0
  55. package/dist/io/parse.d.ts +53 -0
  56. package/dist/io/parse.js +747 -0
  57. package/dist/labels.d.ts +31 -0
  58. package/dist/labels.js +209 -0
  59. package/dist/material/MaterialCard.svelte +135 -0
  60. package/dist/material/MaterialCard.svelte.d.ts +10 -0
  61. package/dist/material/SymmetryCard.svelte +23 -0
  62. package/dist/material/SymmetryCard.svelte.d.ts +9 -0
  63. package/dist/material/index.d.ts +2 -0
  64. package/dist/material/index.js +2 -0
  65. package/dist/math.d.ts +24 -0
  66. package/dist/math.js +216 -0
  67. package/dist/periodic-table/PeriodicTable.svelte +284 -0
  68. package/dist/periodic-table/PeriodicTable.svelte.d.ts +50 -0
  69. package/dist/periodic-table/PropertySelect.svelte +20 -0
  70. package/dist/periodic-table/PropertySelect.svelte.d.ts +13 -0
  71. package/dist/periodic-table/TableInset.svelte +18 -0
  72. package/dist/periodic-table/TableInset.svelte.d.ts +9 -0
  73. package/dist/periodic-table/index.d.ts +9 -0
  74. package/dist/periodic-table/index.js +3 -0
  75. package/dist/plot/ColorBar.svelte +414 -0
  76. package/dist/plot/ColorBar.svelte.d.ts +22 -0
  77. package/dist/plot/ColorScaleSelect.svelte +31 -0
  78. package/dist/plot/ColorScaleSelect.svelte.d.ts +15 -0
  79. package/dist/plot/ElementScatter.svelte +38 -0
  80. package/dist/plot/ElementScatter.svelte.d.ts +14 -0
  81. package/dist/plot/Line.svelte +42 -0
  82. package/dist/plot/Line.svelte.d.ts +15 -0
  83. package/dist/plot/PlotLegend.svelte +206 -0
  84. package/dist/plot/PlotLegend.svelte.d.ts +18 -0
  85. package/dist/plot/ScatterPlot.svelte +1753 -0
  86. package/dist/plot/ScatterPlot.svelte.d.ts +114 -0
  87. package/dist/plot/ScatterPlotControls.svelte +505 -0
  88. package/dist/plot/ScatterPlotControls.svelte.d.ts +33 -0
  89. package/dist/plot/ScatterPoint.svelte +72 -0
  90. package/dist/plot/ScatterPoint.svelte.d.ts +17 -0
  91. package/dist/plot/index.d.ts +168 -0
  92. package/dist/plot/index.js +46 -0
  93. package/dist/state.svelte.d.ts +12 -0
  94. package/dist/state.svelte.js +11 -0
  95. package/dist/structure/Bond.svelte +68 -0
  96. package/dist/structure/Bond.svelte.d.ts +13 -0
  97. package/dist/structure/Lattice.svelte +115 -0
  98. package/dist/structure/Lattice.svelte.d.ts +15 -0
  99. package/dist/structure/Structure.svelte +298 -0
  100. package/dist/structure/Structure.svelte.d.ts +28 -0
  101. package/dist/structure/StructureCard.svelte +26 -0
  102. package/dist/structure/StructureCard.svelte.d.ts +9 -0
  103. package/dist/structure/StructureControls.svelte +383 -0
  104. package/dist/structure/StructureControls.svelte.d.ts +23 -0
  105. package/dist/structure/StructureLegend.svelte +130 -0
  106. package/dist/structure/StructureLegend.svelte.d.ts +17 -0
  107. package/dist/structure/StructureScene.svelte +331 -0
  108. package/dist/structure/StructureScene.svelte.d.ts +47 -0
  109. package/dist/structure/bonding.d.ts +16 -0
  110. package/dist/structure/bonding.js +150 -0
  111. package/dist/structure/index.d.ts +98 -0
  112. package/dist/structure/index.js +114 -0
  113. package/dist/structure/pbc.d.ts +6 -0
  114. package/dist/structure/pbc.js +72 -0
  115. package/dist/trajectory/Sidebar.svelte +412 -0
  116. package/dist/trajectory/Sidebar.svelte.d.ts +14 -0
  117. package/dist/trajectory/Trajectory.svelte +1084 -0
  118. package/dist/trajectory/Trajectory.svelte.d.ts +49 -0
  119. package/dist/trajectory/TrajectoryError.svelte +120 -0
  120. package/dist/trajectory/TrajectoryError.svelte.d.ts +12 -0
  121. package/dist/trajectory/extract.d.ts +5 -0
  122. package/dist/trajectory/extract.js +157 -0
  123. package/dist/trajectory/index.d.ts +16 -0
  124. package/dist/trajectory/index.js +49 -0
  125. package/dist/trajectory/parse.d.ts +13 -0
  126. package/dist/trajectory/parse.js +1093 -0
  127. package/dist/trajectory/plotting.d.ts +12 -0
  128. package/dist/trajectory/plotting.js +148 -0
  129. package/license +21 -0
  130. package/package.json +131 -0
  131. package/readme.md +95 -0
@@ -0,0 +1,747 @@
1
+ import { elem_symbols } from '..';
2
+ import * as math from '../math';
3
+ import { load as yaml_load } from 'js-yaml';
4
+ // Normalize scientific notation in coordinate strings
5
+ // Handles eEdD and *^ notation variants
6
+ function normalize_scientific_notation(str) {
7
+ return str
8
+ .toLowerCase()
9
+ .replace(/d/g, `e`) // Replace D/d with e
10
+ .replace(/\*\^/g, `e`); // Replace *^ with e
11
+ }
12
+ // Parse a coordinate value that might be in various scientific notation formats
13
+ function parse_coordinate(str) {
14
+ const normalized = normalize_scientific_notation(str.trim());
15
+ const value = parseFloat(normalized);
16
+ if (isNaN(value)) {
17
+ throw `Invalid coordinate value: ${str}`;
18
+ }
19
+ return value;
20
+ }
21
+ // Parse coordinates from a line, handling malformed formatting
22
+ function parse_coordinate_line(line) {
23
+ let tokens = line.trim().split(/\s+/);
24
+ // Handle malformed coordinates like "1.0-2.0-3.0" (missing spaces)
25
+ if (tokens.length < 3) {
26
+ const new_tokens = [];
27
+ for (const token of tokens) {
28
+ // Split on minus signs that aren't at the start or after 'e'/'E'
29
+ const parts = token
30
+ .split(/(?<!^|[eE])-/)
31
+ .filter((part) => part.length > 0);
32
+ if (parts.length > 1) {
33
+ new_tokens.push(parts[0]);
34
+ for (let part_idx = 1; part_idx < parts.length; part_idx++) {
35
+ new_tokens.push(`-` + parts[part_idx]);
36
+ }
37
+ }
38
+ else {
39
+ new_tokens.push(token);
40
+ }
41
+ }
42
+ tokens = new_tokens;
43
+ }
44
+ if (tokens.length < 3) {
45
+ throw `Insufficient coordinates in line: ${line}`;
46
+ }
47
+ return tokens.slice(0, 3).map(parse_coordinate);
48
+ }
49
+ // Validate element symbol and provide fallback
50
+ function validate_element_symbol(symbol, index) {
51
+ // Clean symbol (remove suffixes like _pv, /hash)
52
+ const clean_symbol = symbol.split(/[_/]/)[0];
53
+ if (elem_symbols.includes(clean_symbol)) {
54
+ return clean_symbol;
55
+ }
56
+ // Fallback to default elements by atomic number
57
+ const fallback_elements = [
58
+ `H`,
59
+ `He`,
60
+ `Li`,
61
+ `Be`,
62
+ `B`,
63
+ `C`,
64
+ `N`,
65
+ `O`,
66
+ `F`,
67
+ `Ne`,
68
+ ];
69
+ const fallback = fallback_elements[index % fallback_elements.length];
70
+ console.warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
71
+ return fallback;
72
+ }
73
+ // Parse VASP POSCAR file format
74
+ export function parse_poscar(content) {
75
+ try {
76
+ const lines = content.replace(/^\s+/, ``).split(/\r?\n/);
77
+ if (lines.length < 8) {
78
+ console.error(`POSCAR file too short`);
79
+ return null;
80
+ }
81
+ // Parse scaling factor (line 2)
82
+ let scale_factor = parseFloat(lines[1]);
83
+ if (isNaN(scale_factor)) {
84
+ console.error(`Invalid scaling factor in POSCAR`);
85
+ return null;
86
+ }
87
+ // Parse lattice vectors (lines 3-5)
88
+ const parse_vector = (line, line_num) => {
89
+ const coords = line.trim().split(/\s+/).map(parse_coordinate);
90
+ if (coords.length !== 3) {
91
+ throw `Invalid lattice vector on line ${line_num}: expected 3 coordinates, got ${coords.length}`;
92
+ }
93
+ return coords;
94
+ };
95
+ const lattice_vecs = [
96
+ parse_vector(lines[2], 3),
97
+ parse_vector(lines[3], 4),
98
+ parse_vector(lines[4], 5),
99
+ ];
100
+ // Handle negative scale factor (volume-based scaling)
101
+ if (scale_factor < 0) {
102
+ const volume = Math.abs(lattice_vecs[0][0] *
103
+ (lattice_vecs[1][1] * lattice_vecs[2][2] -
104
+ lattice_vecs[1][2] * lattice_vecs[2][1]) +
105
+ lattice_vecs[0][1] *
106
+ (lattice_vecs[1][2] * lattice_vecs[2][0] -
107
+ lattice_vecs[1][0] * lattice_vecs[2][2]) +
108
+ lattice_vecs[0][2] *
109
+ (lattice_vecs[1][0] * lattice_vecs[2][1] -
110
+ lattice_vecs[1][1] * lattice_vecs[2][0]));
111
+ scale_factor = Math.pow(-scale_factor / volume, 1 / 3);
112
+ }
113
+ // Scale lattice vectors
114
+ const scaled_lattice = [
115
+ lattice_vecs[0].map((x) => x * scale_factor),
116
+ lattice_vecs[1].map((x) => x * scale_factor),
117
+ lattice_vecs[2].map((x) => x * scale_factor),
118
+ ];
119
+ // Parse element symbols and atom counts (may span multiple lines)
120
+ let line_index = 5;
121
+ let element_symbols = [];
122
+ let atom_counts = [];
123
+ // Detect if this is VASP 5+ format (has element symbols)
124
+ // Try to parse the first token as a number - if it succeeds, it's VASP 4 format
125
+ const first_token = lines[line_index].trim().split(/\s+/)[0];
126
+ const first_token_as_number = parseInt(first_token);
127
+ const has_element_symbols = isNaN(first_token_as_number);
128
+ if (has_element_symbols) {
129
+ // VASP 5+ format - parse element symbols (may span multiple lines)
130
+ let symbol_lines = 1;
131
+ // Look ahead to find where numbers start (atom counts)
132
+ for (let lookahead_idx = 1; lookahead_idx < 10; lookahead_idx++) {
133
+ if (line_index + lookahead_idx >= lines.length)
134
+ break;
135
+ const next_line_first_token = lines[line_index + lookahead_idx]
136
+ .trim()
137
+ .split(/\s+/)[0];
138
+ const next_token_as_number = parseInt(next_line_first_token);
139
+ if (!isNaN(next_token_as_number)) {
140
+ symbol_lines = lookahead_idx;
141
+ break;
142
+ }
143
+ }
144
+ // Collect all element symbols from the symbol lines
145
+ for (let symbol_line_idx = 0; symbol_line_idx < symbol_lines; symbol_line_idx++) {
146
+ if (line_index + symbol_line_idx < lines.length) {
147
+ element_symbols.push(...lines[line_index + symbol_line_idx].trim().split(/\s+/));
148
+ }
149
+ }
150
+ // Parse atom counts (may span multiple lines)
151
+ for (let count_line_idx = 0; count_line_idx < symbol_lines; count_line_idx++) {
152
+ if (line_index + symbol_lines + count_line_idx < lines.length) {
153
+ const counts = lines[line_index + symbol_lines + count_line_idx]
154
+ .trim()
155
+ .split(/\s+/)
156
+ .map(Number);
157
+ atom_counts.push(...counts);
158
+ }
159
+ }
160
+ line_index += 2 * symbol_lines;
161
+ }
162
+ else {
163
+ // VASP 4 format - only atom counts, generate default element symbols
164
+ atom_counts = lines[line_index].trim().split(/\s+/).map(Number);
165
+ element_symbols = atom_counts.map((_, i) => validate_element_symbol(`Element${i}`, i));
166
+ line_index += 1;
167
+ }
168
+ if (element_symbols.length !== atom_counts.length) {
169
+ console.error(`Mismatch between element symbols and atom counts`);
170
+ return null;
171
+ }
172
+ // Check for selective dynamics
173
+ let has_selective_dynamics = false;
174
+ if (line_index < lines.length) {
175
+ let coordinate_mode = lines[line_index].trim().toUpperCase();
176
+ if (coordinate_mode.startsWith(`S`)) {
177
+ has_selective_dynamics = true;
178
+ line_index += 1;
179
+ if (line_index < lines.length) {
180
+ coordinate_mode = lines[line_index].trim().toUpperCase();
181
+ }
182
+ else {
183
+ console.error(`Missing coordinate mode after selective dynamics`);
184
+ return null;
185
+ }
186
+ }
187
+ // Determine coordinate mode
188
+ const is_direct = coordinate_mode.startsWith(`D`);
189
+ const is_cartesian = coordinate_mode.startsWith(`C`) ||
190
+ coordinate_mode.startsWith(`K`);
191
+ if (!is_direct && !is_cartesian) {
192
+ console.error(`Unknown coordinate mode in POSCAR: ${coordinate_mode}`);
193
+ return null;
194
+ }
195
+ // Parse atomic positions
196
+ const sites = [];
197
+ let atom_index = 0;
198
+ for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
199
+ const element = validate_element_symbol(element_symbols[elem_idx], elem_idx);
200
+ const count = atom_counts[elem_idx];
201
+ for (let atom_count_idx = 0; atom_count_idx < count; atom_count_idx++) {
202
+ const coord_line_idx = line_index + 1 + atom_index + atom_count_idx;
203
+ if (coord_line_idx >= lines.length) {
204
+ console.error(`Not enough coordinate lines in POSCAR`);
205
+ return null;
206
+ }
207
+ const coords = parse_coordinate_line(lines[coord_line_idx]);
208
+ // Parse selective dynamics if present
209
+ let selective_dynamics;
210
+ if (has_selective_dynamics) {
211
+ const tokens = lines[coord_line_idx].trim().split(/\s+/);
212
+ if (tokens.length >= 6) {
213
+ selective_dynamics = [
214
+ tokens[3] === `T`,
215
+ tokens[4] === `T`,
216
+ tokens[5] === `T`,
217
+ ];
218
+ }
219
+ }
220
+ let xyz;
221
+ let abc;
222
+ if (is_direct) {
223
+ // Store fractional coordinates
224
+ abc = [coords[0], coords[1], coords[2]];
225
+ // Convert fractional to Cartesian coordinates
226
+ xyz = [
227
+ coords[0] * scaled_lattice[0][0] +
228
+ coords[1] * scaled_lattice[1][0] +
229
+ coords[2] * scaled_lattice[2][0],
230
+ coords[0] * scaled_lattice[0][1] +
231
+ coords[1] * scaled_lattice[1][1] +
232
+ coords[2] * scaled_lattice[2][1],
233
+ coords[0] * scaled_lattice[0][2] +
234
+ coords[1] * scaled_lattice[1][2] +
235
+ coords[2] * scaled_lattice[2][2],
236
+ ];
237
+ }
238
+ else {
239
+ // Already Cartesian, scale if needed
240
+ xyz = [
241
+ coords[0] * scale_factor,
242
+ coords[1] * scale_factor,
243
+ coords[2] * scale_factor,
244
+ ];
245
+ // Calculate fractional coordinates using proper matrix inversion
246
+ // Note: Our lattice matrix is stored as row vectors, but for coordinate conversion
247
+ // we need column vectors, so we transpose before inversion
248
+ try {
249
+ const lattice_transposed = math.transpose_matrix(scaled_lattice);
250
+ const lattice_inv = math.matrix_inverse_3x3(lattice_transposed);
251
+ abc = math.mat3x3_vec3_multiply(lattice_inv, xyz);
252
+ }
253
+ catch {
254
+ // Fallback to simplified method if matrix is singular
255
+ abc = [
256
+ xyz[0] / scaled_lattice[0][0],
257
+ xyz[1] / scaled_lattice[1][1],
258
+ xyz[2] / scaled_lattice[2][2],
259
+ ];
260
+ }
261
+ }
262
+ const site = {
263
+ species: [{ element, occu: 1, oxidation_state: 0 }],
264
+ abc,
265
+ xyz,
266
+ label: `${element}${atom_index + atom_count_idx + 1}`,
267
+ properties: selective_dynamics
268
+ ? { selective_dynamics: selective_dynamics }
269
+ : {},
270
+ };
271
+ sites.push(site);
272
+ }
273
+ atom_index += count;
274
+ }
275
+ const lattice_params = math.calc_lattice_params(scaled_lattice);
276
+ const structure = {
277
+ sites,
278
+ lattice: {
279
+ matrix: scaled_lattice,
280
+ ...lattice_params,
281
+ },
282
+ };
283
+ return structure;
284
+ }
285
+ else {
286
+ console.error(`Missing coordinate mode line in POSCAR`);
287
+ return null;
288
+ }
289
+ }
290
+ catch (error) {
291
+ console.error(`Error parsing POSCAR file:`, error);
292
+ return null;
293
+ }
294
+ }
295
+ // Parse XYZ file format. Supports both standard XYZ and extended XYZ formats with multi-frame support
296
+ export function parse_xyz(content) {
297
+ try {
298
+ const normalized_content = content.trim();
299
+ if (!normalized_content) {
300
+ console.error(`Empty XYZ file`);
301
+ return null;
302
+ }
303
+ // Split into frames by reading the atom count and slicing lines
304
+ const all_lines = normalized_content.split(/\r?\n/);
305
+ const frames = [];
306
+ let line_idx = 0;
307
+ while (line_idx < all_lines.length) {
308
+ const numAtoms = parseInt(all_lines[line_idx].trim(), 10);
309
+ if (!isNaN(numAtoms) &&
310
+ numAtoms > 0 &&
311
+ line_idx + numAtoms + 1 < all_lines.length) {
312
+ const frameLines = all_lines.slice(line_idx, line_idx + numAtoms + 2);
313
+ frames.push(frameLines.join(`\n`));
314
+ line_idx += numAtoms + 2;
315
+ }
316
+ else {
317
+ line_idx++;
318
+ }
319
+ }
320
+ // If no frames found, try simple parsing
321
+ if (frames.length === 0) {
322
+ frames.push(normalized_content);
323
+ }
324
+ // Parse the last frame (or only frame)
325
+ const frame_content = frames[frames.length - 1];
326
+ const lines = frame_content.trim().split(/\r?\n/);
327
+ if (lines.length < 2) {
328
+ console.error(`XYZ frame too short`);
329
+ return null;
330
+ }
331
+ // Parse number of atoms (line 1)
332
+ const num_atoms = parseInt(lines[0].trim());
333
+ if (isNaN(num_atoms) || num_atoms <= 0) {
334
+ console.error(`Invalid number of atoms in XYZ file`);
335
+ return null;
336
+ }
337
+ // Parse comment line (line 2) - may contain lattice info for extended XYZ
338
+ const comment_line = lines[1];
339
+ let lattice;
340
+ // Check for extended XYZ lattice information in comment line
341
+ const lattice_match = comment_line.match(/Lattice="([^"]+)"/);
342
+ if (lattice_match) {
343
+ const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
344
+ if (lattice_values.length === 9) {
345
+ const lattice_vectors = [
346
+ [lattice_values[0], lattice_values[1], lattice_values[2]],
347
+ [lattice_values[3], lattice_values[4], lattice_values[5]],
348
+ [lattice_values[6], lattice_values[7], lattice_values[8]],
349
+ ];
350
+ const lattice_params = math.calc_lattice_params(lattice_vectors);
351
+ lattice = {
352
+ matrix: lattice_vectors,
353
+ ...lattice_params,
354
+ };
355
+ }
356
+ }
357
+ // Parse atomic coordinates (starting from line 3)
358
+ const sites = [];
359
+ for (let atom_idx = 0; atom_idx < num_atoms; atom_idx++) {
360
+ const line_idx = atom_idx + 2;
361
+ if (line_idx >= lines.length) {
362
+ console.error(`Not enough coordinate lines in XYZ file`);
363
+ return null;
364
+ }
365
+ const parts = lines[line_idx].trim().split(/\s+/);
366
+ if (parts.length < 4) {
367
+ console.error(`Invalid coordinate line in XYZ file`);
368
+ return null;
369
+ }
370
+ const element = validate_element_symbol(parts[0], atom_idx);
371
+ const coords = [
372
+ parse_coordinate(parts[1]),
373
+ parse_coordinate(parts[2]),
374
+ parse_coordinate(parts[3]),
375
+ ];
376
+ // For XYZ files, coordinates are typically in Cartesian
377
+ const xyz = [coords[0], coords[1], coords[2]];
378
+ // Calculate fractional coordinates if lattice is available
379
+ let abc = [0, 0, 0];
380
+ if (lattice) {
381
+ // Calculate fractional coordinates using proper matrix inversion
382
+ // Note: Our lattice matrix is stored as row vectors, but for coordinate conversion
383
+ // we need column vectors, so we transpose before inversion
384
+ try {
385
+ const lattice_transposed = math.transpose_matrix(lattice.matrix);
386
+ const lattice_inv = math.matrix_inverse_3x3(lattice_transposed);
387
+ abc = math.mat3x3_vec3_multiply(lattice_inv, xyz);
388
+ }
389
+ catch {
390
+ // Fallback to simplified method if matrix is singular
391
+ abc = [xyz[0] / lattice.a, xyz[1] / lattice.b, xyz[2] / lattice.c];
392
+ }
393
+ }
394
+ const site = {
395
+ species: [{ element, occu: 1, oxidation_state: 0 }],
396
+ abc,
397
+ xyz,
398
+ label: `${element}${atom_idx + 1}`,
399
+ properties: {},
400
+ };
401
+ sites.push(site);
402
+ }
403
+ const structure = {
404
+ sites,
405
+ ...(lattice && { lattice }),
406
+ };
407
+ return structure;
408
+ }
409
+ catch (error) {
410
+ console.error(`Error parsing XYZ file:`, error);
411
+ return null;
412
+ }
413
+ }
414
+ // Parse CIF (Crystallographic Information File) format
415
+ export function parse_cif(content) {
416
+ try {
417
+ const lines = content.trim().split(/\r?\n/);
418
+ if (lines.length < 2) {
419
+ console.error(`CIF file too short`);
420
+ return null;
421
+ }
422
+ // Parse unit cell parameters
423
+ let [cell_a, cell_b, cell_c] = [1, 1, 1];
424
+ let [alpha, beta, gamma] = [90, 90, 90];
425
+ // Find unit cell parameters
426
+ for (const line of lines) {
427
+ const trimmed = line.trim();
428
+ if (trimmed.startsWith(`_cell_length_a`)) {
429
+ cell_a = parseFloat(trimmed.split(/\s+/)[1]);
430
+ }
431
+ else if (trimmed.startsWith(`_cell_length_b`)) {
432
+ cell_b = parseFloat(trimmed.split(/\s+/)[1]);
433
+ }
434
+ else if (trimmed.startsWith(`_cell_length_c`)) {
435
+ cell_c = parseFloat(trimmed.split(/\s+/)[1]);
436
+ }
437
+ else if (trimmed.startsWith(`_cell_angle_alpha`)) {
438
+ alpha = parseFloat(trimmed.split(/\s+/)[1]);
439
+ }
440
+ else if (trimmed.startsWith(`_cell_angle_beta`)) {
441
+ beta = parseFloat(trimmed.split(/\s+/)[1]);
442
+ }
443
+ else if (trimmed.startsWith(`_cell_angle_gamma`)) {
444
+ gamma = parseFloat(trimmed.split(/\s+/)[1]);
445
+ }
446
+ }
447
+ // Calculate lattice vectors from unit cell parameters using math utility
448
+ const lattice_matrix = math.cell_to_lattice_matrix(...[cell_a, cell_b, cell_c, alpha, beta, gamma]);
449
+ // Calculate lattice parameters (including volume)
450
+ const calculated_lattice_params = math.calc_lattice_params(lattice_matrix);
451
+ // Find atom site data
452
+ const sites = [];
453
+ let in_atom_site_loop = false;
454
+ let atom_site_headers = [];
455
+ let header_indices = {};
456
+ for (let line_idx = 0; line_idx < lines.length; line_idx++) {
457
+ const line = lines[line_idx].trim();
458
+ // Look for atom site loop
459
+ if (line === `loop_`) {
460
+ // Check if next few lines contain atom site labels
461
+ let next_line_idx = line_idx + 1;
462
+ const potential_headers = [];
463
+ while (next_line_idx < lines.length) {
464
+ const next_line = lines[next_line_idx].trim();
465
+ if (next_line.startsWith(`_atom_site_`)) {
466
+ potential_headers.push(next_line);
467
+ next_line_idx++;
468
+ }
469
+ else
470
+ break;
471
+ }
472
+ if (potential_headers.length > 0) {
473
+ in_atom_site_loop = true;
474
+ atom_site_headers = potential_headers;
475
+ // Build header-to-index mapping once
476
+ header_indices = {};
477
+ for (let header_idx = 0; header_idx < atom_site_headers.length; header_idx++) {
478
+ const header = atom_site_headers[header_idx];
479
+ if (header.includes(`_atom_site_label`)) {
480
+ header_indices.label = header_idx;
481
+ }
482
+ else if (header.includes(`_atom_site_type_symbol`)) {
483
+ header_indices.symbol = header_idx;
484
+ }
485
+ else if (header.includes(`_atom_site_fract_x`)) {
486
+ header_indices.x = header_idx;
487
+ }
488
+ else if (header.includes(`_atom_site_fract_y`)) {
489
+ header_indices.y = header_idx;
490
+ }
491
+ else if (header.includes(`_atom_site_fract_z`)) {
492
+ header_indices.z = header_idx;
493
+ }
494
+ else if (header.includes(`_atom_site_occupancy`)) {
495
+ header_indices.occupancy = header_idx;
496
+ }
497
+ }
498
+ line_idx = next_line_idx - 1; // Skip to data section
499
+ continue;
500
+ }
501
+ }
502
+ // Parse atom site data
503
+ if (in_atom_site_loop &&
504
+ line &&
505
+ !line.startsWith(`_`) &&
506
+ !line.startsWith(`#`)) {
507
+ const tokens = line.split(/\s+/);
508
+ if (tokens.length >= atom_site_headers.length) {
509
+ // Use precomputed header indices
510
+ const label_idx = header_indices.label >= 0 ? header_indices.label : -1;
511
+ const symbol_idx = header_indices.symbol >= 0 ? header_indices.symbol : -1;
512
+ const x_idx = header_indices.x >= 0 ? header_indices.x : -1;
513
+ const y_idx = header_indices.y >= 0 ? header_indices.y : -1;
514
+ const z_idx = header_indices.z >= 0 ? header_indices.z : -1;
515
+ const occ_idx = header_indices.occupancy >= 0 ? header_indices.occupancy : -1;
516
+ if (symbol_idx >= 0 && x_idx >= 0 && y_idx >= 0 && z_idx >= 0) {
517
+ try {
518
+ const element_symbol = tokens[symbol_idx];
519
+ const fract_x = parseFloat(tokens[x_idx]);
520
+ const fract_y = parseFloat(tokens[y_idx]);
521
+ const fract_z = parseFloat(tokens[z_idx]);
522
+ const occupancy = occ_idx >= 0 ? parseFloat(tokens[occ_idx]) : 1.0;
523
+ const label = label_idx >= 0 ? tokens[label_idx] : element_symbol;
524
+ if (isNaN(fract_x) || isNaN(fract_y) || isNaN(fract_z)) {
525
+ continue;
526
+ }
527
+ const element = validate_element_symbol(element_symbol, sites.length);
528
+ const abc = [fract_x, fract_y, fract_z];
529
+ // Convert fractional to Cartesian coordinates
530
+ const xyz = math.mat3x3_vec3_multiply(math.transpose_matrix(lattice_matrix), abc);
531
+ const site = {
532
+ species: [{ element, occu: occupancy, oxidation_state: 0 }],
533
+ abc,
534
+ xyz,
535
+ label,
536
+ properties: {},
537
+ };
538
+ sites.push(site);
539
+ }
540
+ catch (error) {
541
+ console.warn(`Error parsing CIF atom site line: ${line}`, error);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ // End of loop or start of new section
547
+ if (in_atom_site_loop &&
548
+ (line.startsWith(`loop_`) || line.startsWith(`data_`) || line === ``)) {
549
+ in_atom_site_loop = false;
550
+ }
551
+ }
552
+ if (sites.length === 0) {
553
+ console.error(`No atom sites found in CIF file`);
554
+ return null;
555
+ }
556
+ const structure = {
557
+ sites,
558
+ lattice: {
559
+ matrix: lattice_matrix,
560
+ ...calculated_lattice_params,
561
+ },
562
+ };
563
+ return structure;
564
+ }
565
+ catch (error) {
566
+ console.error(`Error parsing CIF file:`, error);
567
+ return null;
568
+ }
569
+ }
570
+ // Convert phonopy cell to ParsedStructure
571
+ function convert_phonopy_cell(cell) {
572
+ const sites = [];
573
+ // Phonopy stores lattice vectors as rows, use them directly
574
+ const lattice_matrix = [
575
+ [cell.lattice[0][0], cell.lattice[0][1], cell.lattice[0][2]],
576
+ [cell.lattice[1][0], cell.lattice[1][1], cell.lattice[1][2]],
577
+ [cell.lattice[2][0], cell.lattice[2][1], cell.lattice[2][2]],
578
+ ];
579
+ // Process each atomic site
580
+ for (const point of cell.points) {
581
+ const element = validate_element_symbol(point.symbol, sites.length);
582
+ const abc = [
583
+ point.coordinates[0],
584
+ point.coordinates[1],
585
+ point.coordinates[2],
586
+ ];
587
+ // Convert fractional to Cartesian coordinates
588
+ const xyz = math.mat3x3_vec3_multiply(math.transpose_matrix(lattice_matrix), abc);
589
+ const properties = {
590
+ mass: point.mass,
591
+ ...(point.reduced_to !== undefined && { reduced_to: point.reduced_to }),
592
+ };
593
+ const species = [{ element, occu: 1.0, oxidation_state: 0 }];
594
+ const site = { species, abc, xyz, label: point.symbol, properties };
595
+ sites.push(site);
596
+ }
597
+ // Calculate lattice parameters
598
+ const calculated_lattice_params = math.calc_lattice_params(lattice_matrix);
599
+ return { sites, lattice: { matrix: lattice_matrix, ...calculated_lattice_params } };
600
+ }
601
+ // Parse phonopy YAML file and return the requested cell type (or preferred single structure)
602
+ export function parse_phonopy_yaml(content, cell_type) {
603
+ try {
604
+ // Parse YAML content but exclude large phonon_displacements array for performance
605
+ const lines = content.split(`\n`);
606
+ const filtered_lines = [];
607
+ let skip_displacements = false;
608
+ for (const line of lines) {
609
+ // Skip phonon_displacements section for performance
610
+ if (line.trim().startsWith(`phonon_displacements:`)) {
611
+ skip_displacements = true;
612
+ continue;
613
+ }
614
+ // Check if we're still in the phonon_displacements section
615
+ if (skip_displacements) {
616
+ if (line.match(/^[a-zA-Z_]/)) {
617
+ // New top-level key, stop skipping
618
+ skip_displacements = false;
619
+ }
620
+ else
621
+ continue; // Still in phonon_displacements, skip this line
622
+ }
623
+ filtered_lines.push(line);
624
+ }
625
+ const filtered_content = filtered_lines.join(`\n`);
626
+ const data = yaml_load(filtered_content);
627
+ if (!data) {
628
+ console.error(`Failed to parse phonopy YAML`);
629
+ return null;
630
+ }
631
+ // If specific cell type requested, parse only that one
632
+ if (cell_type && cell_type !== `auto`) {
633
+ const cell = data[cell_type];
634
+ if (cell)
635
+ return convert_phonopy_cell(cell);
636
+ else {
637
+ console.error(`Requested cell type '${cell_type}' not found in phonopy YAML`);
638
+ return null;
639
+ }
640
+ }
641
+ // Auto mode: return preferred structure in order of preference
642
+ // 1. supercell (most detailed)
643
+ // 2. phonon_supercell
644
+ // 3. unit_cell
645
+ // 4. phonon_primitive_cell
646
+ // 5. primitive_cell
647
+ if (data.supercell)
648
+ return convert_phonopy_cell(data.supercell);
649
+ else if (data.phonon_supercell)
650
+ return convert_phonopy_cell(data.phonon_supercell);
651
+ else if (data.unit_cell)
652
+ return convert_phonopy_cell(data.unit_cell);
653
+ else if (data.phonon_primitive_cell) {
654
+ return convert_phonopy_cell(data.phonon_primitive_cell);
655
+ }
656
+ else if (data.primitive_cell)
657
+ return convert_phonopy_cell(data.primitive_cell);
658
+ console.error(`No valid cells found in phonopy YAML`);
659
+ return null;
660
+ }
661
+ catch (error) {
662
+ console.error(`Error parsing phonopy YAML:`, error);
663
+ return null;
664
+ }
665
+ }
666
+ // Auto-detect file format and parse accordingly
667
+ export function parse_structure_file(content, filename) {
668
+ // If a filename is provided, try to detect format by file extension first
669
+ if (filename) {
670
+ // Handle compressed files by removing .gz extension
671
+ let base_filename = filename.toLowerCase();
672
+ if (base_filename.endsWith(`.gz`))
673
+ base_filename = base_filename.slice(0, -3); // Remove .gz
674
+ const ext = base_filename.split(`.`).pop();
675
+ // Try to detect format by file extension
676
+ if (ext === `xyz`) {
677
+ return parse_xyz(content);
678
+ }
679
+ // CIF files
680
+ if (ext === `cif`) {
681
+ return parse_cif(content);
682
+ }
683
+ // YAML files (phonopy)
684
+ if (ext === `yaml` || ext === `yml`)
685
+ return parse_phonopy_yaml(content);
686
+ // POSCAR files may not have extensions or have various names
687
+ if (ext === `poscar` || base_filename.includes(`poscar`)) {
688
+ return parse_poscar(content);
689
+ }
690
+ }
691
+ // Try to auto-detect based on content
692
+ const lines = content.trim().split(/\r?\n/);
693
+ if (lines.length < 2) {
694
+ console.error(`File too short to determine format`);
695
+ return null;
696
+ }
697
+ // XYZ format detection: first line should be a number, second line is comment
698
+ const first_line_number = parseInt(lines[0].trim());
699
+ if (!isNaN(first_line_number) && first_line_number > 0) {
700
+ // Check if this looks like XYZ format
701
+ if (lines.length >= first_line_number + 2) {
702
+ // Try to parse a coordinate line to see if it looks like XYZ
703
+ const coord_line_idx = 2; // First coordinate line in XYZ
704
+ if (coord_line_idx < lines.length) {
705
+ const parts = lines[coord_line_idx].trim().split(/\s+/);
706
+ // XYZ format: element symbol followed by 3 coordinates
707
+ if (parts.length >= 4) {
708
+ const first_token = parts[0];
709
+ const coords = parts.slice(1, 4);
710
+ // Check if first token looks like an element symbol (not a number)
711
+ // and the next 3 tokens look like coordinates (numbers)
712
+ const is_element_symbol = isNaN(parseInt(first_token)) &&
713
+ first_token.length <= 3;
714
+ const are_coordinates = coords.every((coord) => !isNaN(parseFloat(coord)));
715
+ if (is_element_symbol && are_coordinates) {
716
+ // First token is likely an element symbol, likely XYZ
717
+ return parse_xyz(content);
718
+ }
719
+ }
720
+ }
721
+ }
722
+ }
723
+ // POSCAR format detection: look for typical structure
724
+ if (lines.length >= 8) {
725
+ const second_line_number = parseFloat(lines[1].trim());
726
+ // Second line is a number (scale factor), likely POSCAR
727
+ if (!isNaN(second_line_number))
728
+ return parse_poscar(content);
729
+ }
730
+ // CIF format detection: look for CIF-specific keywords
731
+ const has_cif_keywords = lines.some((line) => line.startsWith(`data_`) ||
732
+ line.includes(`_cell_length_`) ||
733
+ line.includes(`_atom_site_`) ||
734
+ line.trim() === `loop_`);
735
+ if (has_cif_keywords)
736
+ return parse_cif(content);
737
+ // YAML format detection: look for phonopy-specific keywords
738
+ const has_phonopy_keywords = lines.some((line) => line.includes(`phono3py:`) ||
739
+ line.includes(`phonopy:`) ||
740
+ line.includes(`primitive_cell:`) ||
741
+ line.includes(`supercell:`) ||
742
+ line.includes(`phonon_supercell:`));
743
+ if (has_phonopy_keywords)
744
+ return parse_phonopy_yaml(content);
745
+ console.error(`Unable to determine file format`);
746
+ return null;
747
+ }