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,222 @@
|
|
|
1
|
+
"""Generic delimited-text (CSV/TSV) parser. Port of MATLAB parser.importCSV.
|
|
2
|
+
|
|
3
|
+
Auto-detects delimiter, comment lines, the header row, an optional units row,
|
|
4
|
+
and the data start. By default the first column is the x-axis (time) and the
|
|
5
|
+
remaining numeric columns are values. Named ``delimited`` to avoid shadowing
|
|
6
|
+
the stdlib ``csv`` module.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from quantized.datastruct import DataStruct
|
|
20
|
+
from quantized.io.base import resolve_column
|
|
21
|
+
|
|
22
|
+
__all__ = ["import_csv"]
|
|
23
|
+
|
|
24
|
+
_COMMENT_CHARS = "#%"
|
|
25
|
+
_NA_TOKENS = {"", "nan", "na", "-", "n/a"}
|
|
26
|
+
_DELIM_CANDIDATES = (",", "\t", ";", " ")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_numeric(token: str) -> bool:
|
|
30
|
+
"""True if token parses to a number; NaN counts as non-numeric (str2double parity)."""
|
|
31
|
+
try:
|
|
32
|
+
value = float(token)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return False
|
|
35
|
+
return not math.isnan(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _to_float(token: str) -> float:
|
|
39
|
+
stripped = token.strip()
|
|
40
|
+
if stripped.lower() in _NA_TOKENS:
|
|
41
|
+
return float("nan")
|
|
42
|
+
try:
|
|
43
|
+
return float(stripped)
|
|
44
|
+
except ValueError:
|
|
45
|
+
return float("nan")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_raw_lines(text: str) -> list[str]:
|
|
49
|
+
out: list[str] = []
|
|
50
|
+
for raw in text.splitlines():
|
|
51
|
+
stripped = raw.strip()
|
|
52
|
+
if not stripped or stripped[0] in _COMMENT_CHARS:
|
|
53
|
+
continue
|
|
54
|
+
out.append(stripped)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _detect_delimiter(raw_lines: Sequence[str]) -> str:
|
|
59
|
+
test = raw_lines[:10]
|
|
60
|
+
best_delim = ","
|
|
61
|
+
best_score = 0.0
|
|
62
|
+
for ch in _DELIM_CANDIDATES:
|
|
63
|
+
counts = [line.count(ch) for line in test]
|
|
64
|
+
if counts and all(c > 0 for c in counts):
|
|
65
|
+
mean = sum(counts) / len(counts)
|
|
66
|
+
std = (sum((c - mean) ** 2 for c in counts) / len(counts)) ** 0.5
|
|
67
|
+
if std < mean * 0.5 and mean > best_score:
|
|
68
|
+
best_score = mean
|
|
69
|
+
best_delim = ch
|
|
70
|
+
return best_delim
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_units(header: str) -> tuple[str, str]:
|
|
74
|
+
"""``'Temp (C)'`` -> ``('C', 'Temp')`` (also ``[...]``). Returns (unit, label)."""
|
|
75
|
+
paren = re.match(r"(.+?)\s*\(([^)]+)\)\s*$", header)
|
|
76
|
+
if paren:
|
|
77
|
+
return paren.group(2).strip(), paren.group(1).strip()
|
|
78
|
+
brack = re.match(r"(.+?)\s*\[([^\]]+)\]\s*$", header)
|
|
79
|
+
if brack:
|
|
80
|
+
return brack.group(2).strip(), brack.group(1).strip()
|
|
81
|
+
return "", header
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _numeric_score(row: Sequence[str]) -> float:
|
|
85
|
+
if not row:
|
|
86
|
+
return 0.0
|
|
87
|
+
return sum(1 for t in row if _is_numeric(t.strip())) / len(row)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _looks_like_units_row(row: Sequence[str], n_data_cols: int) -> bool:
|
|
91
|
+
n = len(row)
|
|
92
|
+
if n < max(n_data_cols * 0.5, 2):
|
|
93
|
+
return False
|
|
94
|
+
n_unit_like = 0
|
|
95
|
+
n_non_empty = 0
|
|
96
|
+
for cell in row:
|
|
97
|
+
token = cell.strip()
|
|
98
|
+
if not token:
|
|
99
|
+
n_unit_like += 1
|
|
100
|
+
continue
|
|
101
|
+
n_non_empty += 1
|
|
102
|
+
if re.match(r"^[(\[{].*[)\]}]$", token):
|
|
103
|
+
n_unit_like += 1
|
|
104
|
+
elif " " not in token and not _is_numeric(token):
|
|
105
|
+
has_non_alpha = re.search(r"[^a-zA-Z]", token) is not None
|
|
106
|
+
if (has_non_alpha and len(token) <= 10) or (not has_non_alpha and len(token) <= 4):
|
|
107
|
+
n_unit_like += 1
|
|
108
|
+
return n_non_empty > 0 and (n_unit_like / max(n, 1)) >= 0.6
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _detect_layout(tokens: Sequence[Sequence[str]]) -> tuple[int, int, int]:
|
|
112
|
+
"""Return 0-based (header_row, data_start, units_row); -1 when absent."""
|
|
113
|
+
scores = [_numeric_score(row) for row in tokens]
|
|
114
|
+
first_data = next((i for i, s in enumerate(scores) if s > 0.5), 0)
|
|
115
|
+
header_row = -1
|
|
116
|
+
units_row = -1
|
|
117
|
+
if (
|
|
118
|
+
first_data >= 2
|
|
119
|
+
and scores[first_data - 1] < 0.5
|
|
120
|
+
and scores[first_data - 2] < 0.5
|
|
121
|
+
and _looks_like_units_row(tokens[first_data - 1], len(tokens[first_data]))
|
|
122
|
+
):
|
|
123
|
+
units_row = first_data - 1
|
|
124
|
+
header_row = first_data - 2
|
|
125
|
+
elif first_data >= 1 and scores[first_data - 1] < 0.5:
|
|
126
|
+
header_row = first_data - 1
|
|
127
|
+
return header_row, first_data, units_row
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def import_csv(
|
|
131
|
+
filepath: str | Path,
|
|
132
|
+
*,
|
|
133
|
+
time_column: int | str = 0,
|
|
134
|
+
data_columns: Sequence[int | str] | None = None,
|
|
135
|
+
) -> DataStruct:
|
|
136
|
+
"""Import a generic delimited text file (first column = x-axis by default)."""
|
|
137
|
+
path = Path(filepath)
|
|
138
|
+
raw_lines = _read_raw_lines(path.read_text(encoding="latin-1"))
|
|
139
|
+
if not raw_lines:
|
|
140
|
+
raise ValueError(f"file empty or only comments: {path.name}")
|
|
141
|
+
delim = _detect_delimiter(raw_lines)
|
|
142
|
+
tokens = [line.split(delim) for line in raw_lines]
|
|
143
|
+
|
|
144
|
+
header_row, data_start, units_row = _detect_layout(tokens)
|
|
145
|
+
n_data_cols = len(tokens[data_start])
|
|
146
|
+
if header_row >= 0:
|
|
147
|
+
col_headers = [c.strip() for c in tokens[header_row]]
|
|
148
|
+
else:
|
|
149
|
+
col_headers = [f"Col{k + 1}" for k in range(n_data_cols)]
|
|
150
|
+
if len(col_headers) < n_data_cols:
|
|
151
|
+
col_headers += [f"Col{k + 1}" for k in range(len(col_headers), n_data_cols)]
|
|
152
|
+
elif len(col_headers) > n_data_cols:
|
|
153
|
+
col_headers = col_headers[:n_data_cols]
|
|
154
|
+
col_headers = [h if h.strip() else f"Col{k + 1}" for k, h in enumerate(col_headers)]
|
|
155
|
+
n_cols = len(col_headers)
|
|
156
|
+
|
|
157
|
+
row_units: list[str] = []
|
|
158
|
+
if units_row >= 0:
|
|
159
|
+
utok = [u.strip() for u in tokens[units_row]]
|
|
160
|
+
for k in range(n_cols):
|
|
161
|
+
cell = utok[k] if k < len(utok) else ""
|
|
162
|
+
row_units.append(re.sub(r"^\s*[(\[](.*?)[)\]]\s*$", r"\1", cell))
|
|
163
|
+
|
|
164
|
+
rows: list[list[float]] = []
|
|
165
|
+
for row in tokens[data_start:]:
|
|
166
|
+
vals = [float("nan")] * n_cols
|
|
167
|
+
for c in range(min(len(row), n_cols)):
|
|
168
|
+
vals[c] = _to_float(row[c])
|
|
169
|
+
rows.append(vals)
|
|
170
|
+
matrix = np.asarray(rows, dtype=float)
|
|
171
|
+
n_rows = matrix.shape[0]
|
|
172
|
+
|
|
173
|
+
if isinstance(time_column, int) and time_column < 0:
|
|
174
|
+
time_idx = -1
|
|
175
|
+
else:
|
|
176
|
+
time_idx = resolve_column(time_column, col_headers)
|
|
177
|
+
|
|
178
|
+
time_vec = np.arange(1, n_rows + 1, dtype=float) if time_idx < 0 else matrix[:, time_idx]
|
|
179
|
+
|
|
180
|
+
if data_columns is None:
|
|
181
|
+
candidates = [c for c in range(n_cols) if c != time_idx]
|
|
182
|
+
data_idx = [
|
|
183
|
+
c
|
|
184
|
+
for c in candidates
|
|
185
|
+
if (np.count_nonzero(~np.isnan(matrix[:, c])) / n_rows) > 0.1
|
|
186
|
+
]
|
|
187
|
+
else:
|
|
188
|
+
data_idx = [resolve_column(s, col_headers) for s in data_columns]
|
|
189
|
+
if not data_idx:
|
|
190
|
+
raise ValueError(f"no valid data columns in {path.name}")
|
|
191
|
+
|
|
192
|
+
labels: list[str] = []
|
|
193
|
+
units: list[str] = []
|
|
194
|
+
for c in data_idx:
|
|
195
|
+
unit, label = _extract_units(col_headers[c])
|
|
196
|
+
labels.append(label)
|
|
197
|
+
units.append(unit)
|
|
198
|
+
if row_units:
|
|
199
|
+
for i, c in enumerate(data_idx):
|
|
200
|
+
if c < len(row_units) and row_units[c]:
|
|
201
|
+
units[i] = row_units[c]
|
|
202
|
+
|
|
203
|
+
if time_idx >= 0:
|
|
204
|
+
x_unit, x_name = _extract_units(col_headers[time_idx])
|
|
205
|
+
if not x_name:
|
|
206
|
+
x_name = col_headers[time_idx]
|
|
207
|
+
if row_units and time_idx < len(row_units) and row_units[time_idx]:
|
|
208
|
+
x_unit = row_units[time_idx]
|
|
209
|
+
else:
|
|
210
|
+
x_name, x_unit = "Sample Index", ""
|
|
211
|
+
|
|
212
|
+
metadata: dict[str, Any] = {
|
|
213
|
+
"source": str(path),
|
|
214
|
+
"parser_name": "import_csv",
|
|
215
|
+
"x_column_name": x_name,
|
|
216
|
+
"x_column_unit": x_unit,
|
|
217
|
+
"delimiter": delim,
|
|
218
|
+
"all_column_names": col_headers,
|
|
219
|
+
}
|
|
220
|
+
return DataStruct.create(
|
|
221
|
+
time_vec, matrix[:, data_idx], labels=labels, units=units, metadata=metadata
|
|
222
|
+
)
|
quantized/io/excel.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Excel ``.xlsx`` parser. Port of MATLAB parser.importExcel (via openpyxl).
|
|
2
|
+
|
|
3
|
+
Reads a sheet's cell grid, detects the header + data-start rows from a numeric
|
|
4
|
+
shadow matrix, and (by default) takes the first column as the x-axis. Mirrors
|
|
5
|
+
importExcel's logic; the cell grid replaces MATLAB's ``readcell``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import zipfile
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import openpyxl
|
|
17
|
+
from openpyxl.utils.exceptions import InvalidFileException
|
|
18
|
+
|
|
19
|
+
from quantized.datastruct import DataStruct
|
|
20
|
+
from quantized.io.base import resolve_column
|
|
21
|
+
from quantized.io.delimited import _extract_units
|
|
22
|
+
|
|
23
|
+
__all__ = ["import_excel"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cell_to_float(value: Any) -> float:
|
|
27
|
+
if isinstance(value, bool): # bool is an int subclass — not data
|
|
28
|
+
return float("nan")
|
|
29
|
+
if isinstance(value, (int, float)):
|
|
30
|
+
return float(value)
|
|
31
|
+
return float("nan")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _header_str(value: Any, col: int) -> str:
|
|
35
|
+
if isinstance(value, str):
|
|
36
|
+
return value.strip()
|
|
37
|
+
if not isinstance(value, bool) and isinstance(value, (int, float)):
|
|
38
|
+
return str(value)
|
|
39
|
+
return f"Col{col + 1}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def import_excel(
|
|
43
|
+
filepath: str | Path,
|
|
44
|
+
*,
|
|
45
|
+
sheet: int | str = 0,
|
|
46
|
+
time_column: int | str = 0,
|
|
47
|
+
data_columns: Sequence[int | str] | None = None,
|
|
48
|
+
) -> DataStruct:
|
|
49
|
+
"""Import an ``.xlsx`` sheet (first column = x-axis by default)."""
|
|
50
|
+
path = Path(filepath)
|
|
51
|
+
try:
|
|
52
|
+
workbook = openpyxl.load_workbook(path, data_only=True, read_only=True)
|
|
53
|
+
except (zipfile.BadZipFile, InvalidFileException, OSError) as exc:
|
|
54
|
+
# An empty / non-ZIP / truncated .xlsx raises BadZipFile or
|
|
55
|
+
# InvalidFileException (neither a ValueError) -> would 500 the import
|
|
56
|
+
# route. Reject cleanly instead.
|
|
57
|
+
raise ValueError(f"{path.name} is not a readable .xlsx workbook: {exc}") from exc
|
|
58
|
+
try:
|
|
59
|
+
worksheet = workbook[sheet] if isinstance(sheet, str) else workbook.worksheets[sheet]
|
|
60
|
+
sheet_name = worksheet.title
|
|
61
|
+
grid: list[list[Any]] = [list(row) for row in worksheet.iter_rows(values_only=True)]
|
|
62
|
+
finally:
|
|
63
|
+
workbook.close()
|
|
64
|
+
|
|
65
|
+
while grid and all(v is None for v in grid[-1]):
|
|
66
|
+
grid.pop()
|
|
67
|
+
if not grid:
|
|
68
|
+
raise ValueError(f"sheet has no data: {path.name}")
|
|
69
|
+
n_cols = max(len(r) for r in grid)
|
|
70
|
+
grid = [r + [None] * (n_cols - len(r)) for r in grid]
|
|
71
|
+
while n_cols > 0 and all(row[n_cols - 1] is None for row in grid):
|
|
72
|
+
n_cols -= 1
|
|
73
|
+
grid = [row[:n_cols] for row in grid]
|
|
74
|
+
|
|
75
|
+
num_mat = np.array([[_cell_to_float(v) for v in row] for row in grid], dtype=float)
|
|
76
|
+
scores = [
|
|
77
|
+
(float(np.count_nonzero(~np.isnan(num_mat[i]))) / n_cols) if n_cols else 0.0
|
|
78
|
+
for i in range(len(grid))
|
|
79
|
+
]
|
|
80
|
+
first_data = next((i for i, s in enumerate(scores) if s > 0.5), 0)
|
|
81
|
+
header_row = first_data - 1 if first_data >= 1 and scores[first_data - 1] < 0.5 else -1
|
|
82
|
+
|
|
83
|
+
if header_row >= 0:
|
|
84
|
+
col_headers = [_header_str(grid[header_row][c], c) for c in range(n_cols)]
|
|
85
|
+
else:
|
|
86
|
+
col_headers = [f"Col{c + 1}" for c in range(n_cols)]
|
|
87
|
+
|
|
88
|
+
data = num_mat[first_data:]
|
|
89
|
+
keep = [i for i in range(data.shape[0]) if not np.all(np.isnan(data[i]))]
|
|
90
|
+
data = data[keep]
|
|
91
|
+
if data.shape[0] == 0:
|
|
92
|
+
raise ValueError(f"no numeric data rows in {path.name}")
|
|
93
|
+
n_rows = data.shape[0]
|
|
94
|
+
|
|
95
|
+
if isinstance(time_column, int) and time_column < 0:
|
|
96
|
+
time_idx = -1
|
|
97
|
+
else:
|
|
98
|
+
time_idx = resolve_column(time_column, col_headers)
|
|
99
|
+
time_vec = np.arange(1, n_rows + 1, dtype=float) if time_idx < 0 else data[:, time_idx]
|
|
100
|
+
|
|
101
|
+
if data_columns is None:
|
|
102
|
+
candidates = [c for c in range(n_cols) if c != time_idx]
|
|
103
|
+
data_idx = [
|
|
104
|
+
c for c in candidates if (np.count_nonzero(~np.isnan(data[:, c])) / n_rows) > 0.1
|
|
105
|
+
]
|
|
106
|
+
else:
|
|
107
|
+
data_idx = [resolve_column(s, col_headers) for s in data_columns]
|
|
108
|
+
if not data_idx:
|
|
109
|
+
raise ValueError(f"no valid data columns in {path.name}")
|
|
110
|
+
|
|
111
|
+
labels: list[str] = []
|
|
112
|
+
units: list[str] = []
|
|
113
|
+
for c in data_idx:
|
|
114
|
+
unit, label = _extract_units(col_headers[c])
|
|
115
|
+
labels.append(label)
|
|
116
|
+
units.append(unit)
|
|
117
|
+
|
|
118
|
+
if time_idx >= 0:
|
|
119
|
+
x_unit, x_name = _extract_units(col_headers[time_idx])
|
|
120
|
+
if not x_name:
|
|
121
|
+
x_name = col_headers[time_idx]
|
|
122
|
+
else:
|
|
123
|
+
x_name, x_unit = "Sample Index", ""
|
|
124
|
+
|
|
125
|
+
metadata: dict[str, Any] = {
|
|
126
|
+
"source": str(path),
|
|
127
|
+
"parser_name": "import_excel",
|
|
128
|
+
"x_column_name": x_name,
|
|
129
|
+
"x_column_unit": x_unit,
|
|
130
|
+
"sheet_name": sheet_name,
|
|
131
|
+
"all_column_names": col_headers,
|
|
132
|
+
}
|
|
133
|
+
return DataStruct.create(
|
|
134
|
+
time_vec, data[:, data_idx], labels=labels, units=units, metadata=metadata
|
|
135
|
+
)
|
quantized/io/hdf5.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Self-describing HDF5 exporter. Port of MATLAB ``+utilities/exportHDF5.m``.
|
|
2
|
+
|
|
3
|
+
Writes a unified ``DataStruct`` (plus optional corrected data, correction
|
|
4
|
+
parameters, and peak fits) to a hierarchical HDF5 file with a schema that is
|
|
5
|
+
consistent across data types (VSM, PPMS, XRD, generic CSV/Excel). All parsers
|
|
6
|
+
produce the same ``DataStruct`` layout, so one schema covers everything.
|
|
7
|
+
|
|
8
|
+
Schema overview (v1.0, identical to the MATLAB original)::
|
|
9
|
+
|
|
10
|
+
/raw/ raw data (always written)
|
|
11
|
+
/corrected/ corrected data (optional)
|
|
12
|
+
/corrections/ xOff, yOff, bgSlope, bgInt (optional)
|
|
13
|
+
/peaks/ peak-fit results (optional)
|
|
14
|
+
/metadata/ common metadata attributes
|
|
15
|
+
/metadata/parserSpecific/ instrument-specific attributes
|
|
16
|
+
|
|
17
|
+
String datasets (labels, units, peak status/model) are written as space-padded
|
|
18
|
+
ASCII ``uint8`` matrices with an ``encoding='ASCII_padded_space'`` attribute,
|
|
19
|
+
matching MATLAB so files round-trip across both implementations.
|
|
20
|
+
|
|
21
|
+
Pure layer: ``DataStruct`` in -> ``.h5`` file out. No fastapi/pydantic imports.
|
|
22
|
+
``h5py`` (BSD-3-Clause) is the only extra dependency; it is imported lazily so
|
|
23
|
+
the rest of ``io/`` works without it installed.
|
|
24
|
+
|
|
25
|
+
The tree-shaping logic lives in :mod:`quantized.io._hdf5_layout` (keeps both
|
|
26
|
+
modules under the 500-line ceiling and makes the layout independently testable).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Mapping, Sequence
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from quantized.datastruct import DataStruct
|
|
39
|
+
from quantized.io import _hdf5_layout as layout
|
|
40
|
+
|
|
41
|
+
__all__ = ["write_hdf5"]
|
|
42
|
+
|
|
43
|
+
_VALID_EXTENSIONS = (".h5", ".hdf5")
|
|
44
|
+
_TOOLBOX_NAME = "quantized"
|
|
45
|
+
_HDF5_SCHEMA = "1.0"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_hdf5(
|
|
49
|
+
data: DataStruct,
|
|
50
|
+
output_path: str | Path,
|
|
51
|
+
*,
|
|
52
|
+
corr_data: DataStruct | None = None,
|
|
53
|
+
corrections: Mapping[str, float] | None = None,
|
|
54
|
+
include_peaks: bool = False,
|
|
55
|
+
peaks: Sequence[Mapping[str, Any]] | None = None,
|
|
56
|
+
overwrite: bool = True,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Write ``data`` to a self-describing HDF5 file (port of ``exportHDF5``).
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
data:
|
|
63
|
+
Unified ``DataStruct`` (from any parser). Written to ``/raw/``.
|
|
64
|
+
output_path:
|
|
65
|
+
Destination file; must end in ``.h5`` or ``.hdf5``. Its parent
|
|
66
|
+
directory must already exist (mirrors the MATLAB ``isfolder`` check).
|
|
67
|
+
corr_data:
|
|
68
|
+
Corrected ``DataStruct`` (same layout as ``data``); writes
|
|
69
|
+
``/corrected/`` when given. ``None`` (or ``struct()`` upstream) skips it.
|
|
70
|
+
corrections:
|
|
71
|
+
Mapping with any of ``xOff``/``yOff``/``bgSlope``/``bgInt`` (also
|
|
72
|
+
accepts the quantized snake_case ``x_off``/``y_off``/``bg_slope``/
|
|
73
|
+
``bg_int``). Writes ``/corrections/`` only when at least one finite,
|
|
74
|
+
value is present (mirrors MATLAB ``hasCorrections``).
|
|
75
|
+
include_peaks:
|
|
76
|
+
Write ``/peaks/`` from ``peaks`` (default ``False``).
|
|
77
|
+
peaks:
|
|
78
|
+
Sequence of peak mappings (``center``/``fwhm``/``height``/``bg``/
|
|
79
|
+
``xRange``/``status``/``model``). Required when ``include_peaks=True``.
|
|
80
|
+
overwrite:
|
|
81
|
+
Delete an existing file first (default ``True``). When ``False`` and the
|
|
82
|
+
file exists, raise ``FileExistsError`` (mirrors MATLAB ``Overwrite``).
|
|
83
|
+
|
|
84
|
+
Raises
|
|
85
|
+
------
|
|
86
|
+
ValueError
|
|
87
|
+
Bad file extension.
|
|
88
|
+
FileNotFoundError
|
|
89
|
+
Output directory does not exist.
|
|
90
|
+
FileExistsError
|
|
91
|
+
File exists and ``overwrite=False``.
|
|
92
|
+
ImportError
|
|
93
|
+
``h5py`` is not installed.
|
|
94
|
+
"""
|
|
95
|
+
out = Path(output_path)
|
|
96
|
+
_validate_path(out, overwrite=overwrite)
|
|
97
|
+
|
|
98
|
+
h5py = _import_h5py()
|
|
99
|
+
|
|
100
|
+
norm_corr = _normalize_corrections(corrections)
|
|
101
|
+
has_corr_data = corr_data is not None
|
|
102
|
+
peak_list = list(peaks) if peaks else []
|
|
103
|
+
has_peaks = include_peaks and len(peak_list) > 0
|
|
104
|
+
has_corrections = _has_corrections(norm_corr)
|
|
105
|
+
corrections_applied = has_corrections and any(v != 0.0 for v in norm_corr.values())
|
|
106
|
+
|
|
107
|
+
if out.exists():
|
|
108
|
+
out.unlink()
|
|
109
|
+
|
|
110
|
+
with h5py.File(out, "w") as hf:
|
|
111
|
+
# Root sentinel dataset + root-level attributes.
|
|
112
|
+
hf.create_dataset("file_schema_version", data=np.uint8(1).reshape(1, 1))
|
|
113
|
+
hf.attrs["toolboxName"] = _TOOLBOX_NAME
|
|
114
|
+
hf.attrs["hdf5Schema"] = _HDF5_SCHEMA
|
|
115
|
+
hf.attrs["createdAt"] = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
|
|
116
|
+
hf.attrs["hasCorrected"] = np.uint8(has_corr_data)
|
|
117
|
+
hf.attrs["hasPeaks"] = np.uint8(has_peaks)
|
|
118
|
+
hf.attrs["correctionsApplied"] = np.uint8(corrections_applied)
|
|
119
|
+
|
|
120
|
+
layout.write_data_group(hf, "raw", data)
|
|
121
|
+
|
|
122
|
+
if has_corr_data:
|
|
123
|
+
assert corr_data is not None # narrowed by has_corr_data
|
|
124
|
+
layout.write_data_group(hf, "corrected", corr_data)
|
|
125
|
+
|
|
126
|
+
if has_corrections:
|
|
127
|
+
layout.write_corrections_group(hf, norm_corr)
|
|
128
|
+
|
|
129
|
+
if has_peaks:
|
|
130
|
+
layout.write_peaks_group(hf, peak_list)
|
|
131
|
+
|
|
132
|
+
layout.write_metadata_group(hf, data.metadata)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_path(out: Path, *, overwrite: bool) -> None:
|
|
139
|
+
"""Validate extension, parent directory, and overwrite policy."""
|
|
140
|
+
if out.suffix.lower() not in _VALID_EXTENSIONS:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"output_path must end in .h5 or .hdf5 (got: {out.suffix!r})"
|
|
143
|
+
)
|
|
144
|
+
parent = out.parent
|
|
145
|
+
if str(parent) not in ("", ".") and not parent.is_dir():
|
|
146
|
+
raise FileNotFoundError(f"Output directory does not exist: {parent}")
|
|
147
|
+
if not overwrite and out.exists():
|
|
148
|
+
raise FileExistsError(
|
|
149
|
+
f"File already exists and overwrite=False: {out}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _import_h5py() -> Any:
|
|
154
|
+
"""Lazily import ``h5py`` with an actionable error message."""
|
|
155
|
+
try:
|
|
156
|
+
import h5py # noqa: PLC0415
|
|
157
|
+
except ImportError as exc: # pragma: no cover - exercised only without h5py
|
|
158
|
+
raise ImportError(
|
|
159
|
+
"h5py is required for HDF5 export. Install it with: uv add h5py"
|
|
160
|
+
) from exc
|
|
161
|
+
return h5py
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Map quantized snake_case correction names onto the MATLAB attribute names.
|
|
165
|
+
_CORRECTION_ALIASES: dict[str, str] = {
|
|
166
|
+
"x_off": "xOff",
|
|
167
|
+
"y_off": "yOff",
|
|
168
|
+
"bg_slope": "bgSlope",
|
|
169
|
+
"bg_int": "bgInt",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _normalize_corrections(
|
|
174
|
+
corrections: Mapping[str, float] | None,
|
|
175
|
+
) -> dict[str, float]:
|
|
176
|
+
"""Coerce a corrections mapping to the four MATLAB-named float keys."""
|
|
177
|
+
if not corrections:
|
|
178
|
+
return {}
|
|
179
|
+
out: dict[str, float] = {}
|
|
180
|
+
for key, val in corrections.items():
|
|
181
|
+
name = _CORRECTION_ALIASES.get(key, key)
|
|
182
|
+
if name in layout.CORRECTION_FIELDS:
|
|
183
|
+
try:
|
|
184
|
+
out[name] = float(val)
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
continue
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _has_corrections(norm_corr: Mapping[str, float]) -> bool:
|
|
191
|
+
"""True when any normalized correction value is finite (mirrors MATLAB)."""
|
|
192
|
+
return any(np.isfinite(v) for v in norm_corr.values())
|