quantized-lab 0.8.0__py3-none-any.whl
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.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Bruker/Siemens Diffrac-AT ``.raw`` binary XRD parser, version "RAW1.01".
|
|
2
|
+
|
|
3
|
+
Also known in ``xylib`` as "Bruker RAW ver. 3" (``bruker_raw.cpp``,
|
|
4
|
+
``load_version1_01``). Fixed-layout little-endian binary produced by Siemens/
|
|
5
|
+
Bruker D-series diffractometers (Diffrac-AT / DIFFRAC^plus).
|
|
6
|
+
|
|
7
|
+
Byte layout (offsets 0-indexed, little-endian). Cross-checked against xylib's
|
|
8
|
+
ASCII ``.UXD`` export of the same raw file::
|
|
9
|
+
|
|
10
|
+
File header (712 bytes):
|
|
11
|
+
0 8 bytes magic b'RAW1.01\\x00'
|
|
12
|
+
12 4 <I range_cnt (number of scan ranges)
|
|
13
|
+
608 4 ascii anode_material
|
|
14
|
+
616 8 <d alpha_average (Ka average wavelength, A)
|
|
15
|
+
624 8 <d alpha1
|
|
16
|
+
632 8 <d alpha2
|
|
17
|
+
|
|
18
|
+
Per-range header (nominally 304 bytes, but read header_len):
|
|
19
|
+
0 4 <I header_len (== 304 for RAW1.01)
|
|
20
|
+
4 4 <I steps (number of data points)
|
|
21
|
+
16 8 <d start_2theta (deg)
|
|
22
|
+
176 8 <d step_size (deg)
|
|
23
|
+
192 4 <f time_per_step (s)
|
|
24
|
+
256 4 <I supplementary_headers_size
|
|
25
|
+
|
|
26
|
+
Intensity data (per range):
|
|
27
|
+
starts at range_start + header_len + supplementary_headers_size
|
|
28
|
+
<steps> float32 counts (stored as float even when integer)
|
|
29
|
+
next range (if any) starts immediately after: data_start + steps*4
|
|
30
|
+
|
|
31
|
+
The supplementary-header block is variable (0 in some files, 40 in others), so
|
|
32
|
+
``data_start`` must be computed from the file's own header_len + supp size, not
|
|
33
|
+
a hardcoded 304 — a one-file test would miss this.
|
|
34
|
+
|
|
35
|
+
Reference: xylib (github.com/wojdyr/xylib), ``bruker_raw.cpp``. Sample files
|
|
36
|
+
``xylib_BT86.raw`` / ``xylib_Cu3Au.raw`` (LGPL-2.1, attribution) seed the
|
|
37
|
+
parity tests.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import struct
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
|
|
48
|
+
from quantized.datastruct import DataStruct
|
|
49
|
+
|
|
50
|
+
__all__ = ["import_bruker_raw", "is_bruker_raw"]
|
|
51
|
+
|
|
52
|
+
_FILE_HEADER_LEN = 712
|
|
53
|
+
_RANGE_HEADER_LEN = 304
|
|
54
|
+
_MAGIC = b"RAW1.01\x00"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_bruker_raw(path: Path) -> bool:
|
|
58
|
+
"""Sniff a ``.raw`` as Bruker RAW1.01 via its magic bytes.
|
|
59
|
+
|
|
60
|
+
Rigaku ``.raw`` uses magic ``FI``, so there is no collision. Other Bruker
|
|
61
|
+
versions (``RAW2``/``RAW4.00``) start with ``RAW`` too but are not the
|
|
62
|
+
RAW1.01 layout; this sniffer accepts only RAW1.01 so the registry does not
|
|
63
|
+
mis-route them.
|
|
64
|
+
"""
|
|
65
|
+
with Path(path).open("rb") as fh:
|
|
66
|
+
return fh.read(8) == _MAGIC
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def import_bruker_raw(
|
|
70
|
+
filepath: str | Path,
|
|
71
|
+
*,
|
|
72
|
+
use_counts_per_sec: bool = False,
|
|
73
|
+
allow_partial: bool = False,
|
|
74
|
+
) -> DataStruct:
|
|
75
|
+
"""Import a Bruker RAW1.01 ``.raw`` (2theta vs intensity).
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
use_counts_per_sec
|
|
80
|
+
Divide counts by ``time_per_step`` and label the channel ``counts/s``.
|
|
81
|
+
allow_partial
|
|
82
|
+
Multi-range files import range 0 only. Without this flag a multi-range
|
|
83
|
+
file raises (so the truncation is never silent); with it, range 0 is
|
|
84
|
+
returned and the extra ranges are recorded in metadata.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
DataStruct
|
|
89
|
+
``time`` = 2theta (deg), one ``Intensity`` channel.
|
|
90
|
+
"""
|
|
91
|
+
path = Path(filepath)
|
|
92
|
+
raw = path.read_bytes()
|
|
93
|
+
n_bytes = len(raw)
|
|
94
|
+
if n_bytes < _FILE_HEADER_LEN + _RANGE_HEADER_LEN:
|
|
95
|
+
raise ValueError(f"file too small to be a Bruker RAW1.01 ({n_bytes} bytes): {path.name}")
|
|
96
|
+
if raw[0:8] != _MAGIC:
|
|
97
|
+
raise ValueError(f"bad magic {raw[0:8]!r} (expected {_MAGIC!r}): {path.name}")
|
|
98
|
+
|
|
99
|
+
range_cnt = int(struct.unpack_from("<I", raw, 12)[0])
|
|
100
|
+
if range_cnt < 1:
|
|
101
|
+
raise ValueError(f"no scan ranges in file: {path.name}")
|
|
102
|
+
|
|
103
|
+
range_start = _FILE_HEADER_LEN
|
|
104
|
+
header_len = int(struct.unpack_from("<I", raw, range_start + 0)[0])
|
|
105
|
+
steps = int(struct.unpack_from("<I", raw, range_start + 4)[0])
|
|
106
|
+
start_2theta = float(struct.unpack_from("<d", raw, range_start + 16)[0])
|
|
107
|
+
step_size = float(struct.unpack_from("<d", raw, range_start + 176)[0])
|
|
108
|
+
time_per_step = float(struct.unpack_from("<f", raw, range_start + 192)[0])
|
|
109
|
+
supp_len = int(struct.unpack_from("<I", raw, range_start + 256)[0])
|
|
110
|
+
|
|
111
|
+
if header_len < _RANGE_HEADER_LEN:
|
|
112
|
+
raise ValueError(f"implausible range header length ({header_len}): {path.name}")
|
|
113
|
+
if steps < 1:
|
|
114
|
+
raise ValueError(f"range has no data points: {path.name}")
|
|
115
|
+
if step_size <= 0 or step_size > 10:
|
|
116
|
+
raise ValueError(f"implausible step size ({step_size:.6g} deg): {path.name}")
|
|
117
|
+
|
|
118
|
+
data_start = range_start + header_len + supp_len
|
|
119
|
+
if data_start + steps * 4 > n_bytes:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"range claims {steps} points but only {(n_bytes - data_start) // 4} fit: {path.name}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if range_cnt > 1 and not allow_partial:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"multi-range .raw detected ({range_cnt} ranges); pass allow_partial=True "
|
|
127
|
+
f"to import the first range only: {path.name}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
intensities = np.frombuffer(raw, dtype="<f4", count=steps, offset=data_start).astype(float)
|
|
131
|
+
two_theta = start_2theta + np.arange(steps) * step_size
|
|
132
|
+
|
|
133
|
+
if use_counts_per_sec and time_per_step > 0:
|
|
134
|
+
values = intensities / time_per_step
|
|
135
|
+
unit = "counts/s"
|
|
136
|
+
else:
|
|
137
|
+
values = intensities
|
|
138
|
+
unit = "counts"
|
|
139
|
+
|
|
140
|
+
anode = raw[608:612].split(b"\x00", 1)[0].decode("ascii", "replace").strip()
|
|
141
|
+
metadata: dict[str, Any] = {
|
|
142
|
+
"source": str(path),
|
|
143
|
+
"parser_name": "import_bruker_raw",
|
|
144
|
+
"format_version": "RAW1.01",
|
|
145
|
+
"x_column_name": "2-Theta",
|
|
146
|
+
"x_column_unit": "deg",
|
|
147
|
+
"num_points": steps,
|
|
148
|
+
"start_angle": start_2theta,
|
|
149
|
+
"end_angle": float(two_theta[-1]),
|
|
150
|
+
"step_size": step_size,
|
|
151
|
+
"time_per_step": time_per_step,
|
|
152
|
+
"range_count": range_cnt,
|
|
153
|
+
"anode_material": anode,
|
|
154
|
+
"alpha_average": float(struct.unpack_from("<d", raw, 616)[0]),
|
|
155
|
+
}
|
|
156
|
+
return DataStruct.create(
|
|
157
|
+
two_theta, values, labels=["Intensity"], units=[unit], metadata=metadata
|
|
158
|
+
)
|
quantized/io/cif.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""CIF (Crystallographic Information File) parser. Port of calc.importCIF.
|
|
2
|
+
|
|
3
|
+
Returns a crystal-structure dict (not a DataStruct — a crystal structure is not a
|
|
4
|
+
time/value series), so this parser is standalone and NOT registered in the
|
|
5
|
+
DataStruct ``registry``. Pure io layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
__all__ = ["import_cif"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _comment_pos(line: str) -> int:
|
|
18
|
+
"""Index of the first '#' not inside a quoted string, or -1."""
|
|
19
|
+
in_single = in_double = False
|
|
20
|
+
for k, ch in enumerate(line):
|
|
21
|
+
if ch == "'" and not in_double:
|
|
22
|
+
in_single = not in_single
|
|
23
|
+
elif ch == '"' and not in_single:
|
|
24
|
+
in_double = not in_double
|
|
25
|
+
elif ch == "#" and not in_single and not in_double:
|
|
26
|
+
return k
|
|
27
|
+
return -1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _strip_uncertainty(s: str) -> float:
|
|
31
|
+
"""'5.4309(2)' -> 5.4309, '3.905' -> 3.905, '?'/'.' -> NaN."""
|
|
32
|
+
s = s.strip()
|
|
33
|
+
if s in ("?", "."):
|
|
34
|
+
return float("nan")
|
|
35
|
+
s = re.sub(r"\([^)]*\)", "", s)
|
|
36
|
+
try:
|
|
37
|
+
return float(s)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return float("nan")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _extract_single_value(token: str) -> str:
|
|
43
|
+
"""Strip surrounding quotes; for unquoted, take the first whitespace token."""
|
|
44
|
+
token = token.strip()
|
|
45
|
+
if not token:
|
|
46
|
+
return ""
|
|
47
|
+
if token[0] == "'":
|
|
48
|
+
idx = token.rfind("'")
|
|
49
|
+
return token[1:idx] if idx > 0 else token[1:]
|
|
50
|
+
if token[0] == '"':
|
|
51
|
+
idx = token.rfind('"')
|
|
52
|
+
return token[1:idx] if idx > 0 else token[1:]
|
|
53
|
+
return token.split()[0]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_semicolon_block(lines: list[str], i: int, n: int) -> tuple[str, int]:
|
|
57
|
+
"""Read a ';'-delimited multi-line text field. Returns (text, last_content_idx)."""
|
|
58
|
+
i += 1
|
|
59
|
+
parts: list[str] = []
|
|
60
|
+
while i < n:
|
|
61
|
+
if lines[i][:1] == ";":
|
|
62
|
+
break
|
|
63
|
+
parts.append(lines[i])
|
|
64
|
+
i += 1
|
|
65
|
+
return " ".join(parts).strip(), i - 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_tag_value(lines: list[str], i: int, n: int) -> tuple[str, str, int]:
|
|
69
|
+
"""Parse a '_tag value' pair (value may be on the next line / a text block)."""
|
|
70
|
+
m = re.match(r"^(\S+)\s*(.*)", lines[i].strip())
|
|
71
|
+
if not m:
|
|
72
|
+
return lines[i].strip(), "", i
|
|
73
|
+
tag = m.group(1).strip().lower()
|
|
74
|
+
value = m.group(2).strip()
|
|
75
|
+
if not value:
|
|
76
|
+
if i + 1 < n:
|
|
77
|
+
next_line = lines[i + 1].strip()
|
|
78
|
+
if next_line and next_line[0] == ";":
|
|
79
|
+
value, i = _read_semicolon_block(lines, i + 1, n)
|
|
80
|
+
else:
|
|
81
|
+
i += 1
|
|
82
|
+
line2 = lines[i].strip()
|
|
83
|
+
cp = _comment_pos(line2)
|
|
84
|
+
if cp >= 0:
|
|
85
|
+
line2 = line2[:cp].strip()
|
|
86
|
+
value = _extract_single_value(line2)
|
|
87
|
+
elif value[0] == ";":
|
|
88
|
+
value, i = _read_semicolon_block(lines, i, n)
|
|
89
|
+
else:
|
|
90
|
+
value = _extract_single_value(value)
|
|
91
|
+
return tag, value, i
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tokenise_cif_line(line: str) -> list[str]:
|
|
95
|
+
"""Split a CIF data line into tokens, respecting single/double quotes."""
|
|
96
|
+
tokens: list[str] = []
|
|
97
|
+
n = len(line)
|
|
98
|
+
k = 0
|
|
99
|
+
while k < n:
|
|
100
|
+
ch = line[k]
|
|
101
|
+
if ch in (" ", "\t"):
|
|
102
|
+
k += 1
|
|
103
|
+
continue
|
|
104
|
+
if ch in ("'", '"'):
|
|
105
|
+
quote = ch
|
|
106
|
+
k += 1
|
|
107
|
+
start = k
|
|
108
|
+
while k < n and line[k] != quote:
|
|
109
|
+
k += 1
|
|
110
|
+
tokens.append(line[start:k])
|
|
111
|
+
k += 1
|
|
112
|
+
else:
|
|
113
|
+
start = k
|
|
114
|
+
while k < n and line[k] not in (" ", "\t"):
|
|
115
|
+
k += 1
|
|
116
|
+
tokens.append(line[start:k])
|
|
117
|
+
return tokens
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_loop(lines: list[str], i: int, n: int) -> tuple[dict[str, Any], int]:
|
|
121
|
+
"""Parse a loop_ block: collect tag names then data tokens into rows."""
|
|
122
|
+
loop_tags: list[str] = []
|
|
123
|
+
while i < n:
|
|
124
|
+
line = lines[i].strip()
|
|
125
|
+
cp = _comment_pos(line)
|
|
126
|
+
if cp >= 0:
|
|
127
|
+
line = line[:cp].strip()
|
|
128
|
+
if not line:
|
|
129
|
+
i += 1
|
|
130
|
+
continue
|
|
131
|
+
if line[0] == "_":
|
|
132
|
+
loop_tags.append(line.strip().lower())
|
|
133
|
+
i += 1
|
|
134
|
+
else:
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
n_cols = len(loop_tags)
|
|
138
|
+
if n_cols == 0:
|
|
139
|
+
return {"tags": [], "data": []}, i
|
|
140
|
+
|
|
141
|
+
tokens: list[str] = []
|
|
142
|
+
while i < n:
|
|
143
|
+
line = lines[i]
|
|
144
|
+
cp = _comment_pos(line)
|
|
145
|
+
if cp >= 0:
|
|
146
|
+
line = line[:cp]
|
|
147
|
+
trimmed = line.strip()
|
|
148
|
+
if not trimmed:
|
|
149
|
+
i += 1
|
|
150
|
+
continue
|
|
151
|
+
first = trimmed.split()[0]
|
|
152
|
+
if first.lower() in ("loop_", "save_") or first[:5].lower() == "data_":
|
|
153
|
+
break
|
|
154
|
+
if first[0] == "_":
|
|
155
|
+
break
|
|
156
|
+
if trimmed[0] == ";":
|
|
157
|
+
block, i = _read_semicolon_block(lines, i, n)
|
|
158
|
+
tokens.append(block)
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
tokens.extend(_tokenise_cif_line(trimmed))
|
|
162
|
+
i += 1
|
|
163
|
+
|
|
164
|
+
n_rows = len(tokens) // n_cols
|
|
165
|
+
data = [[tokens[r * n_cols + c] for c in range(n_cols)] for r in range(n_rows)]
|
|
166
|
+
return {"tags": loop_tags, "data": data}, i
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _find_col(tags: list[str], name: str) -> int:
|
|
170
|
+
return tags.index(name) if name in tags else -1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _get_cell(row: list[str], col: int) -> str:
|
|
174
|
+
return row[col] if 0 <= col < len(row) else ""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_atom_sites(loops: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
178
|
+
"""Build the atom-site list from the first _atom_site_* loop."""
|
|
179
|
+
for lp in loops:
|
|
180
|
+
tags = lp["tags"]
|
|
181
|
+
if not tags or not any(t.startswith("_atom_site_") for t in tags):
|
|
182
|
+
continue
|
|
183
|
+
col = {
|
|
184
|
+
"label": _find_col(tags, "_atom_site_label"),
|
|
185
|
+
"symbol": _find_col(tags, "_atom_site_type_symbol"),
|
|
186
|
+
"x": _find_col(tags, "_atom_site_fract_x"),
|
|
187
|
+
"y": _find_col(tags, "_atom_site_fract_y"),
|
|
188
|
+
"z": _find_col(tags, "_atom_site_fract_z"),
|
|
189
|
+
"occupancy": _find_col(tags, "_atom_site_occupancy"),
|
|
190
|
+
}
|
|
191
|
+
sites = []
|
|
192
|
+
for row in lp["data"]:
|
|
193
|
+
sites.append({
|
|
194
|
+
"label": _get_cell(row, col["label"]),
|
|
195
|
+
"symbol": _get_cell(row, col["symbol"]),
|
|
196
|
+
"x": _strip_uncertainty(_get_cell(row, col["x"])),
|
|
197
|
+
"y": _strip_uncertainty(_get_cell(row, col["y"])),
|
|
198
|
+
"z": _strip_uncertainty(_get_cell(row, col["z"])),
|
|
199
|
+
"occupancy": _strip_uncertainty(_get_cell(row, col["occupancy"])),
|
|
200
|
+
})
|
|
201
|
+
return sites
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def import_cif(file_path: str | Path) -> dict[str, Any]:
|
|
206
|
+
"""Parse a CIF file into a crystal-structure dict. Port of calc.importCIF.
|
|
207
|
+
|
|
208
|
+
Returns ``blockName``, ``tags`` (lowercased tag -> value string), ``loops``,
|
|
209
|
+
``cellParams`` (a/b/c/alpha/beta/gamma, NaN if absent), ``spaceGroup``,
|
|
210
|
+
``formula``, and ``atomSites`` (label/symbol/x/y/z/occupancy).
|
|
211
|
+
"""
|
|
212
|
+
lines = Path(file_path).read_text(encoding="utf-8").splitlines()
|
|
213
|
+
n = len(lines)
|
|
214
|
+
result: dict[str, Any] = {
|
|
215
|
+
"blockName": "",
|
|
216
|
+
"tags": {},
|
|
217
|
+
"loops": [],
|
|
218
|
+
"cellParams": {k: float("nan") for k in ("a", "b", "c", "alpha", "beta", "gamma")},
|
|
219
|
+
"spaceGroup": "",
|
|
220
|
+
"formula": "",
|
|
221
|
+
"atomSites": [],
|
|
222
|
+
}
|
|
223
|
+
tags: dict[str, str] = result["tags"]
|
|
224
|
+
|
|
225
|
+
i = 0
|
|
226
|
+
while i < n:
|
|
227
|
+
raw = lines[i]
|
|
228
|
+
cp = _comment_pos(raw)
|
|
229
|
+
if cp >= 0:
|
|
230
|
+
raw = raw[:cp]
|
|
231
|
+
line = raw.strip()
|
|
232
|
+
if not line or line[0] == "#":
|
|
233
|
+
i += 1
|
|
234
|
+
continue
|
|
235
|
+
if line[:5].lower() == "data_":
|
|
236
|
+
result["blockName"] = line[5:].strip()
|
|
237
|
+
i += 1
|
|
238
|
+
continue
|
|
239
|
+
if line.lower() == "loop_":
|
|
240
|
+
loop_struct, i = _parse_loop(lines, i + 1, n)
|
|
241
|
+
result["loops"].append(loop_struct)
|
|
242
|
+
continue
|
|
243
|
+
if line[0] == "_":
|
|
244
|
+
tag, value, i = _parse_tag_value(lines, i, n)
|
|
245
|
+
tags[tag] = value
|
|
246
|
+
i += 1
|
|
247
|
+
continue
|
|
248
|
+
i += 1
|
|
249
|
+
|
|
250
|
+
cell_map = {
|
|
251
|
+
"_cell_length_a": "a", "_cell_length_b": "b", "_cell_length_c": "c",
|
|
252
|
+
"_cell_angle_alpha": "alpha", "_cell_angle_beta": "beta", "_cell_angle_gamma": "gamma",
|
|
253
|
+
}
|
|
254
|
+
for tag, field in cell_map.items():
|
|
255
|
+
if tag in tags:
|
|
256
|
+
result["cellParams"][field] = _strip_uncertainty(tags[tag])
|
|
257
|
+
|
|
258
|
+
for sg in ("_symmetry_space_group_name_h-m", "_space_group_name_h-m_alt"):
|
|
259
|
+
if sg in tags:
|
|
260
|
+
result["spaceGroup"] = tags[sg].strip()
|
|
261
|
+
break
|
|
262
|
+
if "_chemical_formula_sum" in tags:
|
|
263
|
+
result["formula"] = tags["_chemical_formula_sum"].strip()
|
|
264
|
+
|
|
265
|
+
result["atomSites"] = _extract_atom_sites(result["loops"])
|
|
266
|
+
return result
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Consolidated CSV export: multiple datasets side-by-side in one file. Port of
|
|
2
|
+
the per-dataset-block path of MATLAB ``+bosonPlotter/saveConsolidatedNeutronCSV.m``
|
|
3
|
+
(also the writer ``+bosonPlotter/exportCombinedCSV.m`` uses).
|
|
4
|
+
|
|
5
|
+
Each dataset contributes its own ``Q`` (X) column followed by its value columns,
|
|
6
|
+
each tagged with an Origin designation by role (X / Y / yEr / xEr). Columns may
|
|
7
|
+
differ in length; shorter ones leave trailing cells blank (not ``NaN``) so the
|
|
8
|
+
file imports cleanly into Origin / Excel.
|
|
9
|
+
|
|
10
|
+
Two header styles:
|
|
11
|
+
* ``standard`` — one row of ``<name> (<unit>)``.
|
|
12
|
+
* ``origin`` — four rows: Long Name / Units / File Name / Designation.
|
|
13
|
+
|
|
14
|
+
NOTE: the genuinely-polarized path (shared-Q interpolation + spin asymmetry for
|
|
15
|
+
>=2 distinct ++/-- cross-sections) is not ported here — that narrow neutron case
|
|
16
|
+
needs ++/-- polarization metadata. This covers the general per-dataset block.
|
|
17
|
+
|
|
18
|
+
Pure layer: ``consolidate_csv(datasets, fmt) -> str``. No disk I/O.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from quantized.datastruct import DataStruct
|
|
28
|
+
|
|
29
|
+
__all__ = ["consolidate_csv"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _Col:
|
|
33
|
+
__slots__ = ("name", "unit", "file", "desig", "data")
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, name: str, unit: str, file: str, desig: str, data: np.ndarray
|
|
37
|
+
) -> None:
|
|
38
|
+
self.name = name
|
|
39
|
+
self.unit = unit
|
|
40
|
+
self.file = file
|
|
41
|
+
self.desig = desig
|
|
42
|
+
self.data = data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _meta_get(meta: dict[str, Any], *keys: str, default: Any = None) -> Any:
|
|
46
|
+
sources: list[dict[str, Any]] = [meta]
|
|
47
|
+
for nested in ("parser_specific", "parserSpecific"):
|
|
48
|
+
sub = meta.get(nested)
|
|
49
|
+
if isinstance(sub, dict):
|
|
50
|
+
sources.append(sub)
|
|
51
|
+
for src in sources:
|
|
52
|
+
for key in keys:
|
|
53
|
+
val = src.get(key)
|
|
54
|
+
if val not in (None, ""):
|
|
55
|
+
return val
|
|
56
|
+
return default
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_x_unit(ds: DataStruct) -> str:
|
|
60
|
+
return str(_meta_get(dict(ds.metadata), "xUnit", "x_column_unit", "xColumnUnit", default=""))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _dataset_filename(ds: DataStruct, name: str) -> str:
|
|
64
|
+
source = _meta_get(dict(ds.metadata), "source", "filepath", "filename", default="")
|
|
65
|
+
base = str(source).replace("\\", "/").rsplit("/", 1)[-1]
|
|
66
|
+
return base or name or "dataset"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _column_role(label: str) -> str:
|
|
70
|
+
low = label.lower()
|
|
71
|
+
if low in ("dr", "di") or any(k in low for k in ("uncert", "err", "std", "sigma")):
|
|
72
|
+
return "yEr"
|
|
73
|
+
if "resolution" in low or "dq" in low:
|
|
74
|
+
return "xEr"
|
|
75
|
+
return "Y"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _csv_field(text: str) -> str:
|
|
79
|
+
"""Quote a header cell if it holds a comma, quote, or newline."""
|
|
80
|
+
if any(ch in text for ch in ',"\n\r'):
|
|
81
|
+
return '"' + text.replace('"', '""') + '"'
|
|
82
|
+
return text
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _columns(datasets: list[tuple[DataStruct, str]]) -> list[_Col]:
|
|
86
|
+
cols: list[_Col] = []
|
|
87
|
+
for ds, name in datasets:
|
|
88
|
+
file = _dataset_filename(ds, name)
|
|
89
|
+
time = np.asarray(ds.time, dtype=float)
|
|
90
|
+
values = np.asarray(ds.values, dtype=float)
|
|
91
|
+
cols.append(_Col("Q", _resolve_x_unit(ds), file, "X", time))
|
|
92
|
+
for i, label in enumerate(ds.labels):
|
|
93
|
+
unit = ds.units[i] if i < len(ds.units) else ""
|
|
94
|
+
cols.append(_Col(label, unit, file, _column_role(label), values[:, i]))
|
|
95
|
+
return cols
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def consolidate_csv(datasets: list[tuple[DataStruct, str]], *, fmt: str = "standard") -> str:
|
|
99
|
+
"""Combine ``datasets`` (each ``(DataStruct, name)``) into one CSV string."""
|
|
100
|
+
if fmt not in ("standard", "origin"):
|
|
101
|
+
raise ValueError("fmt must be 'standard' or 'origin'")
|
|
102
|
+
if not datasets:
|
|
103
|
+
raise ValueError("no datasets to consolidate")
|
|
104
|
+
|
|
105
|
+
cols = _columns(datasets)
|
|
106
|
+
lines: list[str] = []
|
|
107
|
+
|
|
108
|
+
if fmt == "origin":
|
|
109
|
+
lines.append(",".join(_csv_field(c.name) for c in cols))
|
|
110
|
+
lines.append(",".join(_csv_field(c.unit) for c in cols))
|
|
111
|
+
lines.append(",".join(_csv_field(c.file) for c in cols))
|
|
112
|
+
lines.append(",".join(_csv_field(c.desig) for c in cols))
|
|
113
|
+
else:
|
|
114
|
+
hdr = [f"{c.name} ({c.unit})" if c.unit else c.name for c in cols]
|
|
115
|
+
lines.append(",".join(_csv_field(h) for h in hdr))
|
|
116
|
+
|
|
117
|
+
max_rows = max((c.data.size for c in cols), default=0)
|
|
118
|
+
for r in range(max_rows):
|
|
119
|
+
cells = [f"{c.data[r]:.10g}" if r < c.data.size else "" for c in cols]
|
|
120
|
+
lines.append(",".join(cells))
|
|
121
|
+
|
|
122
|
+
return "\n".join(lines) + "\n"
|