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,227 @@
|
|
|
1
|
+
"""Emitters: turn calc result dicts into :class:`ReportSheet`s (#36).
|
|
2
|
+
|
|
3
|
+
Each analysis already returns a plain result dict; these functions map the
|
|
4
|
+
known fields into the report schema (:mod:`quantized.calc.report`) so a fit,
|
|
5
|
+
peak fit, or stats test lands as a structured report that the #37/#38
|
|
6
|
+
exporters and the frontend viewer render uniformly. No new math — pure
|
|
7
|
+
re-shaping.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping, Sequence
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from quantized.calc.report import (
|
|
16
|
+
ReportSheet,
|
|
17
|
+
params_block,
|
|
18
|
+
section,
|
|
19
|
+
table_block,
|
|
20
|
+
text_block,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"from_anova",
|
|
25
|
+
"from_batch_integrate",
|
|
26
|
+
"from_curve_fit",
|
|
27
|
+
"from_integrate",
|
|
28
|
+
"from_multipeak_fit",
|
|
29
|
+
"from_stats_table",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _gof_table(result: Mapping[str, Any], keys: Sequence[tuple[str, str]]) -> dict[str, Any]:
|
|
34
|
+
"""A two-column goodness-of-fit table from selected result keys."""
|
|
35
|
+
rows = [[label, result[key]] for label, key in keys if result.get(key) is not None]
|
|
36
|
+
return table_block(["Metric", "Value"], rows, caption="Goodness of fit")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def from_curve_fit(
|
|
40
|
+
result: Mapping[str, Any],
|
|
41
|
+
*,
|
|
42
|
+
param_names: Sequence[str],
|
|
43
|
+
param_units: Sequence[str] | None = None,
|
|
44
|
+
title: str = "Curve fit",
|
|
45
|
+
model_name: str | None = None,
|
|
46
|
+
source_refs: Sequence[Mapping[str, Any]] | None = None,
|
|
47
|
+
) -> ReportSheet:
|
|
48
|
+
"""Build a report from a ``calc.fitting`` result dict.
|
|
49
|
+
|
|
50
|
+
``result`` carries ``params`` / ``errors`` arrays (whose order matches
|
|
51
|
+
``param_names``) plus goodness-of-fit scalars (R2, chiSqRed, RMSE, AIC).
|
|
52
|
+
"""
|
|
53
|
+
params = list(result.get("params", []))
|
|
54
|
+
errors = list(result.get("errors", []) or [None] * len(params))
|
|
55
|
+
units = list(param_units) if param_units is not None else [""] * len(params)
|
|
56
|
+
if len(param_names) != len(params):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"param_names ({len(param_names)}) must match params ({len(params)})"
|
|
59
|
+
)
|
|
60
|
+
rows = [
|
|
61
|
+
{"name": param_names[i], "value": params[i],
|
|
62
|
+
"error": errors[i] if i < len(errors) else None,
|
|
63
|
+
"unit": units[i] if i < len(units) else ""}
|
|
64
|
+
for i in range(len(params))
|
|
65
|
+
]
|
|
66
|
+
blocks: list[dict[str, Any]] = []
|
|
67
|
+
if model_name:
|
|
68
|
+
blocks.append(text_block(f"Model: {model_name}"))
|
|
69
|
+
blocks.append(params_block(rows, caption="Fitted parameters"))
|
|
70
|
+
blocks.append(
|
|
71
|
+
_gof_table(result, [
|
|
72
|
+
("R²", "R2"), ("Reduced χ²", "chiSqRed"),
|
|
73
|
+
("RMSE", "RMSE"), ("AIC", "AIC"),
|
|
74
|
+
("Free params", "nFree"), ("Points", "nPoints"),
|
|
75
|
+
])
|
|
76
|
+
)
|
|
77
|
+
return ReportSheet(
|
|
78
|
+
title=title,
|
|
79
|
+
sections=(section("Fit results", blocks),),
|
|
80
|
+
source_refs=tuple(dict(r) for r in (source_refs or ())),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def from_multipeak_fit(
|
|
85
|
+
result: Mapping[str, Any],
|
|
86
|
+
*,
|
|
87
|
+
title: str = "Multi-peak fit",
|
|
88
|
+
source_refs: Sequence[Mapping[str, Any]] | None = None,
|
|
89
|
+
) -> ReportSheet:
|
|
90
|
+
"""Build a report from a ``calc.peak_multifit`` result dict."""
|
|
91
|
+
peaks = list(result.get("peaks", []))
|
|
92
|
+
cols = ["Peak", "Model", "Center", "FWHM", "Height", "Area", "η"]
|
|
93
|
+
rows = []
|
|
94
|
+
for i, pk in enumerate(peaks, start=1):
|
|
95
|
+
rows.append([
|
|
96
|
+
i, pk.get("model", result.get("model", "")),
|
|
97
|
+
pk.get("center"), pk.get("fwhm"), pk.get("height"),
|
|
98
|
+
pk.get("area"), pk.get("eta"),
|
|
99
|
+
])
|
|
100
|
+
blocks: list[dict[str, Any]] = [
|
|
101
|
+
table_block(cols, rows, caption=f"{len(peaks)} peak(s)"),
|
|
102
|
+
_gof_table(result, [("RMSE", "rmse"), ("Peaks", "nPeaks")]),
|
|
103
|
+
]
|
|
104
|
+
return ReportSheet(
|
|
105
|
+
title=title,
|
|
106
|
+
sections=(section("Peak fit", blocks),),
|
|
107
|
+
source_refs=tuple(dict(r) for r in (source_refs or ())),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Human labels + display order for the common ANOVA-style row-dict keys.
|
|
112
|
+
_STATS_COLUMNS: dict[str, str] = {
|
|
113
|
+
"source": "Source", "SS": "SS", "df": "df", "MS": "MS", "F": "F", "p": "p",
|
|
114
|
+
"group": "Group", "diff": "Difference", "statistic": "Statistic",
|
|
115
|
+
"ciLow": "CI low", "ciHigh": "CI high", "significant": "Significant",
|
|
116
|
+
"i": "i", "j": "j",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def from_stats_table(
|
|
121
|
+
records: Sequence[Mapping[str, Any]],
|
|
122
|
+
*,
|
|
123
|
+
title: str,
|
|
124
|
+
section_title: str = "Results",
|
|
125
|
+
columns: Sequence[str] | None = None,
|
|
126
|
+
caption: str | None = None,
|
|
127
|
+
source_refs: Sequence[Mapping[str, Any]] | None = None,
|
|
128
|
+
) -> ReportSheet:
|
|
129
|
+
"""Build a report from a list of uniform row-dicts (ANOVA, post-hoc, ...).
|
|
130
|
+
|
|
131
|
+
``columns`` selects/orders the dict keys to show; by default it uses the
|
|
132
|
+
keys of the first record, relabeled via a small known-key map.
|
|
133
|
+
"""
|
|
134
|
+
if not records:
|
|
135
|
+
raise ValueError("from_stats_table needs at least one record")
|
|
136
|
+
keys = list(columns) if columns is not None else list(records[0].keys())
|
|
137
|
+
headers = [_STATS_COLUMNS.get(k, k) for k in keys]
|
|
138
|
+
rows = [[rec.get(k) for k in keys] for rec in records]
|
|
139
|
+
return ReportSheet(
|
|
140
|
+
title=title,
|
|
141
|
+
sections=(section(section_title, [table_block(headers, rows, caption=caption)]),),
|
|
142
|
+
source_refs=tuple(dict(r) for r in (source_refs or ())),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def from_anova(
|
|
147
|
+
result: Mapping[str, Any],
|
|
148
|
+
*,
|
|
149
|
+
title: str = "ANOVA",
|
|
150
|
+
source_refs: Sequence[Mapping[str, Any]] | None = None,
|
|
151
|
+
) -> ReportSheet:
|
|
152
|
+
"""Build a report from any ANOVA result dict carrying a ``table`` key."""
|
|
153
|
+
table = result.get("table")
|
|
154
|
+
if not table:
|
|
155
|
+
raise ValueError("from_anova needs a result with a non-empty 'table'")
|
|
156
|
+
return from_stats_table(
|
|
157
|
+
table, title=title, section_title="ANOVA table",
|
|
158
|
+
columns=["source", "SS", "df", "MS", "F", "p"],
|
|
159
|
+
source_refs=source_refs,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def from_integrate(
|
|
164
|
+
result: Mapping[str, Any],
|
|
165
|
+
*,
|
|
166
|
+
title: str = "Peak integration",
|
|
167
|
+
source_refs: Sequence[Mapping[str, Any]] | None = None,
|
|
168
|
+
) -> ReportSheet:
|
|
169
|
+
"""Build a report from a ``calc.peak_integrate.integrate_peaks`` result."""
|
|
170
|
+
peaks = list(result.get("peaks", []))
|
|
171
|
+
if not peaks:
|
|
172
|
+
raise ValueError("from_integrate needs a result with peaks")
|
|
173
|
+
cols = ["Region", "Area", "% area", "Centroid", "Height", "Position", "FWHM"]
|
|
174
|
+
rows = []
|
|
175
|
+
for i, pk in enumerate(peaks, start=1):
|
|
176
|
+
region = pk.get("region")
|
|
177
|
+
region_str = f"{region[0]:g}–{region[1]:g}" if isinstance(region, list) else i
|
|
178
|
+
rows.append([
|
|
179
|
+
region_str, pk.get("area"), pk.get("area_pct"), pk.get("centroid"),
|
|
180
|
+
pk.get("height"), pk.get("position"), pk.get("fwhm"),
|
|
181
|
+
])
|
|
182
|
+
caption = f"{len(peaks)} region(s), {result.get('baseline')} baseline"
|
|
183
|
+
blocks = [
|
|
184
|
+
table_block(cols, rows, caption=caption),
|
|
185
|
+
text_block(f"Total net area: {result.get('total_area')}"),
|
|
186
|
+
]
|
|
187
|
+
return ReportSheet(
|
|
188
|
+
title=title,
|
|
189
|
+
sections=(section("Integration", blocks),),
|
|
190
|
+
source_refs=tuple(dict(r) for r in (source_refs or ())),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def from_batch_integrate(
|
|
195
|
+
result: Mapping[str, Any],
|
|
196
|
+
*,
|
|
197
|
+
title: str = "Batch peak integration",
|
|
198
|
+
source_refs: Sequence[Mapping[str, Any]] | None = None,
|
|
199
|
+
) -> ReportSheet:
|
|
200
|
+
"""Build a report from a ``calc.peak_batch.batch_integrate_peaks`` result.
|
|
201
|
+
|
|
202
|
+
The area matrix (spectrum x region) becomes a trend table — one row per
|
|
203
|
+
spectrum, one column per region — plus per-spectrum alignment shift.
|
|
204
|
+
"""
|
|
205
|
+
results = list(result.get("results", []))
|
|
206
|
+
regions = list(result.get("regions", []))
|
|
207
|
+
if not results or not regions:
|
|
208
|
+
raise ValueError("from_batch_integrate needs results and regions")
|
|
209
|
+
area_m = result.get("area_matrix", [])
|
|
210
|
+
region_cols = [f"{r[0]:g}–{r[1]:g}" for r in regions]
|
|
211
|
+
header = ["Spectrum", *(["Shift"] if result.get("aligned") else []), *region_cols]
|
|
212
|
+
rows = []
|
|
213
|
+
for i, row in enumerate(results):
|
|
214
|
+
cells: list[Any] = [row.get("label", i)]
|
|
215
|
+
if result.get("aligned"):
|
|
216
|
+
cells.append(row.get("shift_samples"))
|
|
217
|
+
cells.extend(area_m[i] if i < len(area_m) else [None] * len(regions))
|
|
218
|
+
rows.append(cells)
|
|
219
|
+
n_failed = result.get("n_failed", 0)
|
|
220
|
+
caption = f"{len(results)} spectra × {len(regions)} region(s)"
|
|
221
|
+
if n_failed:
|
|
222
|
+
caption += f" ({n_failed} failed)"
|
|
223
|
+
return ReportSheet(
|
|
224
|
+
title=title,
|
|
225
|
+
sections=(section("Area trends", [table_block(header, rows, caption=caption)]),),
|
|
226
|
+
source_refs=tuple(dict(r) for r in (source_refs or ())),
|
|
227
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Resample a DataStruct onto a new x-grid. Port of utilities.resampleData.
|
|
2
|
+
|
|
3
|
+
Pure calc layer. Supports four grid modes (n_points / step / grid / match_dataset;
|
|
4
|
+
exactly one, or none for a 500-point default) and four interpolation methods
|
|
5
|
+
(linear / pchip / spline=not-a-knot / makima). Out-of-range samples are NaN unless
|
|
6
|
+
``extrapolate=True``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from numpy.typing import ArrayLike, NDArray
|
|
15
|
+
from scipy.interpolate import (
|
|
16
|
+
Akima1DInterpolator,
|
|
17
|
+
CubicSpline,
|
|
18
|
+
PchipInterpolator,
|
|
19
|
+
interp1d,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ..datastruct import DataStruct
|
|
23
|
+
|
|
24
|
+
__all__ = ["resample_data"]
|
|
25
|
+
|
|
26
|
+
_METHODS = ("linear", "pchip", "spline", "makima")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _colon(a: float, d: float, b: float) -> NDArray[np.float64]:
|
|
30
|
+
"""MATLAB ``a:d:b`` colon grid (endpoint included only if it lands exactly)."""
|
|
31
|
+
if d == 0:
|
|
32
|
+
raise ValueError("resample step must be non-zero")
|
|
33
|
+
n = int(math.floor((b - a) / d + 1e-10))
|
|
34
|
+
return np.asarray(a + np.arange(n + 1) * d, dtype=float)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _sanitize_xy(
|
|
38
|
+
x: NDArray[np.float64], y: NDArray[np.float64]
|
|
39
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
40
|
+
"""Make ``(x, y)`` safe for interpolation: drop non-finite pairs, sort by x,
|
|
41
|
+
and collapse duplicate x (mean y). Mirrors MATLAB ``interp1``'s effective
|
|
42
|
+
tolerance (it ignores NaN values and accepts unsorted input). Every step is
|
|
43
|
+
guarded, so clean, strictly-increasing, finite data passes through untouched
|
|
44
|
+
— golden parity is preserved; only real-world data (NaN gaps, hysteresis /
|
|
45
|
+
unsorted sweeps, step profiles with repeated x) is repaired."""
|
|
46
|
+
finite = np.isfinite(x) & np.isfinite(y)
|
|
47
|
+
if not bool(finite.all()):
|
|
48
|
+
x, y = x[finite], y[finite]
|
|
49
|
+
if x.size > 1 and not bool(np.all(np.diff(x) > 0)):
|
|
50
|
+
order = np.argsort(x, kind="stable")
|
|
51
|
+
x, y = x[order], y[order]
|
|
52
|
+
if bool((x[1:] == x[:-1]).any()):
|
|
53
|
+
ux, inv = np.unique(x, return_inverse=True)
|
|
54
|
+
sums = np.zeros(ux.size)
|
|
55
|
+
cnts = np.zeros(ux.size)
|
|
56
|
+
np.add.at(sums, inv, y)
|
|
57
|
+
np.add.at(cnts, inv, 1.0)
|
|
58
|
+
x, y = ux, sums / cnts
|
|
59
|
+
return x, y
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _interp_column(
|
|
63
|
+
x: NDArray[np.float64],
|
|
64
|
+
y: NDArray[np.float64],
|
|
65
|
+
x_new: NDArray[np.float64],
|
|
66
|
+
method: str,
|
|
67
|
+
extrapolate: bool,
|
|
68
|
+
) -> NDArray[np.float64]:
|
|
69
|
+
x, y = _sanitize_xy(x, y)
|
|
70
|
+
if x.size < 2:
|
|
71
|
+
# Too few finite points to interpolate — undefined everywhere.
|
|
72
|
+
return np.full(np.shape(x_new), np.nan, dtype=float)
|
|
73
|
+
if method == "linear":
|
|
74
|
+
if extrapolate:
|
|
75
|
+
fn = interp1d(x, y, kind="linear", fill_value="extrapolate", assume_sorted=True)
|
|
76
|
+
return np.asarray(fn(x_new), dtype=float)
|
|
77
|
+
return np.asarray(np.interp(x_new, x, y, left=np.nan, right=np.nan), dtype=float)
|
|
78
|
+
if method == "pchip":
|
|
79
|
+
return np.asarray(PchipInterpolator(x, y, extrapolate=extrapolate)(x_new), dtype=float)
|
|
80
|
+
if method == "spline":
|
|
81
|
+
spline = CubicSpline(x, y, bc_type="not-a-knot", extrapolate=extrapolate)
|
|
82
|
+
return np.asarray(spline(x_new), dtype=float)
|
|
83
|
+
akima = Akima1DInterpolator(x, y, method="makima", extrapolate=extrapolate)
|
|
84
|
+
return np.asarray(akima(x_new), dtype=float)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resample_data(
|
|
88
|
+
data: DataStruct,
|
|
89
|
+
*,
|
|
90
|
+
n_points: int | None = None,
|
|
91
|
+
step: float | None = None,
|
|
92
|
+
grid: ArrayLike | None = None,
|
|
93
|
+
match_dataset: DataStruct | None = None,
|
|
94
|
+
method: str = "makima",
|
|
95
|
+
extrapolate: bool = False,
|
|
96
|
+
) -> DataStruct:
|
|
97
|
+
"""Resample ``data`` onto a new x-grid, interpolating every channel.
|
|
98
|
+
|
|
99
|
+
Specify exactly one grid mode (``n_points``, ``step``, ``grid`` or
|
|
100
|
+
``match_dataset``); with none, a 500-point linspace over the data range is
|
|
101
|
+
used. Returns a new DataStruct with ``resampled``/``resampleMethod``/
|
|
102
|
+
``resamplePoints`` stamped into ``metadata``.
|
|
103
|
+
"""
|
|
104
|
+
if method not in _METHODS:
|
|
105
|
+
raise ValueError(f"method must be one of {_METHODS}")
|
|
106
|
+
x_old = np.asarray(data.time, dtype=float).ravel()
|
|
107
|
+
y_old = np.asarray(data.values, dtype=float)
|
|
108
|
+
if y_old.ndim == 1:
|
|
109
|
+
y_old = y_old.reshape(-1, 1)
|
|
110
|
+
if x_old.size < 2:
|
|
111
|
+
raise ValueError("need at least 2 data points")
|
|
112
|
+
|
|
113
|
+
modes = sum(v is not None for v in (n_points, step, grid, match_dataset))
|
|
114
|
+
if modes > 1:
|
|
115
|
+
raise ValueError("specify only one of: n_points, step, grid, match_dataset")
|
|
116
|
+
# MATLAB min/max ignore NaN; mirror that so a NaN in the x axis doesn't
|
|
117
|
+
# collapse the auto-grid to all-NaN. No-op on clean data (golden parity).
|
|
118
|
+
if not np.isfinite(x_old).any():
|
|
119
|
+
raise ValueError("x axis has no finite values")
|
|
120
|
+
lo, hi = float(np.nanmin(x_old)), float(np.nanmax(x_old))
|
|
121
|
+
if modes == 0:
|
|
122
|
+
x_new = np.linspace(lo, hi, 500)
|
|
123
|
+
elif n_points is not None:
|
|
124
|
+
x_new = np.linspace(lo, hi, int(n_points))
|
|
125
|
+
elif step is not None:
|
|
126
|
+
x_new = _colon(lo, float(step), hi)
|
|
127
|
+
elif grid is not None:
|
|
128
|
+
x_new = np.asarray(grid, dtype=float).ravel()
|
|
129
|
+
else: # match_dataset
|
|
130
|
+
x_new = np.asarray(match_dataset.time, dtype=float).ravel() # type: ignore[union-attr]
|
|
131
|
+
|
|
132
|
+
y_new = np.empty((x_new.size, y_old.shape[1]))
|
|
133
|
+
for c in range(y_old.shape[1]):
|
|
134
|
+
y_new[:, c] = _interp_column(x_old, y_old[:, c], x_new, method, extrapolate)
|
|
135
|
+
|
|
136
|
+
meta = dict(data.metadata)
|
|
137
|
+
meta["resampled"] = True
|
|
138
|
+
meta["resampleMethod"] = method
|
|
139
|
+
meta["resamplePoints"] = int(x_new.size)
|
|
140
|
+
return DataStruct.create(
|
|
141
|
+
x_new, y_new, labels=data.labels, units=data.units, metadata=meta
|
|
142
|
+
)
|
quantized/calc/rsm.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""RSM strain & relaxation analysis. Port of MATLAB ``fitting.rsmStrain``.
|
|
2
|
+
|
|
3
|
+
Given the reciprocal-space peak centres of a substrate and an epitaxial film
|
|
4
|
+
(from a reciprocal-space map — see ``io/xrdml`` 2D + ``calc/qspace``), compute
|
|
5
|
+
the in-plane / out-of-plane strain and the relaxation.
|
|
6
|
+
|
|
7
|
+
Theory (no Miller indices needed — the absolute scale cancels in the ratios):
|
|
8
|
+
for a fixed (hkl), ``|Q| = 2*pi/a`` so the real-space lattice parameter is
|
|
9
|
+
inversely proportional to Q. The in-plane lattice ``a_par ~ 1/Qx`` and the
|
|
10
|
+
out-of-plane ``a_perp ~ 1/Qz``, hence::
|
|
11
|
+
|
|
12
|
+
eps_parallel = (a_film_par - a_sub_par) / a_sub_par = Qx_sub/Qx_film - 1
|
|
13
|
+
eps_perp = (a_film_perp - a_sub_perp) / a_sub_perp = Qz_sub/Qz_film - 1
|
|
14
|
+
|
|
15
|
+
Relaxation measures how far the film has departed from pseudomorphism
|
|
16
|
+
(``Qx_film == Qx_sub``) toward its bulk (relaxed) position::
|
|
17
|
+
|
|
18
|
+
R = (Qx_film - Qx_sub) / (Qx_bulk - Qx_sub) # 0 = strained, 1 = relaxed
|
|
19
|
+
|
|
20
|
+
Returned absolute lattices use the nominal ``a = 2*pi/|Q|`` (consistent ratios).
|
|
21
|
+
|
|
22
|
+
Pure calc layer — no fastapi/pydantic. Mirrors the MATLAB function's outputs.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import math
|
|
28
|
+
import sys
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
__all__ = ["rsm_strain"]
|
|
32
|
+
|
|
33
|
+
# Machine epsilon floor for |Qx| (matches MATLAB's ``max(abs(Qx), eps)`` guard
|
|
34
|
+
# so a symmetric reflection with Qx == 0 gives a finite nominal lattice).
|
|
35
|
+
_EPS = sys.float_info.epsilon
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def rsm_strain(
|
|
39
|
+
q_sub: tuple[float, float],
|
|
40
|
+
q_film: tuple[float, float],
|
|
41
|
+
*,
|
|
42
|
+
bulk: tuple[float, float] | None = None,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
"""Strain + relaxation from a substrate/film peak pair in an RSM.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
q_sub: substrate peak centre ``(Qx, Qz)`` in reciprocal space (Ang^-1).
|
|
48
|
+
q_film: film peak centre ``(Qx, Qz)`` (Ang^-1).
|
|
49
|
+
bulk: optional bulk (relaxed) film position ``(Qx, Qz)``; enables the
|
|
50
|
+
relaxation calculation. When omitted, ``relaxation`` is NaN.
|
|
51
|
+
|
|
52
|
+
Returns a dict with ``eps_parallel``, ``eps_perp``, ``a_sub_parallel``,
|
|
53
|
+
``a_sub_perp``, ``a_film_parallel``, ``a_film_perp``, ``relaxation``.
|
|
54
|
+
``eps_parallel`` is NaN for a symmetric reflection (``Qx == 0``: no in-plane
|
|
55
|
+
information). Raises ``ValueError`` if either ``Qz`` is zero.
|
|
56
|
+
"""
|
|
57
|
+
qx_sub, qz_sub = float(q_sub[0]), float(q_sub[1])
|
|
58
|
+
qx_film, qz_film = float(q_film[0]), float(q_film[1])
|
|
59
|
+
|
|
60
|
+
if qz_sub == 0 or qz_film == 0:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Qz must be non-zero for both peaks (got sub={qz_sub:.4g}, film={qz_film:.4g})"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Strain via Q ratios (no Miller indices required).
|
|
66
|
+
eps_par = math.nan if (qx_sub == 0 or qx_film == 0) else qx_sub / qx_film - 1
|
|
67
|
+
eps_perp = qz_sub / qz_film - 1
|
|
68
|
+
|
|
69
|
+
# Nominal absolute lattices (|Q| = 2*pi/a for any (hkl); consistent ratios).
|
|
70
|
+
two_pi = 2.0 * math.pi
|
|
71
|
+
a_sub_par = two_pi / max(abs(qx_sub), _EPS)
|
|
72
|
+
a_sub_perp = two_pi / abs(qz_sub)
|
|
73
|
+
a_film_par = two_pi / max(abs(qx_film), _EPS)
|
|
74
|
+
a_film_perp = two_pi / abs(qz_film)
|
|
75
|
+
|
|
76
|
+
# Relaxation (only when a bulk position is supplied).
|
|
77
|
+
relaxation = math.nan
|
|
78
|
+
if bulk is not None:
|
|
79
|
+
qx_bulk = float(bulk[0])
|
|
80
|
+
denom = qx_bulk - qx_sub
|
|
81
|
+
relaxation = math.nan if denom == 0 else (qx_film - qx_sub) / denom
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"eps_parallel": eps_par,
|
|
85
|
+
"eps_perp": eps_perp,
|
|
86
|
+
"a_sub_parallel": a_sub_par,
|
|
87
|
+
"a_sub_perp": a_sub_perp,
|
|
88
|
+
"a_film_parallel": a_film_par,
|
|
89
|
+
"a_film_perp": a_film_perp,
|
|
90
|
+
"relaxation": relaxation,
|
|
91
|
+
}
|