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.
- package/dist/BohrAtom.svelte +105 -0
- package/dist/BohrAtom.svelte.d.ts +21 -0
- package/dist/ControlPanel.svelte +158 -0
- package/dist/ControlPanel.svelte.d.ts +18 -0
- package/dist/Icon.svelte +23 -0
- package/dist/Icon.svelte.d.ts +8 -0
- package/dist/InfoCard.svelte +79 -0
- package/dist/InfoCard.svelte.d.ts +23 -0
- package/dist/Nucleus.svelte +64 -0
- package/dist/Nucleus.svelte.d.ts +16 -0
- package/dist/Spinner.svelte +44 -0
- package/dist/Spinner.svelte.d.ts +7 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +30 -0
- package/dist/colors/alloy-colors.json +111 -0
- package/dist/colors/dark-mode-colors.json +111 -0
- package/dist/colors/index.d.ts +26 -0
- package/dist/colors/index.js +72 -0
- package/dist/colors/jmol-colors.json +111 -0
- package/dist/colors/muted-colors.json +111 -0
- package/dist/colors/pastel-colors.json +111 -0
- package/dist/colors/vesta-colors.json +111 -0
- package/dist/composition/BarChart.svelte +260 -0
- package/dist/composition/BarChart.svelte.d.ts +33 -0
- package/dist/composition/BubbleChart.svelte +166 -0
- package/dist/composition/BubbleChart.svelte.d.ts +30 -0
- package/dist/composition/Composition.svelte +73 -0
- package/dist/composition/Composition.svelte.d.ts +27 -0
- package/dist/composition/PieChart.svelte +236 -0
- package/dist/composition/PieChart.svelte.d.ts +36 -0
- package/dist/composition/index.d.ts +5 -0
- package/dist/composition/index.js +5 -0
- package/dist/composition/parse.d.ts +14 -0
- package/dist/composition/parse.js +307 -0
- package/dist/element/ElementHeading.svelte +21 -0
- package/dist/element/ElementHeading.svelte.d.ts +8 -0
- package/dist/element/ElementPhoto.svelte +56 -0
- package/dist/element/ElementPhoto.svelte.d.ts +9 -0
- package/dist/element/ElementStats.svelte +73 -0
- package/dist/element/ElementStats.svelte.d.ts +8 -0
- package/dist/element/ElementTile.svelte +449 -0
- package/dist/element/ElementTile.svelte.d.ts +25 -0
- package/dist/element/data.d.ts +4958 -0
- package/dist/element/data.js +5628 -0
- package/dist/element/index.d.ts +4 -0
- package/dist/element/index.js +4 -0
- package/dist/icons.d.ts +435 -0
- package/dist/icons.js +435 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +43 -0
- package/dist/io/decompress.d.ts +16 -0
- package/dist/io/decompress.js +78 -0
- package/dist/io/export.d.ts +9 -0
- package/dist/io/export.js +205 -0
- package/dist/io/parse.d.ts +53 -0
- package/dist/io/parse.js +747 -0
- package/dist/labels.d.ts +31 -0
- package/dist/labels.js +209 -0
- package/dist/material/MaterialCard.svelte +135 -0
- package/dist/material/MaterialCard.svelte.d.ts +10 -0
- package/dist/material/SymmetryCard.svelte +23 -0
- package/dist/material/SymmetryCard.svelte.d.ts +9 -0
- package/dist/material/index.d.ts +2 -0
- package/dist/material/index.js +2 -0
- package/dist/math.d.ts +24 -0
- package/dist/math.js +216 -0
- package/dist/periodic-table/PeriodicTable.svelte +284 -0
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +50 -0
- package/dist/periodic-table/PropertySelect.svelte +20 -0
- package/dist/periodic-table/PropertySelect.svelte.d.ts +13 -0
- package/dist/periodic-table/TableInset.svelte +18 -0
- package/dist/periodic-table/TableInset.svelte.d.ts +9 -0
- package/dist/periodic-table/index.d.ts +9 -0
- package/dist/periodic-table/index.js +3 -0
- package/dist/plot/ColorBar.svelte +414 -0
- package/dist/plot/ColorBar.svelte.d.ts +22 -0
- package/dist/plot/ColorScaleSelect.svelte +31 -0
- package/dist/plot/ColorScaleSelect.svelte.d.ts +15 -0
- package/dist/plot/ElementScatter.svelte +38 -0
- package/dist/plot/ElementScatter.svelte.d.ts +14 -0
- package/dist/plot/Line.svelte +42 -0
- package/dist/plot/Line.svelte.d.ts +15 -0
- package/dist/plot/PlotLegend.svelte +206 -0
- package/dist/plot/PlotLegend.svelte.d.ts +18 -0
- package/dist/plot/ScatterPlot.svelte +1753 -0
- package/dist/plot/ScatterPlot.svelte.d.ts +114 -0
- package/dist/plot/ScatterPlotControls.svelte +505 -0
- package/dist/plot/ScatterPlotControls.svelte.d.ts +33 -0
- package/dist/plot/ScatterPoint.svelte +72 -0
- package/dist/plot/ScatterPoint.svelte.d.ts +17 -0
- package/dist/plot/index.d.ts +168 -0
- package/dist/plot/index.js +46 -0
- package/dist/state.svelte.d.ts +12 -0
- package/dist/state.svelte.js +11 -0
- package/dist/structure/Bond.svelte +68 -0
- package/dist/structure/Bond.svelte.d.ts +13 -0
- package/dist/structure/Lattice.svelte +115 -0
- package/dist/structure/Lattice.svelte.d.ts +15 -0
- package/dist/structure/Structure.svelte +298 -0
- package/dist/structure/Structure.svelte.d.ts +28 -0
- package/dist/structure/StructureCard.svelte +26 -0
- package/dist/structure/StructureCard.svelte.d.ts +9 -0
- package/dist/structure/StructureControls.svelte +383 -0
- package/dist/structure/StructureControls.svelte.d.ts +23 -0
- package/dist/structure/StructureLegend.svelte +130 -0
- package/dist/structure/StructureLegend.svelte.d.ts +17 -0
- package/dist/structure/StructureScene.svelte +331 -0
- package/dist/structure/StructureScene.svelte.d.ts +47 -0
- package/dist/structure/bonding.d.ts +16 -0
- package/dist/structure/bonding.js +150 -0
- package/dist/structure/index.d.ts +98 -0
- package/dist/structure/index.js +114 -0
- package/dist/structure/pbc.d.ts +6 -0
- package/dist/structure/pbc.js +72 -0
- package/dist/trajectory/Sidebar.svelte +412 -0
- package/dist/trajectory/Sidebar.svelte.d.ts +14 -0
- package/dist/trajectory/Trajectory.svelte +1084 -0
- package/dist/trajectory/Trajectory.svelte.d.ts +49 -0
- package/dist/trajectory/TrajectoryError.svelte +120 -0
- package/dist/trajectory/TrajectoryError.svelte.d.ts +12 -0
- package/dist/trajectory/extract.d.ts +5 -0
- package/dist/trajectory/extract.js +157 -0
- package/dist/trajectory/index.d.ts +16 -0
- package/dist/trajectory/index.js +49 -0
- package/dist/trajectory/parse.d.ts +13 -0
- package/dist/trajectory/parse.js +1093 -0
- package/dist/trajectory/plotting.d.ts +12 -0
- package/dist/trajectory/plotting.js +148 -0
- package/license +21 -0
- package/package.json +131 -0
- package/readme.md +95 -0
package/dist/io/parse.js
ADDED
|
@@ -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
|
+
}
|