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
quantized/io/qd.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Quantum Design VSM / PPMS / MPMS ``.dat`` parser.
|
|
2
|
+
|
|
3
|
+
Port of MATLAB ``parser.importQDVSM`` — reads the standard [Header]/[Data]
|
|
4
|
+
format into a :class:`~quantized.datastruct.DataStruct`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from quantized.datastruct import DataStruct
|
|
16
|
+
from quantized.io.base import NO_COLUMN, parse_col_header, resolve_column
|
|
17
|
+
|
|
18
|
+
__all__ = ["import_mpms", "import_ppms", "import_qd_vsm", "is_ppms_dat", "is_qd_file"]
|
|
19
|
+
|
|
20
|
+
_COMMENT_CHARS = (";", "#", "%")
|
|
21
|
+
|
|
22
|
+
# Shorthand -> canonical QD column name (from importQDVSM's resolveQDColumn map).
|
|
23
|
+
_QD_SHORTHAND: dict[str, str] = {
|
|
24
|
+
"field": "Magnetic Field",
|
|
25
|
+
"moment": "Moment",
|
|
26
|
+
"dc": "Moment",
|
|
27
|
+
"dcmoment": "Moment",
|
|
28
|
+
"acmoment": "AC Moment",
|
|
29
|
+
"acsusceptibility": "AC Susceptibility",
|
|
30
|
+
"acsuscept": "AC Susceptibility",
|
|
31
|
+
"temp": "Temperature",
|
|
32
|
+
"temperature": "Temperature",
|
|
33
|
+
"time": "Time Stamp",
|
|
34
|
+
"stderr": "M. Std. Err.",
|
|
35
|
+
"mass": "Mass",
|
|
36
|
+
"pressure": "Pressure",
|
|
37
|
+
"frequency": "Frequency",
|
|
38
|
+
"amplitude": "Peak Amplitude",
|
|
39
|
+
"range": "Range",
|
|
40
|
+
"motorcurrent": "Motor Current",
|
|
41
|
+
"coilsignal": "Coil Signal",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_qd_file(path: Path) -> bool:
|
|
46
|
+
"""Content sniffer: a Quantum Design ``.dat`` has [Header] ... [Data]."""
|
|
47
|
+
head = Path(path).read_text(encoding="latin-1", errors="replace")[:4096].lower()
|
|
48
|
+
return "[header]" in head and ("[data]" in head or "byapp" in head)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _to_float(token: str) -> float:
|
|
52
|
+
token = token.strip()
|
|
53
|
+
if not token:
|
|
54
|
+
return float("nan")
|
|
55
|
+
try:
|
|
56
|
+
return float(token)
|
|
57
|
+
except ValueError:
|
|
58
|
+
return float("nan")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# MPMS3 ``.dat`` files leave the legacy "Moment" column blank and write the
|
|
62
|
+
# signal to "DC Moment Free Ctr" / "DC Moment Fixed Ctr". When the resolved
|
|
63
|
+
# Moment column is entirely empty, fall back to a populated DC-moment column so
|
|
64
|
+
# the data actually plots. (MATLAB importQDVSM lacks this — MPMS3 M(H)/M(T) files
|
|
65
|
+
# import there but the Moment trace is all-NaN; this is a deliberate improvement.)
|
|
66
|
+
_DC_MOMENT_FALLBACKS = ("DC Moment Free Ctr", "DC Moment Fixed Ctr")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _first_populated(
|
|
70
|
+
col_names: Sequence[str], matrix: np.ndarray, candidates: Sequence[str]
|
|
71
|
+
) -> int | None:
|
|
72
|
+
for name in candidates:
|
|
73
|
+
if name in col_names:
|
|
74
|
+
i = list(col_names).index(name)
|
|
75
|
+
if i < matrix.shape[1] and np.isfinite(matrix[:, i]).any():
|
|
76
|
+
return i
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _apply_moment_fallback(
|
|
81
|
+
col_names: Sequence[str], matrix: np.ndarray, y_idx: list[int]
|
|
82
|
+
) -> list[int]:
|
|
83
|
+
"""Swap an all-empty 'Moment' column for a populated DC-moment column."""
|
|
84
|
+
out: list[int] = []
|
|
85
|
+
for idx in y_idx:
|
|
86
|
+
if col_names[idx] == "Moment" and not np.isfinite(matrix[:, idx]).any():
|
|
87
|
+
fb = _first_populated(col_names, matrix, _DC_MOMENT_FALLBACKS)
|
|
88
|
+
out.append(fb if fb is not None else idx)
|
|
89
|
+
else:
|
|
90
|
+
out.append(idx)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# When the resolved x-axis is constant — e.g. an M-vs-T sweep imported with the
|
|
95
|
+
# field default, where the field is regulated flat — the default plot collapses to
|
|
96
|
+
# a single vertical line. Fall back to a sweep axis that actually varies (in
|
|
97
|
+
# priority order) so the data plots meaningfully by default; the user can still
|
|
98
|
+
# re-pick the x-axis in the UI afterwards.
|
|
99
|
+
_X_SWEEP_FALLBACKS = ("temp", "field", "time")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_constant_axis(col: np.ndarray) -> bool:
|
|
103
|
+
"""True if a column has <2 finite points or a negligible (<0.1%) span."""
|
|
104
|
+
finite = col[np.isfinite(col)]
|
|
105
|
+
if finite.size < 2:
|
|
106
|
+
return True
|
|
107
|
+
lo = float(finite.min())
|
|
108
|
+
hi = float(finite.max())
|
|
109
|
+
scale = max(abs(lo), abs(hi), 1.0)
|
|
110
|
+
return (hi - lo) / scale < 1e-3
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _auto_x_index(col_names: Sequence[str], matrix: np.ndarray, x_idx: int) -> int:
|
|
114
|
+
"""If the chosen x column is constant, swap to the first varying sweep axis."""
|
|
115
|
+
if not _is_constant_axis(matrix[:, x_idx]):
|
|
116
|
+
return x_idx
|
|
117
|
+
for short in _X_SWEEP_FALLBACKS:
|
|
118
|
+
try:
|
|
119
|
+
cand = resolve_column(short, col_names, _QD_SHORTHAND, "x-axis")
|
|
120
|
+
except (KeyError, IndexError):
|
|
121
|
+
continue
|
|
122
|
+
if cand not in (NO_COLUMN, x_idx) and not _is_constant_axis(matrix[:, cand]):
|
|
123
|
+
return cand
|
|
124
|
+
return x_idx
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def import_qd_vsm(
|
|
128
|
+
filepath: str | Path,
|
|
129
|
+
*,
|
|
130
|
+
x_axis: str | int = "field",
|
|
131
|
+
y_axis: str | int | Sequence[str | int] = "moment",
|
|
132
|
+
include_raw: bool = False,
|
|
133
|
+
) -> DataStruct:
|
|
134
|
+
"""Import a QD ``.dat`` file. Defaults to Magnetic Field (x) vs Moment (y)."""
|
|
135
|
+
path = Path(filepath)
|
|
136
|
+
raw_lines = path.read_text(encoding="latin-1").splitlines()
|
|
137
|
+
|
|
138
|
+
header, data_start = _parse_header(raw_lines)
|
|
139
|
+
if data_start < 0:
|
|
140
|
+
raise ValueError(f"[Data] section not found in {path.name}")
|
|
141
|
+
|
|
142
|
+
col_names, col_units = _parse_column_row(raw_lines[data_start])
|
|
143
|
+
matrix = _parse_data_rows(raw_lines[data_start + 1 :], len(col_names))
|
|
144
|
+
if matrix.shape[0] == 0:
|
|
145
|
+
raise ValueError(f"no valid data rows in {path.name}")
|
|
146
|
+
|
|
147
|
+
x_idx = resolve_column(x_axis, col_names, _QD_SHORTHAND, "x-axis")
|
|
148
|
+
if x_idx == NO_COLUMN:
|
|
149
|
+
raise ValueError("x-axis column could not be resolved")
|
|
150
|
+
x_idx = _auto_x_index(col_names, matrix, x_idx)
|
|
151
|
+
|
|
152
|
+
if isinstance(y_axis, str) and y_axis.lower() == "all":
|
|
153
|
+
y_idx = _resolve_all_columns(col_names, matrix, x_idx, include_raw)
|
|
154
|
+
else:
|
|
155
|
+
specs: list[str | int] = [y_axis] if isinstance(y_axis, (str, int)) else list(y_axis)
|
|
156
|
+
y_idx = [resolve_column(s, col_names, _QD_SHORTHAND, "y-axis") for s in specs]
|
|
157
|
+
if not y_idx:
|
|
158
|
+
raise ValueError("no valid data columns resolved")
|
|
159
|
+
y_idx = _apply_moment_fallback(col_names, matrix, y_idx)
|
|
160
|
+
|
|
161
|
+
metadata: dict[str, Any] = {
|
|
162
|
+
"source": str(path),
|
|
163
|
+
"parser_name": "import_qd_vsm",
|
|
164
|
+
"x_column_name": col_names[x_idx],
|
|
165
|
+
"x_column_unit": col_units[x_idx],
|
|
166
|
+
"x_column_index": x_idx,
|
|
167
|
+
"y_column_indices": list(y_idx),
|
|
168
|
+
"all_column_names": col_names,
|
|
169
|
+
"all_column_units": col_units,
|
|
170
|
+
**header,
|
|
171
|
+
}
|
|
172
|
+
return DataStruct.create(
|
|
173
|
+
matrix[:, x_idx],
|
|
174
|
+
matrix[:, y_idx],
|
|
175
|
+
labels=[col_names[i] for i in y_idx],
|
|
176
|
+
units=[col_units[i] for i in y_idx],
|
|
177
|
+
metadata=metadata,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _parse_header(raw_lines: Sequence[str]) -> tuple[dict[str, Any], int]:
|
|
182
|
+
header: dict[str, Any] = {"instrument": {}}
|
|
183
|
+
in_header = False
|
|
184
|
+
for i, raw in enumerate(raw_lines):
|
|
185
|
+
line = raw.strip()
|
|
186
|
+
if line.lower() == "[header]":
|
|
187
|
+
in_header = True
|
|
188
|
+
continue
|
|
189
|
+
if line.lower() == "[data]":
|
|
190
|
+
return header, i + 1
|
|
191
|
+
if not in_header or line.startswith(";"):
|
|
192
|
+
continue
|
|
193
|
+
parts = line.split(",")
|
|
194
|
+
if len(parts) < 2:
|
|
195
|
+
continue
|
|
196
|
+
key = parts[0].strip().upper()
|
|
197
|
+
if key == "TITLE":
|
|
198
|
+
header["title"] = ",".join(parts[1:]).strip()
|
|
199
|
+
elif key == "BYAPP":
|
|
200
|
+
header["app"] = ",".join(parts[1:]).strip()
|
|
201
|
+
elif key == "INFO" and len(parts) >= 3:
|
|
202
|
+
header["instrument"][parts[2].strip()] = parts[1].strip()
|
|
203
|
+
elif key == "STARTUPAXIS" and len(parts) >= 3:
|
|
204
|
+
axis = parts[1].strip().lower()
|
|
205
|
+
try:
|
|
206
|
+
col = int(float(parts[2]))
|
|
207
|
+
except ValueError:
|
|
208
|
+
col = NO_COLUMN
|
|
209
|
+
header["startup_axis_x" if axis == "x" else "startup_axis_y"] = col
|
|
210
|
+
return header, -1
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _parse_column_row(col_header: str) -> tuple[list[str], list[str]]:
|
|
214
|
+
names: list[str] = []
|
|
215
|
+
units: list[str] = []
|
|
216
|
+
for cell in col_header.split(","):
|
|
217
|
+
name, unit = parse_col_header(cell.strip())
|
|
218
|
+
names.append(name)
|
|
219
|
+
units.append(unit)
|
|
220
|
+
return names, units
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _parse_data_rows(data_lines: Sequence[str], n_cols: int) -> np.ndarray:
|
|
224
|
+
rows: list[list[float]] = []
|
|
225
|
+
for raw in data_lines:
|
|
226
|
+
if not raw.strip():
|
|
227
|
+
continue
|
|
228
|
+
tokens = raw.split(",")
|
|
229
|
+
row = [float("nan")] * n_cols
|
|
230
|
+
for c in range(min(len(tokens), n_cols)):
|
|
231
|
+
row[c] = _to_float(tokens[c])
|
|
232
|
+
rows.append(row)
|
|
233
|
+
if not rows:
|
|
234
|
+
return np.empty((0, n_cols), dtype=float)
|
|
235
|
+
return np.asarray(rows, dtype=float)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def import_mpms(
|
|
239
|
+
filepath: str | Path,
|
|
240
|
+
*,
|
|
241
|
+
x_axis: str | int = "temp",
|
|
242
|
+
y_axis: str | int | Sequence[str | int] = "dcmoment",
|
|
243
|
+
include_raw: bool = False,
|
|
244
|
+
) -> DataStruct:
|
|
245
|
+
"""Import a QD MPMS SQUID ``.dat``.
|
|
246
|
+
|
|
247
|
+
MATLAB's importMPMS delegates to importQDVSM with MPMS defaults (temperature
|
|
248
|
+
vs DC moment) and re-tags the metadata; this mirrors that exactly.
|
|
249
|
+
"""
|
|
250
|
+
ds = import_qd_vsm(filepath, x_axis=x_axis, y_axis=y_axis, include_raw=include_raw)
|
|
251
|
+
meta = dict(ds.metadata)
|
|
252
|
+
meta["parser_name"] = "import_mpms"
|
|
253
|
+
meta["instrument_type"] = "MPMS SQUID"
|
|
254
|
+
return DataStruct.create(
|
|
255
|
+
ds.time, ds.values, labels=list(ds.labels), units=list(ds.units), metadata=meta
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def is_ppms_dat(path: Path) -> bool:
|
|
260
|
+
"""Sniff a plain-CSV PPMS ``.dat``: no [Header]; first data line names a QD column."""
|
|
261
|
+
head = Path(path).read_text(encoding="latin-1", errors="replace")[:2048]
|
|
262
|
+
if "[header]" in head.lower():
|
|
263
|
+
return False
|
|
264
|
+
for line in head.splitlines():
|
|
265
|
+
stripped = line.strip()
|
|
266
|
+
if not stripped or stripped[0] in _COMMENT_CHARS:
|
|
267
|
+
continue
|
|
268
|
+
low = stripped.lower()
|
|
269
|
+
return ("," in stripped or "\t" in stripped) and (
|
|
270
|
+
"magnetic field" in low or "moment" in low or "temperature" in low
|
|
271
|
+
)
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def import_ppms(
|
|
276
|
+
filepath: str | Path,
|
|
277
|
+
*,
|
|
278
|
+
x_axis: str | int = "field",
|
|
279
|
+
y_axis: str | int | Sequence[str | int] = "moment",
|
|
280
|
+
include_raw: bool = False,
|
|
281
|
+
) -> DataStruct:
|
|
282
|
+
"""Import a legacy PPMS/VSM plain-CSV ``.dat`` (no [Header]/[Data] markers)."""
|
|
283
|
+
path = Path(filepath)
|
|
284
|
+
lines = path.read_text(encoding="latin-1").splitlines()
|
|
285
|
+
|
|
286
|
+
header_idx = next(
|
|
287
|
+
(i for i, ln in enumerate(lines) if ln.strip() and ln.strip()[0] not in _COMMENT_CHARS),
|
|
288
|
+
-1,
|
|
289
|
+
)
|
|
290
|
+
if header_idx < 0:
|
|
291
|
+
raise ValueError(f"no header row found in {path.name}")
|
|
292
|
+
header_line = lines[header_idx]
|
|
293
|
+
delim = "\t" if "\t" in header_line else ","
|
|
294
|
+
|
|
295
|
+
raw_headers = [h.strip() for h in header_line.split(delim)]
|
|
296
|
+
first_col = 0
|
|
297
|
+
if raw_headers and (raw_headers[0].lower() == "comment" or raw_headers[0] == ""):
|
|
298
|
+
raw_headers = raw_headers[1:]
|
|
299
|
+
first_col = 1
|
|
300
|
+
col_names: list[str] = []
|
|
301
|
+
col_units: list[str] = []
|
|
302
|
+
for cell in raw_headers:
|
|
303
|
+
name, unit = parse_col_header(cell)
|
|
304
|
+
col_names.append(name)
|
|
305
|
+
col_units.append(unit)
|
|
306
|
+
n_cols = len(col_names)
|
|
307
|
+
|
|
308
|
+
rows: list[list[float]] = []
|
|
309
|
+
for ln in lines[header_idx + 1 :]:
|
|
310
|
+
if not ln.strip():
|
|
311
|
+
continue
|
|
312
|
+
parts = ln.split(delim)
|
|
313
|
+
row = [float("nan")] * n_cols
|
|
314
|
+
for c in range(n_cols):
|
|
315
|
+
src = c + first_col
|
|
316
|
+
if src < len(parts):
|
|
317
|
+
row[c] = _to_float(parts[src])
|
|
318
|
+
if any(not np.isnan(v) for v in row):
|
|
319
|
+
rows.append(row)
|
|
320
|
+
if not rows:
|
|
321
|
+
raise ValueError(f"no valid data rows in {path.name}")
|
|
322
|
+
matrix = np.asarray(rows, dtype=float)
|
|
323
|
+
|
|
324
|
+
# The PPMS sniffer accepts any QD-ish plain CSV (e.g. resistance-vs-temperature
|
|
325
|
+
# or moment-only files), so the default x/y ("field"/"moment") may be absent.
|
|
326
|
+
# Degrade to auto-detection instead of crashing with a KeyError.
|
|
327
|
+
try:
|
|
328
|
+
x_idx = resolve_column(x_axis, col_names, _QD_SHORTHAND, "x-axis")
|
|
329
|
+
except KeyError:
|
|
330
|
+
x_idx = NO_COLUMN
|
|
331
|
+
if x_idx == NO_COLUMN:
|
|
332
|
+
x_idx = 0
|
|
333
|
+
x_idx = _auto_x_index(col_names, matrix, x_idx)
|
|
334
|
+
if isinstance(y_axis, str) and y_axis.lower() == "all":
|
|
335
|
+
y_idx = _resolve_all_columns(col_names, matrix, x_idx, include_raw)
|
|
336
|
+
else:
|
|
337
|
+
specs: list[str | int] = [y_axis] if isinstance(y_axis, (str, int)) else list(y_axis)
|
|
338
|
+
try:
|
|
339
|
+
y_idx = [resolve_column(s, col_names, _QD_SHORTHAND, "y-axis") for s in specs]
|
|
340
|
+
except KeyError:
|
|
341
|
+
y_idx = _resolve_all_columns(col_names, matrix, x_idx, include_raw)
|
|
342
|
+
if not y_idx:
|
|
343
|
+
raise ValueError("no valid data columns resolved")
|
|
344
|
+
y_idx = _apply_moment_fallback(col_names, matrix, y_idx)
|
|
345
|
+
|
|
346
|
+
metadata: dict[str, Any] = {
|
|
347
|
+
"source": str(path),
|
|
348
|
+
"parser_name": "import_ppms",
|
|
349
|
+
"x_column_name": col_names[x_idx],
|
|
350
|
+
"x_column_unit": col_units[x_idx],
|
|
351
|
+
"all_column_names": col_names,
|
|
352
|
+
"all_column_units": col_units,
|
|
353
|
+
}
|
|
354
|
+
return DataStruct.create(
|
|
355
|
+
matrix[:, x_idx],
|
|
356
|
+
matrix[:, y_idx],
|
|
357
|
+
labels=[col_names[i] for i in y_idx],
|
|
358
|
+
units=[col_units[i] for i in y_idx],
|
|
359
|
+
metadata=metadata,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _resolve_all_columns(
|
|
364
|
+
col_names: Sequence[str],
|
|
365
|
+
matrix: np.ndarray,
|
|
366
|
+
x_idx: int,
|
|
367
|
+
include_raw: bool,
|
|
368
|
+
) -> list[int]:
|
|
369
|
+
"""All numeric columns except x / Comment / Map* with >50% finite values."""
|
|
370
|
+
n_rows = matrix.shape[0]
|
|
371
|
+
idx: list[int] = []
|
|
372
|
+
for c, name in enumerate(col_names):
|
|
373
|
+
if c == x_idx or name == "Comment" or name.startswith("Map"):
|
|
374
|
+
continue
|
|
375
|
+
if not include_raw and ("Raw" in name or "Quad" in name):
|
|
376
|
+
continue
|
|
377
|
+
frac = float(np.count_nonzero(~np.isnan(matrix[:, c]))) / n_rows
|
|
378
|
+
if frac > 0.5:
|
|
379
|
+
idx.append(c)
|
|
380
|
+
return idx
|
quantized/io/refl1d.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""refl1d output ``.dat`` parser (profile / refl / slabs / steps).
|
|
2
|
+
|
|
3
|
+
Port of MATLAB parser.importRefl1dDat. ``#``-prefixed header with optional
|
|
4
|
+
``key: value`` metadata lines and one column-name line ("z (A) rho (1e-6/A2)
|
|
5
|
+
..."); first column -> time, the rest -> values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from quantized.datastruct import DataStruct
|
|
17
|
+
|
|
18
|
+
__all__ = ["import_refl1d_dat", "is_refl1d_dat"]
|
|
19
|
+
|
|
20
|
+
_KV_RE = re.compile(r"^(\w[\w\s]*\w|\w+):\s*(.+)$")
|
|
21
|
+
_TOKEN_RE = re.compile(r"\S+(?:\s*\([^)]*\))?")
|
|
22
|
+
_UNIT_RE = re.compile(r"^(.+?)\s*\(([^)]+)\)$")
|
|
23
|
+
# Column-header signals, matched on word boundaries so a prose comment like
|
|
24
|
+
# "rhodium thermometer" or "quick readout" no longer false-positives (the bare
|
|
25
|
+
# substrings "rho"/"q"+"r" did — mis-routing PPMS files into this parser).
|
|
26
|
+
_RHO_RE = re.compile(r"\brho\b")
|
|
27
|
+
_Z_COL_RE = re.compile(r"\bz\s*\(")
|
|
28
|
+
_Q_COL_RE = re.compile(r"\bq\b")
|
|
29
|
+
_R_COL_RE = re.compile(r"\br\b")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_refl1d_dat(path: Path) -> bool:
|
|
33
|
+
"""Sniff a ``.dat`` as refl1d output: a ``#``-comment column header naming a
|
|
34
|
+
profile (``z``/``rho``) or reflectivity (``Q``/``R``) axis, and not a QD
|
|
35
|
+
``[Header]`` file. The column header may follow other ``#`` metadata lines
|
|
36
|
+
(e.g. ``# intensity:`` / ``# background:`` in refl-fit exports), so scan every
|
|
37
|
+
comment line rather than only the first non-empty one."""
|
|
38
|
+
head = Path(path).read_text(encoding="latin-1", errors="replace")[:512]
|
|
39
|
+
if "[header]" in head.lower():
|
|
40
|
+
return False
|
|
41
|
+
for line in head.splitlines():
|
|
42
|
+
stripped = line.strip()
|
|
43
|
+
if not stripped.startswith("#"):
|
|
44
|
+
continue
|
|
45
|
+
low = stripped.lower()
|
|
46
|
+
if (
|
|
47
|
+
_RHO_RE.search(low)
|
|
48
|
+
or _Z_COL_RE.search(low)
|
|
49
|
+
or (_Q_COL_RE.search(low) and _R_COL_RE.search(low))
|
|
50
|
+
):
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def import_refl1d_dat(filepath: str | Path) -> DataStruct:
|
|
56
|
+
path = Path(filepath)
|
|
57
|
+
lines = path.read_text(encoding="latin-1").splitlines()
|
|
58
|
+
|
|
59
|
+
header_meta: dict[str, Any] = {}
|
|
60
|
+
column_line = ""
|
|
61
|
+
data_start = len(lines)
|
|
62
|
+
for i, raw in enumerate(lines):
|
|
63
|
+
stripped = raw.strip()
|
|
64
|
+
if not stripped.startswith("#"):
|
|
65
|
+
data_start = i
|
|
66
|
+
break
|
|
67
|
+
content = stripped[1:].strip()
|
|
68
|
+
if not content:
|
|
69
|
+
continue
|
|
70
|
+
kv = _KV_RE.match(content)
|
|
71
|
+
if kv:
|
|
72
|
+
key, val = kv.group(1), kv.group(2)
|
|
73
|
+
try:
|
|
74
|
+
header_meta[key] = float(val)
|
|
75
|
+
except ValueError:
|
|
76
|
+
header_meta[key] = val
|
|
77
|
+
else:
|
|
78
|
+
column_line = content
|
|
79
|
+
|
|
80
|
+
labels_all: list[str] = []
|
|
81
|
+
units_all: list[str] = []
|
|
82
|
+
for tok in _TOKEN_RE.findall(column_line):
|
|
83
|
+
unit_match = _UNIT_RE.match(tok.strip())
|
|
84
|
+
if unit_match:
|
|
85
|
+
labels_all.append(unit_match.group(1).strip())
|
|
86
|
+
units_all.append(unit_match.group(2).strip())
|
|
87
|
+
else:
|
|
88
|
+
labels_all.append(tok.strip())
|
|
89
|
+
units_all.append("")
|
|
90
|
+
|
|
91
|
+
rows: list[list[float]] = []
|
|
92
|
+
for raw in lines[data_start:]:
|
|
93
|
+
stripped = raw.strip()
|
|
94
|
+
if not stripped or stripped.startswith("#"):
|
|
95
|
+
continue
|
|
96
|
+
try:
|
|
97
|
+
rows.append([float(t) for t in stripped.split()])
|
|
98
|
+
except ValueError:
|
|
99
|
+
continue
|
|
100
|
+
if not rows:
|
|
101
|
+
raise ValueError(f"no numeric data in {path.name}")
|
|
102
|
+
# Pad/truncate ragged rows to the column count (header if known, else the
|
|
103
|
+
# widest row), filling gaps with NaN — mirrors MATLAB textscan, which yields
|
|
104
|
+
# NaN for missing fields rather than failing on a truncated/disk-cut file.
|
|
105
|
+
target = len(labels_all) if labels_all else max(len(r) for r in rows)
|
|
106
|
+
if any(len(r) != target for r in rows):
|
|
107
|
+
rows = [(r + [float("nan")] * (target - len(r)))[:target] for r in rows]
|
|
108
|
+
matrix = np.asarray(rows, dtype=float)
|
|
109
|
+
n_cols = matrix.shape[1]
|
|
110
|
+
if n_cols < 2:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"refl1d .dat needs at least 2 columns (found {n_cols}) in {path.name}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not labels_all:
|
|
116
|
+
labels_all = [f"Col{j + 1}" for j in range(n_cols)]
|
|
117
|
+
units_all = [""] * n_cols
|
|
118
|
+
|
|
119
|
+
metadata: dict[str, Any] = {
|
|
120
|
+
"source": str(path),
|
|
121
|
+
"parser_name": "import_refl1d_dat",
|
|
122
|
+
"x_column_name": labels_all[0],
|
|
123
|
+
"x_column_unit": units_all[0],
|
|
124
|
+
**header_meta,
|
|
125
|
+
}
|
|
126
|
+
return DataStruct.create(
|
|
127
|
+
matrix[:, 0],
|
|
128
|
+
matrix[:, 1:],
|
|
129
|
+
labels=labels_all[1:n_cols],
|
|
130
|
+
units=units_all[1:n_cols],
|
|
131
|
+
metadata=metadata,
|
|
132
|
+
)
|