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/sims.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""SIMS depth-profile parser (.csv/.tsv/.xlsx). Port of parser.importSIMS.
|
|
2
|
+
|
|
3
|
+
Handles two layouts:
|
|
4
|
+
* shared-depth — column 0 is the depth axis, columns 1.. are concentrations;
|
|
5
|
+
* paired — each element has its own (depth, concentration) column pair.
|
|
6
|
+
|
|
7
|
+
For paired files whose elements share an identical depth grid, no interpolation
|
|
8
|
+
is done. Otherwise a union depth grid (finest positive step over the full
|
|
9
|
+
range) is built and each element is linearly interpolated onto it (NaN outside
|
|
10
|
+
its measured range), matching MATLAB's ``interp1(..., 'linear', NaN)``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from quantized.datastruct import DataStruct
|
|
24
|
+
|
|
25
|
+
__all__ = ["import_sims", "is_sims_file"]
|
|
26
|
+
|
|
27
|
+
_COMMENT_CHARS = "#%"
|
|
28
|
+
_DELIM_CANDIDATES = (",", "\t", ";", " ")
|
|
29
|
+
_EXCEL_EXTS = {".xlsx", ".xls", ".xlsm", ".xlsb", ".ods"}
|
|
30
|
+
|
|
31
|
+
# SIMS exports share .csv/.tsv/.xlsx with generic tables, so detect them by a
|
|
32
|
+
# vendor banner ("SIMS", "Evans Analytical", "Drawn Curves") or, failing that,
|
|
33
|
+
# the structural fingerprint of a depth axis carrying concentration units.
|
|
34
|
+
_SIMS_WORD_RE = re.compile(r"\bsims\b", re.IGNORECASE)
|
|
35
|
+
_SIMS_PHRASE_MARKERS = ("evans analytical", "drawn curves", "secondary ion")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _sims_signals(text: str) -> bool:
|
|
39
|
+
if _SIMS_WORD_RE.search(text):
|
|
40
|
+
return True
|
|
41
|
+
low = text.lower()
|
|
42
|
+
if any(m in low for m in _SIMS_PHRASE_MARKERS):
|
|
43
|
+
return True
|
|
44
|
+
return "depth" in low and ("atoms/cc" in low or "atoms/cm" in low)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _excel_preview_text(path: Path, max_rows: int = 8) -> str:
|
|
48
|
+
"""First few rows of sheet 0 flattened to a string (for content sniffing)."""
|
|
49
|
+
import openpyxl
|
|
50
|
+
|
|
51
|
+
workbook = openpyxl.load_workbook(path, data_only=True, read_only=True)
|
|
52
|
+
try:
|
|
53
|
+
ws = workbook.worksheets[0]
|
|
54
|
+
cells: list[str] = []
|
|
55
|
+
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
|
56
|
+
if i >= max_rows:
|
|
57
|
+
break
|
|
58
|
+
cells.extend(str(v) for v in row if v is not None)
|
|
59
|
+
finally:
|
|
60
|
+
workbook.close()
|
|
61
|
+
return " ".join(cells)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_sims_file(path: Path) -> bool:
|
|
65
|
+
"""Content sniffer for ambiguous .csv/.tsv/.xlsx: True for a SIMS depth profile."""
|
|
66
|
+
p = Path(path)
|
|
67
|
+
try:
|
|
68
|
+
if p.suffix.lower() in _EXCEL_EXTS:
|
|
69
|
+
text = _excel_preview_text(p)
|
|
70
|
+
else:
|
|
71
|
+
text = p.read_text(encoding="latin-1", errors="replace")[:4096]
|
|
72
|
+
except Exception: # noqa: BLE001 — a sniffer must never raise; unreadable -> not SIMS
|
|
73
|
+
return False
|
|
74
|
+
return _sims_signals(text)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_numeric(token: str) -> bool:
|
|
78
|
+
try:
|
|
79
|
+
value = float(token)
|
|
80
|
+
except ValueError:
|
|
81
|
+
return False
|
|
82
|
+
return not math.isnan(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _to_float(token: str) -> float:
|
|
86
|
+
stripped = token.strip()
|
|
87
|
+
if not stripped:
|
|
88
|
+
return float("nan")
|
|
89
|
+
try:
|
|
90
|
+
return float(stripped)
|
|
91
|
+
except ValueError:
|
|
92
|
+
return float("nan")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_raw_lines(text: str) -> list[str]:
|
|
96
|
+
out: list[str] = []
|
|
97
|
+
for raw in text.splitlines():
|
|
98
|
+
stripped = raw.strip()
|
|
99
|
+
if not stripped or stripped[0] in _COMMENT_CHARS:
|
|
100
|
+
continue
|
|
101
|
+
out.append(stripped)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _detect_delimiter(raw_lines: Sequence[str]) -> str:
|
|
106
|
+
test = raw_lines[:10]
|
|
107
|
+
best, best_score = ",", 0.0
|
|
108
|
+
for ch in _DELIM_CANDIDATES:
|
|
109
|
+
counts = [line.count(ch) for line in test]
|
|
110
|
+
if counts and all(c > 0 for c in counts):
|
|
111
|
+
mean = sum(counts) / len(counts)
|
|
112
|
+
std = (sum((c - mean) ** 2 for c in counts) / len(counts)) ** 0.5
|
|
113
|
+
if std < mean * 0.5 and mean > best_score:
|
|
114
|
+
best, best_score = ch, mean
|
|
115
|
+
return best
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _numeric_score(row: Sequence[str]) -> float:
|
|
119
|
+
if not row:
|
|
120
|
+
return 0.0
|
|
121
|
+
return sum(1 for t in row if _is_numeric(t.strip())) / len(row)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _detect_layout(tokens: Sequence[Sequence[str]]) -> tuple[int, int]:
|
|
125
|
+
"""0-based (header_row, data_start); header walks back past blank rows."""
|
|
126
|
+
scores = [_numeric_score(r) for r in tokens]
|
|
127
|
+
first_data = next((i for i, s in enumerate(scores) if s > 0.5), 0)
|
|
128
|
+
header_row = -1
|
|
129
|
+
if first_data > 0:
|
|
130
|
+
cand = first_data - 1
|
|
131
|
+
while cand >= 0:
|
|
132
|
+
if any(t.strip() for t in tokens[cand]) and scores[cand] < 0.5:
|
|
133
|
+
break
|
|
134
|
+
cand -= 1
|
|
135
|
+
header_row = cand
|
|
136
|
+
return header_row, first_data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _detect_paired(matrix: np.ndarray) -> bool:
|
|
140
|
+
n_cols = int(matrix.shape[1])
|
|
141
|
+
if n_cols < 4 or n_cols % 2 != 0:
|
|
142
|
+
return False
|
|
143
|
+
n_odd = n_cols // 2
|
|
144
|
+
n_monotonic = 0
|
|
145
|
+
for k in range(n_odd):
|
|
146
|
+
col = matrix[:, 2 * k] # 0-based even = MATLAB odd (depth) columns
|
|
147
|
+
col = col[~np.isnan(col)]
|
|
148
|
+
if col.size >= 2 and bool(np.all(np.diff(col) > 0)):
|
|
149
|
+
n_monotonic += 1
|
|
150
|
+
return bool((n_monotonic / n_odd) >= 0.8)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_BARE_UNIT_RE = re.compile(r"^\(([^)]+)\)$")
|
|
154
|
+
_PAREN_RE = re.compile(r"(.+?)\s*\(([^)]+)\)\s*$")
|
|
155
|
+
_BRACK_RE = re.compile(r"(.+?)\s*\[([^\]]+)\]\s*$")
|
|
156
|
+
_CONC_RE = re.compile(r"^\s*(?:conc(?:entration)?)\s+", re.IGNORECASE)
|
|
157
|
+
_MASS_PREFIX_RE = re.compile(r"^\d+([A-Z][a-z]?)")
|
|
158
|
+
_MASS_TRAIL_RE = re.compile(r"^([A-Z][a-z]?)\d+[+\-]?$")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _clean_element_names(headers: Sequence[str]) -> tuple[list[str], list[str]]:
|
|
162
|
+
names: list[str] = []
|
|
163
|
+
units: list[str] = []
|
|
164
|
+
for header in headers:
|
|
165
|
+
h = header.strip()
|
|
166
|
+
bare = _BARE_UNIT_RE.match(h)
|
|
167
|
+
if bare:
|
|
168
|
+
names.append("")
|
|
169
|
+
units.append(bare.group(1).strip())
|
|
170
|
+
continue
|
|
171
|
+
unit = ""
|
|
172
|
+
paren = _PAREN_RE.match(h)
|
|
173
|
+
brack = _BRACK_RE.match(h)
|
|
174
|
+
if paren:
|
|
175
|
+
h, unit = paren.group(1).strip(), paren.group(2).strip()
|
|
176
|
+
elif brack:
|
|
177
|
+
h, unit = brack.group(1).strip(), brack.group(2).strip()
|
|
178
|
+
h = _CONC_RE.sub("", h)
|
|
179
|
+
mass_prefix = _MASS_PREFIX_RE.match(h)
|
|
180
|
+
if mass_prefix:
|
|
181
|
+
h = mass_prefix.group(1)
|
|
182
|
+
mass_trail = _MASS_TRAIL_RE.match(h)
|
|
183
|
+
if mass_trail:
|
|
184
|
+
h = mass_trail.group(1)
|
|
185
|
+
h = re.sub(r"[+\-]+$", "", h)
|
|
186
|
+
names.append(h)
|
|
187
|
+
units.append(unit)
|
|
188
|
+
return names, units
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _clean_vendor_element(raw: str) -> str:
|
|
192
|
+
name = re.sub(r"-+>$", "", raw).strip()
|
|
193
|
+
if name and name.isalpha():
|
|
194
|
+
name = name[0].upper() + name[1:].lower()
|
|
195
|
+
return name
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _matlab_colon(a: float, d: float, b: float) -> np.ndarray:
|
|
199
|
+
n = int(math.floor((b - a) / d + 1e-10))
|
|
200
|
+
grid = a + np.arange(n + 1) * d
|
|
201
|
+
if grid[-1] < b:
|
|
202
|
+
grid = np.append(grid, b)
|
|
203
|
+
return grid
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _build_union_grid(
|
|
207
|
+
depths: Sequence[np.ndarray], concs: Sequence[np.ndarray]
|
|
208
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
209
|
+
n_e = len(depths)
|
|
210
|
+
all_same = True
|
|
211
|
+
if n_e > 1:
|
|
212
|
+
ref = depths[0]
|
|
213
|
+
scale = float(np.max(np.abs(ref))) if ref.size else 0.0
|
|
214
|
+
for e in range(1, n_e):
|
|
215
|
+
if depths[e].size != ref.size or np.any(
|
|
216
|
+
np.abs(depths[e] - ref) > np.spacing(scale) * 10
|
|
217
|
+
):
|
|
218
|
+
all_same = False
|
|
219
|
+
break
|
|
220
|
+
if all_same and depths[0].size:
|
|
221
|
+
union = depths[0].copy()
|
|
222
|
+
return union, np.column_stack([c for c in concs])
|
|
223
|
+
|
|
224
|
+
all_min, all_max, min_step = math.inf, -math.inf, math.inf
|
|
225
|
+
for d in depths:
|
|
226
|
+
if d.size == 0:
|
|
227
|
+
continue
|
|
228
|
+
all_min = min(all_min, float(d.min()))
|
|
229
|
+
all_max = max(all_max, float(d.max()))
|
|
230
|
+
steps = np.diff(d)
|
|
231
|
+
pos = steps[steps > 0]
|
|
232
|
+
if pos.size:
|
|
233
|
+
min_step = min(min_step, float(pos.min()))
|
|
234
|
+
|
|
235
|
+
if math.isinf(min_step) or all_min >= all_max:
|
|
236
|
+
return depths[0].copy(), np.column_stack([c for c in concs])
|
|
237
|
+
|
|
238
|
+
union = _matlab_colon(all_min, min_step, all_max)
|
|
239
|
+
interp = np.full((union.size, n_e), np.nan)
|
|
240
|
+
for e in range(n_e):
|
|
241
|
+
d, c = depths[e], concs[e]
|
|
242
|
+
if d.size < 2:
|
|
243
|
+
if d.size:
|
|
244
|
+
interp[int(np.argmin(np.abs(union - d[0]))), e] = c[0]
|
|
245
|
+
continue
|
|
246
|
+
interp[:, e] = np.interp(union, d, c, left=np.nan, right=np.nan)
|
|
247
|
+
return union, interp
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _detect_depth_unit(col_headers: Sequence[str], header_meta: Sequence[str]) -> str:
|
|
251
|
+
text = " ".join(list(col_headers) + list(header_meta)).lower()
|
|
252
|
+
if "um" in text or "µ" in text or "micron" in text or "micrometer" in text:
|
|
253
|
+
return "um"
|
|
254
|
+
if "nm" in text or "nanometer" in text:
|
|
255
|
+
return "nm"
|
|
256
|
+
if "angstrom" in text or "Å" in " ".join(col_headers):
|
|
257
|
+
return "A"
|
|
258
|
+
return "nm"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _read_text_tokens(path: Path) -> list[list[str]]:
|
|
262
|
+
raw_lines = _read_raw_lines(path.read_text(encoding="latin-1"))
|
|
263
|
+
if not raw_lines:
|
|
264
|
+
raise ValueError(f"file empty or only comments: {path.name}")
|
|
265
|
+
delim = _detect_delimiter(raw_lines)
|
|
266
|
+
return [line.split(delim) for line in raw_lines]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _read_excel_tokens(path: Path, sheet: int | str) -> list[list[str]]:
|
|
270
|
+
import openpyxl
|
|
271
|
+
|
|
272
|
+
workbook = openpyxl.load_workbook(path, data_only=True, read_only=True)
|
|
273
|
+
try:
|
|
274
|
+
ws = workbook[sheet] if isinstance(sheet, str) else workbook.worksheets[sheet]
|
|
275
|
+
grid = [list(row) for row in ws.iter_rows(values_only=True)]
|
|
276
|
+
finally:
|
|
277
|
+
workbook.close()
|
|
278
|
+
tokens: list[list[str]] = []
|
|
279
|
+
for row in grid:
|
|
280
|
+
cells: list[str] = []
|
|
281
|
+
for v in row:
|
|
282
|
+
if isinstance(v, str):
|
|
283
|
+
cells.append(v)
|
|
284
|
+
elif isinstance(v, bool) or v is None:
|
|
285
|
+
cells.append("")
|
|
286
|
+
elif isinstance(v, (int, float)):
|
|
287
|
+
cells.append(f"{v:.15g}")
|
|
288
|
+
else:
|
|
289
|
+
cells.append(str(v))
|
|
290
|
+
tokens.append(cells)
|
|
291
|
+
return tokens
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def import_sims(
|
|
295
|
+
filepath: str | Path,
|
|
296
|
+
*,
|
|
297
|
+
depth_unit: str = "auto",
|
|
298
|
+
sheet: int | str = 0,
|
|
299
|
+
) -> DataStruct:
|
|
300
|
+
"""Import a SIMS depth profile (paired or shared-depth layout)."""
|
|
301
|
+
path = Path(filepath)
|
|
302
|
+
is_excel = path.suffix.lower() in {".xlsx", ".xls", ".xlsm", ".xlsb", ".ods"}
|
|
303
|
+
tokens = _read_excel_tokens(path, sheet) if is_excel else _read_text_tokens(path)
|
|
304
|
+
|
|
305
|
+
header_row, data_start = _detect_layout(tokens)
|
|
306
|
+
data_rows = tokens[data_start:]
|
|
307
|
+
n_data_cols = max(len(r) for r in data_rows)
|
|
308
|
+
if header_row >= 0:
|
|
309
|
+
col_headers = [c.strip() for c in tokens[header_row]]
|
|
310
|
+
else:
|
|
311
|
+
col_headers = []
|
|
312
|
+
if len(col_headers) < n_data_cols:
|
|
313
|
+
col_headers += [f"Col{k + 1}" for k in range(len(col_headers), n_data_cols)]
|
|
314
|
+
elif len(col_headers) > n_data_cols:
|
|
315
|
+
col_headers = col_headers[:n_data_cols]
|
|
316
|
+
col_headers = [h if h.strip() else f"Col{k + 1}" for k, h in enumerate(col_headers)]
|
|
317
|
+
|
|
318
|
+
header_meta: list[str] = []
|
|
319
|
+
if header_row > 0:
|
|
320
|
+
header_meta = [" ".join(t.strip() for t in tokens[mi]) for mi in range(header_row)]
|
|
321
|
+
|
|
322
|
+
n_cols = len(col_headers)
|
|
323
|
+
matrix = np.full((len(data_rows), n_cols), np.nan)
|
|
324
|
+
for r, row in enumerate(data_rows):
|
|
325
|
+
for c in range(min(len(row), n_cols)):
|
|
326
|
+
matrix[r, c] = _to_float(row[c])
|
|
327
|
+
|
|
328
|
+
empty_mask = np.all(np.isnan(matrix), axis=0)
|
|
329
|
+
matrix = matrix[:, ~empty_mask]
|
|
330
|
+
col_headers = [h for h, drop in zip(col_headers, empty_mask, strict=True) if not drop]
|
|
331
|
+
n_cols = matrix.shape[1]
|
|
332
|
+
if n_cols < 2:
|
|
333
|
+
raise ValueError(f"need >=2 non-empty columns in {path.name}")
|
|
334
|
+
|
|
335
|
+
is_paired = _detect_paired(matrix)
|
|
336
|
+
depths: list[np.ndarray] = []
|
|
337
|
+
concs: list[np.ndarray] = []
|
|
338
|
+
elem_headers: list[str] = []
|
|
339
|
+
if is_paired:
|
|
340
|
+
for p in range(n_cols // 2):
|
|
341
|
+
d_col, c_col = matrix[:, 2 * p], matrix[:, 2 * p + 1]
|
|
342
|
+
valid = ~np.isnan(d_col) & ~np.isnan(c_col)
|
|
343
|
+
depths.append(d_col[valid])
|
|
344
|
+
concs.append(c_col[valid])
|
|
345
|
+
elem_headers.append(col_headers[2 * p + 1])
|
|
346
|
+
else:
|
|
347
|
+
shared = matrix[:, 0]
|
|
348
|
+
for e in range(1, n_cols):
|
|
349
|
+
c_col = matrix[:, e]
|
|
350
|
+
valid = ~np.isnan(shared) & ~np.isnan(c_col)
|
|
351
|
+
depths.append(shared[valid])
|
|
352
|
+
concs.append(c_col[valid])
|
|
353
|
+
elem_headers.append(col_headers[e])
|
|
354
|
+
|
|
355
|
+
elem_names, conc_units = _clean_element_names(elem_headers)
|
|
356
|
+
if is_paired and any(not n for n in elem_names) and header_row > 0:
|
|
357
|
+
_recover_paired_names(elem_names, tokens, header_row, empty_mask)
|
|
358
|
+
|
|
359
|
+
union_depth, interp = _build_union_grid(depths, concs)
|
|
360
|
+
if depth_unit != "auto":
|
|
361
|
+
resolved_unit = depth_unit
|
|
362
|
+
else:
|
|
363
|
+
resolved_unit = _detect_depth_unit(col_headers, header_meta)
|
|
364
|
+
|
|
365
|
+
metadata: dict[str, Any] = {
|
|
366
|
+
"source": str(path),
|
|
367
|
+
"parser_name": "import_sims",
|
|
368
|
+
"x_column_name": "Depth",
|
|
369
|
+
"x_column_unit": resolved_unit,
|
|
370
|
+
"is_paired_layout": is_paired,
|
|
371
|
+
}
|
|
372
|
+
return DataStruct.create(
|
|
373
|
+
union_depth, interp, labels=elem_names, units=conc_units, metadata=metadata
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _recover_paired_names(
|
|
378
|
+
elem_names: list[str],
|
|
379
|
+
tokens: Sequence[Sequence[str]],
|
|
380
|
+
header_row: int,
|
|
381
|
+
empty_mask: np.ndarray,
|
|
382
|
+
) -> None:
|
|
383
|
+
n_e = len(elem_names)
|
|
384
|
+
width = empty_mask.size
|
|
385
|
+
for mi in range(header_row - 1, -1, -1):
|
|
386
|
+
row = list(tokens[mi])
|
|
387
|
+
row = (row + [""] * width)[:width]
|
|
388
|
+
parts = [cell for cell, drop in zip(row, empty_mask, strict=True) if not drop]
|
|
389
|
+
parts = (parts + [""] * (2 * n_e))[: 2 * n_e]
|
|
390
|
+
odd = [p.strip() for p in parts[0::2]]
|
|
391
|
+
even = [p.strip() for p in parts[1::2]]
|
|
392
|
+
n_even_blank = sum(1 for p in even if not p)
|
|
393
|
+
n_odd_text = sum(1 for p in odd if p and not _is_numeric(p))
|
|
394
|
+
if n_even_blank >= n_e // 2 and n_odd_text >= n_e // 2:
|
|
395
|
+
for p in range(n_e):
|
|
396
|
+
if not elem_names[p] and odd[p]:
|
|
397
|
+
elem_names[p] = _clean_vendor_element(odd[p])
|
|
398
|
+
return
|