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/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""quantized — Python backend for the quantized toolbox port.
|
|
2
|
+
|
|
3
|
+
Layered, enforced architecture (see CLAUDE.md):
|
|
4
|
+
- ``datastruct`` + ``io`` + ``calc`` are pure libraries (no web imports).
|
|
5
|
+
- ``routes`` are thin FastAPI adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.7.0"
|
|
11
|
+
__all__ = ["__version__"]
|
quantized/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Enable ``python -m quantized`` → runs the ``qz`` CLI.
|
|
2
|
+
|
|
3
|
+
The Tauri desktop shell's dev fallback spawns the server as
|
|
4
|
+
``python -m quantized --no-browser`` (so ``kill()`` reaches uvicorn directly,
|
|
5
|
+
not an orphaned launcher). Keeping this module trivial means the entry point
|
|
6
|
+
stays stable regardless of how ``cli.main`` evolves.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from quantized.cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
main()
|
quantized/api.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""``quantized`` headless public API — drive the analysis engine from code.
|
|
2
|
+
|
|
3
|
+
ORIGIN_GAP_PLAN #9. Everything the app does to your data is a pure function you
|
|
4
|
+
can call from a script, notebook, or CI job — no GUI, no server, cross-platform.
|
|
5
|
+
This is the differentiator Origin structurally can't match: *analysis is code*.
|
|
6
|
+
|
|
7
|
+
This module is the **blessed, stable surface**. Names here are frozen — a
|
|
8
|
+
rename or removal breaks ``tests/test_public_api.py`` on purpose. The
|
|
9
|
+
underlying modules (``quantized.calc.*`` / ``quantized.io.*``) may reorganize;
|
|
10
|
+
import from here to stay insulated.
|
|
11
|
+
|
|
12
|
+
Example — load a scan, integrate a peak, build a report, render it::
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import quantized.api as qz
|
|
16
|
+
|
|
17
|
+
data = qz.load("scan.xy") # any registered format -> DataStruct
|
|
18
|
+
x, y = data.time, data.column(0)
|
|
19
|
+
|
|
20
|
+
result = qz.integrate_peaks(x, y, [(28.0, 32.0)]) # net area / centroid / FWHM
|
|
21
|
+
fit = qz.fit_single_peak(x, y, 28.0, 32.0, seed_center=30.0, model="Gaussian")
|
|
22
|
+
|
|
23
|
+
report = qz.from_stats_table(
|
|
24
|
+
[{"region": "111", **result["peaks"][0]}], title="Peak integration"
|
|
25
|
+
)
|
|
26
|
+
open("report.html", "w").write(qz.to_html(report.to_dict()))
|
|
27
|
+
pdf = qz.render_figure(x, [("intensity", y)], fmt="pdf") # publication vector
|
|
28
|
+
|
|
29
|
+
Note: importing this module pulls in matplotlib (for the render helpers), so it
|
|
30
|
+
is heavier than ``import quantized``; the FastAPI server never imports it.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
# ── Data contract + file loading ──────────────────────────────────────────
|
|
36
|
+
from quantized.calc.backgrounds import (
|
|
37
|
+
anchor_baseline,
|
|
38
|
+
footprint_correction,
|
|
39
|
+
shirley_background,
|
|
40
|
+
xrd_low_angle_background,
|
|
41
|
+
)
|
|
42
|
+
from quantized.calc.baseline import (
|
|
43
|
+
baseline_als,
|
|
44
|
+
baseline_modpoly,
|
|
45
|
+
baseline_rolling_ball,
|
|
46
|
+
estimate_background,
|
|
47
|
+
fit_region_background,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# ── Corrections ────────────────────────────────────────────────────────────
|
|
51
|
+
from quantized.calc.corrections import apply_corrections
|
|
52
|
+
|
|
53
|
+
# ── Publication rendering (matplotlib) ─────────────────────────────────────
|
|
54
|
+
from quantized.calc.figure import render_figure
|
|
55
|
+
from quantized.calc.figure_map import render_map_figure
|
|
56
|
+
from quantized.calc.figure_statplots import render_statplot_figure
|
|
57
|
+
|
|
58
|
+
# ── Curve & peak fitting ───────────────────────────────────────────────────
|
|
59
|
+
from quantized.calc.fitting import curve_fit
|
|
60
|
+
from quantized.calc.peak_batch import batch_integrate_peaks
|
|
61
|
+
from quantized.calc.peak_fit import fit_single_peak
|
|
62
|
+
from quantized.calc.peak_integrate import integrate_peaks
|
|
63
|
+
from quantized.calc.peak_multifit import fit_multi_peak
|
|
64
|
+
from quantized.calc.peaks import find_peaks_robust
|
|
65
|
+
|
|
66
|
+
# ── Reporting ──────────────────────────────────────────────────────────────
|
|
67
|
+
from quantized.calc.report import (
|
|
68
|
+
ReportSheet,
|
|
69
|
+
figure_block,
|
|
70
|
+
params_block,
|
|
71
|
+
section,
|
|
72
|
+
source_ref,
|
|
73
|
+
table_block,
|
|
74
|
+
text_block,
|
|
75
|
+
validate_report,
|
|
76
|
+
)
|
|
77
|
+
from quantized.calc.report_emit import (
|
|
78
|
+
from_anova,
|
|
79
|
+
from_batch_integrate,
|
|
80
|
+
from_curve_fit,
|
|
81
|
+
from_integrate,
|
|
82
|
+
from_multipeak_fit,
|
|
83
|
+
from_stats_table,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# ── Statistical plots ──────────────────────────────────────────────────────
|
|
87
|
+
from quantized.calc.statplots import (
|
|
88
|
+
box_stats,
|
|
89
|
+
grouped_box_stats,
|
|
90
|
+
histogram,
|
|
91
|
+
qq_plot,
|
|
92
|
+
violin_kde,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# ── Statistics ─────────────────────────────────────────────────────────────
|
|
96
|
+
from quantized.calc.stats import (
|
|
97
|
+
anova1,
|
|
98
|
+
descriptive_stats,
|
|
99
|
+
lin_regress,
|
|
100
|
+
pca_analysis,
|
|
101
|
+
t_test,
|
|
102
|
+
)
|
|
103
|
+
from quantized.calc.stats_anova2 import anova2, dunnett_test, tukey_hsd
|
|
104
|
+
from quantized.calc.stats_anova_ext import anova2_unbalanced, repeated_measures_anova
|
|
105
|
+
from quantized.calc.stats_dist import fit_distribution, required_n, t_test_power
|
|
106
|
+
from quantized.calc.stats_multivar import (
|
|
107
|
+
correlation_matrix,
|
|
108
|
+
multiple_regression,
|
|
109
|
+
partial_correlation,
|
|
110
|
+
stepwise_regression,
|
|
111
|
+
)
|
|
112
|
+
from quantized.calc.stats_tests import (
|
|
113
|
+
anderson_darling,
|
|
114
|
+
friedman,
|
|
115
|
+
kruskal_wallis,
|
|
116
|
+
ks_normal,
|
|
117
|
+
ks_two_sample,
|
|
118
|
+
levene,
|
|
119
|
+
mann_whitney,
|
|
120
|
+
recommend_test,
|
|
121
|
+
shapiro_wilk,
|
|
122
|
+
sign_test,
|
|
123
|
+
wilcoxon_signed_rank,
|
|
124
|
+
)
|
|
125
|
+
from quantized.datastruct import DataStruct
|
|
126
|
+
from quantized.io.registry import import_auto as load
|
|
127
|
+
from quantized.io.report_export import (
|
|
128
|
+
format_value_error,
|
|
129
|
+
render_report,
|
|
130
|
+
to_html,
|
|
131
|
+
to_latex,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
__all__ = [
|
|
135
|
+
# data + I/O
|
|
136
|
+
"DataStruct",
|
|
137
|
+
"load",
|
|
138
|
+
# fitting
|
|
139
|
+
"curve_fit",
|
|
140
|
+
"fit_single_peak",
|
|
141
|
+
"fit_multi_peak",
|
|
142
|
+
"find_peaks_robust",
|
|
143
|
+
"integrate_peaks",
|
|
144
|
+
"batch_integrate_peaks",
|
|
145
|
+
# baseline
|
|
146
|
+
"estimate_background",
|
|
147
|
+
"fit_region_background",
|
|
148
|
+
"baseline_als",
|
|
149
|
+
"baseline_modpoly",
|
|
150
|
+
"baseline_rolling_ball",
|
|
151
|
+
# backgrounds (GOTO #2/#3/#7 — new beyond MATLAB parity)
|
|
152
|
+
"anchor_baseline",
|
|
153
|
+
"shirley_background",
|
|
154
|
+
"xrd_low_angle_background",
|
|
155
|
+
"footprint_correction",
|
|
156
|
+
# corrections
|
|
157
|
+
"apply_corrections",
|
|
158
|
+
# statistics — core
|
|
159
|
+
"descriptive_stats",
|
|
160
|
+
"lin_regress",
|
|
161
|
+
"t_test",
|
|
162
|
+
"anova1",
|
|
163
|
+
"pca_analysis",
|
|
164
|
+
# statistics — multivariate
|
|
165
|
+
"multiple_regression",
|
|
166
|
+
"correlation_matrix",
|
|
167
|
+
"partial_correlation",
|
|
168
|
+
"stepwise_regression",
|
|
169
|
+
# statistics — designed experiments
|
|
170
|
+
"anova2",
|
|
171
|
+
"anova2_unbalanced",
|
|
172
|
+
"repeated_measures_anova",
|
|
173
|
+
"tukey_hsd",
|
|
174
|
+
"dunnett_test",
|
|
175
|
+
# statistics — nonparametric + assumptions
|
|
176
|
+
"mann_whitney",
|
|
177
|
+
"wilcoxon_signed_rank",
|
|
178
|
+
"kruskal_wallis",
|
|
179
|
+
"friedman",
|
|
180
|
+
"sign_test",
|
|
181
|
+
"shapiro_wilk",
|
|
182
|
+
"anderson_darling",
|
|
183
|
+
"levene",
|
|
184
|
+
"ks_normal",
|
|
185
|
+
"ks_two_sample",
|
|
186
|
+
"recommend_test",
|
|
187
|
+
# statistics — distributions + power
|
|
188
|
+
"fit_distribution",
|
|
189
|
+
"t_test_power",
|
|
190
|
+
"required_n",
|
|
191
|
+
# statistical plots
|
|
192
|
+
"box_stats",
|
|
193
|
+
"grouped_box_stats",
|
|
194
|
+
"violin_kde",
|
|
195
|
+
"qq_plot",
|
|
196
|
+
"histogram",
|
|
197
|
+
# reporting
|
|
198
|
+
"ReportSheet",
|
|
199
|
+
"section",
|
|
200
|
+
"text_block",
|
|
201
|
+
"table_block",
|
|
202
|
+
"params_block",
|
|
203
|
+
"figure_block",
|
|
204
|
+
"source_ref",
|
|
205
|
+
"validate_report",
|
|
206
|
+
"from_curve_fit",
|
|
207
|
+
"from_multipeak_fit",
|
|
208
|
+
"from_anova",
|
|
209
|
+
"from_stats_table",
|
|
210
|
+
"from_integrate",
|
|
211
|
+
"from_batch_integrate",
|
|
212
|
+
# export / rendering
|
|
213
|
+
"render_figure",
|
|
214
|
+
"render_map_figure",
|
|
215
|
+
"render_statplot_figure",
|
|
216
|
+
"render_report",
|
|
217
|
+
"to_latex",
|
|
218
|
+
"to_html",
|
|
219
|
+
"format_value_error",
|
|
220
|
+
]
|
quantized/app.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""FastAPI application factory for quantized.
|
|
2
|
+
|
|
3
|
+
Thin transport layer only — business logic lives in ``calc/`` and ``io/``.
|
|
4
|
+
Composes the per-domain routers; each router is a thin adapter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
16
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
17
|
+
from fastapi.staticfiles import StaticFiles
|
|
18
|
+
|
|
19
|
+
from quantized import __version__
|
|
20
|
+
from quantized.plugins import load_plugins
|
|
21
|
+
from quantized.routes import (
|
|
22
|
+
aggregate,
|
|
23
|
+
baseline,
|
|
24
|
+
books,
|
|
25
|
+
calc,
|
|
26
|
+
corrections,
|
|
27
|
+
crystallography,
|
|
28
|
+
diffusion,
|
|
29
|
+
electrical,
|
|
30
|
+
electrochemistry,
|
|
31
|
+
export,
|
|
32
|
+
export_facets,
|
|
33
|
+
export_figures,
|
|
34
|
+
export_page,
|
|
35
|
+
fitting,
|
|
36
|
+
fitting_bumps,
|
|
37
|
+
import_template,
|
|
38
|
+
import_wizard,
|
|
39
|
+
jobs_api,
|
|
40
|
+
magnetic,
|
|
41
|
+
magnetometry,
|
|
42
|
+
optics,
|
|
43
|
+
parsers,
|
|
44
|
+
peaks,
|
|
45
|
+
plot,
|
|
46
|
+
reductions,
|
|
47
|
+
reference,
|
|
48
|
+
reflectivity,
|
|
49
|
+
report_export,
|
|
50
|
+
rsm,
|
|
51
|
+
samples,
|
|
52
|
+
semiconductor,
|
|
53
|
+
sld,
|
|
54
|
+
spectral,
|
|
55
|
+
statplots,
|
|
56
|
+
stats,
|
|
57
|
+
stats_design,
|
|
58
|
+
substrates,
|
|
59
|
+
superconductor,
|
|
60
|
+
thermal,
|
|
61
|
+
thin_film,
|
|
62
|
+
vacuum,
|
|
63
|
+
xray,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__ = ["create_app", "app"]
|
|
67
|
+
|
|
68
|
+
# Built SPA (vite build → src/quantized/web/). A build artifact, gitignored;
|
|
69
|
+
# present in packaged/installed runs, absent in a bare dev checkout.
|
|
70
|
+
#
|
|
71
|
+
# One resolution serves both cases: Path(__file__).parent always points at
|
|
72
|
+
# wherever the ``quantized`` package itself lives, so a PyPI/pipx install
|
|
73
|
+
# (the built wheel bundles ``web/`` alongside app.py — see
|
|
74
|
+
# [tool.hatch.build.targets.wheel] artifacts in pyproject.toml and the
|
|
75
|
+
# "build frontend first" step in README.md) and a dev checkout that has run
|
|
76
|
+
# ``npm run build`` resolve to the same relative path with no branching.
|
|
77
|
+
_WEB_DIR = Path(__file__).parent / "web"
|
|
78
|
+
|
|
79
|
+
# ── Desktop-style lifecycle (client presence over /api/ws) ──────────────────
|
|
80
|
+
# The SPA holds a WebSocket open; the status bar's connected dot reflects it.
|
|
81
|
+
# Auto-shutdown on last-tab-close is opt-in (env QZ_AUTO_SHUTDOWN=1, for the
|
|
82
|
+
# future `qz --desktop` run model) so dev/tests are never killed.
|
|
83
|
+
_clients = 0
|
|
84
|
+
_ever_connected = False
|
|
85
|
+
_AUTO_SHUTDOWN = os.environ.get("QZ_AUTO_SHUTDOWN") == "1"
|
|
86
|
+
_SHUTDOWN_GRACE_S = 1.5 # tab refresh disconnects + reconnects within ~1 s
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _origin_allowed(origin: str) -> bool:
|
|
90
|
+
"""Allow same-machine origins only (the WS upgrade bypasses CORS)."""
|
|
91
|
+
host = urlparse(origin).hostname
|
|
92
|
+
return host in {"127.0.0.1", "localhost", "::1"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _lifecycle_ws(ws: WebSocket) -> None:
|
|
96
|
+
"""Client-presence socket: count live tabs; (optionally) shut down when the
|
|
97
|
+
last one drops past a refresh-safe grace window. Module-level to keep
|
|
98
|
+
create_app simple."""
|
|
99
|
+
global _clients, _ever_connected
|
|
100
|
+
origin = ws.headers.get("origin")
|
|
101
|
+
if origin and not _origin_allowed(origin):
|
|
102
|
+
await ws.close(code=1008) # policy violation
|
|
103
|
+
return
|
|
104
|
+
await ws.accept()
|
|
105
|
+
_clients += 1
|
|
106
|
+
_ever_connected = True
|
|
107
|
+
try:
|
|
108
|
+
while True:
|
|
109
|
+
await ws.receive_text() # idles until disconnect
|
|
110
|
+
except WebSocketDisconnect:
|
|
111
|
+
pass
|
|
112
|
+
finally:
|
|
113
|
+
_clients -= 1
|
|
114
|
+
if _AUTO_SHUTDOWN and _clients == 0:
|
|
115
|
+
asyncio.get_running_loop().create_task(_grace_check())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _grace_check() -> None:
|
|
119
|
+
"""Exit unless a client reconnected within the grace window."""
|
|
120
|
+
await asyncio.sleep(_SHUTDOWN_GRACE_S)
|
|
121
|
+
if _AUTO_SHUTDOWN and _ever_connected and _clients == 0:
|
|
122
|
+
os._exit(0)
|
|
123
|
+
|
|
124
|
+
# Vite dev server origins (the SPA in --dev mode). Same-origin in production.
|
|
125
|
+
_DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_app() -> FastAPI:
|
|
129
|
+
"""Build the FastAPI app and wire the domain routers."""
|
|
130
|
+
application = FastAPI(title="quantized", version=__version__)
|
|
131
|
+
application.add_middleware(
|
|
132
|
+
CORSMiddleware,
|
|
133
|
+
allow_origins=_DEV_ORIGINS,
|
|
134
|
+
allow_methods=["*"],
|
|
135
|
+
allow_headers=["*"],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@application.get("/api/health")
|
|
139
|
+
def health() -> dict[str, str]:
|
|
140
|
+
return {"status": "ok", "version": __version__}
|
|
141
|
+
|
|
142
|
+
application.include_router(parsers.router)
|
|
143
|
+
application.include_router(books.router)
|
|
144
|
+
application.include_router(samples.router)
|
|
145
|
+
application.include_router(import_wizard.router)
|
|
146
|
+
application.include_router(import_template.router)
|
|
147
|
+
application.include_router(plot.router)
|
|
148
|
+
application.include_router(corrections.router)
|
|
149
|
+
application.include_router(fitting.router)
|
|
150
|
+
application.include_router(fitting_bumps.router)
|
|
151
|
+
application.include_router(jobs_api.router)
|
|
152
|
+
application.include_router(baseline.router)
|
|
153
|
+
application.include_router(stats.router)
|
|
154
|
+
application.include_router(stats_design.router)
|
|
155
|
+
application.include_router(statplots.router)
|
|
156
|
+
application.include_router(reference.router)
|
|
157
|
+
application.include_router(export.router)
|
|
158
|
+
application.include_router(export_figures.router)
|
|
159
|
+
application.include_router(export_facets.router)
|
|
160
|
+
application.include_router(export_page.router)
|
|
161
|
+
application.include_router(report_export.router)
|
|
162
|
+
application.include_router(magnetometry.router)
|
|
163
|
+
application.include_router(peaks.router)
|
|
164
|
+
application.include_router(reductions.router)
|
|
165
|
+
application.include_router(reflectivity.router)
|
|
166
|
+
application.include_router(rsm.router)
|
|
167
|
+
application.include_router(xray.router)
|
|
168
|
+
application.include_router(sld.router)
|
|
169
|
+
application.include_router(spectral.router)
|
|
170
|
+
application.include_router(crystallography.router)
|
|
171
|
+
application.include_router(electrical.router)
|
|
172
|
+
application.include_router(optics.router)
|
|
173
|
+
application.include_router(vacuum.router)
|
|
174
|
+
application.include_router(thermal.router)
|
|
175
|
+
application.include_router(diffusion.router)
|
|
176
|
+
application.include_router(electrochemistry.router)
|
|
177
|
+
application.include_router(substrates.router)
|
|
178
|
+
application.include_router(semiconductor.router)
|
|
179
|
+
application.include_router(thin_film.router)
|
|
180
|
+
application.include_router(superconductor.router)
|
|
181
|
+
application.include_router(magnetic.router)
|
|
182
|
+
application.include_router(aggregate.router)
|
|
183
|
+
application.include_router(calc.router)
|
|
184
|
+
|
|
185
|
+
# Client-presence WebSocket (registered before the SPA mount so the
|
|
186
|
+
# catch-all StaticFiles route never shadows it).
|
|
187
|
+
application.websocket("/api/ws")(_lifecycle_ws)
|
|
188
|
+
|
|
189
|
+
# Load user/third-party plugins once (gap #8). Registration is isolated
|
|
190
|
+
# per-plugin and logged; a broken plugin never crashes startup. Cheap when
|
|
191
|
+
# none are installed (an empty dir scan + entry-point lookup). Fit-model /
|
|
192
|
+
# parser routes read the shared registries at request time, so plugin
|
|
193
|
+
# contributions are visible regardless of this call's position.
|
|
194
|
+
try:
|
|
195
|
+
load_plugins()
|
|
196
|
+
except Exception: # pragma: no cover - load_plugins already isolates per-plugin
|
|
197
|
+
logging.getLogger("quantized.plugins").exception("plugin loading failed")
|
|
198
|
+
|
|
199
|
+
# Serve the built SPA at / when present (production / packaged runs). In a
|
|
200
|
+
# bare dev checkout the dir is absent and the Vite dev server serves the UI.
|
|
201
|
+
if _WEB_DIR.is_dir():
|
|
202
|
+
application.mount("/", StaticFiles(directory=_WEB_DIR, html=True), name="web")
|
|
203
|
+
return application
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
app = create_app()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Pure calculation library: corrections, fitting, calculators, stats, plotting math.
|
|
2
|
+
|
|
3
|
+
Pure layer — MUST NOT import fastapi / pydantic / starlette / quantized.routes
|
|
4
|
+
(enforced by tests/test_repo_integrity.py). This is where parity with the
|
|
5
|
+
MATLAB toolbox lives.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Shared Lieber-Mahadevan-Jansen iterative clip-fit loop (MAIN #8c).
|
|
2
|
+
|
|
3
|
+
:func:`quantized.calc.baseline.baseline_modpoly` and
|
|
4
|
+
:func:`quantized.calc.backgrounds.xrd_low_angle_background` run the same
|
|
5
|
+
iterative clip discipline (Lieber & Mahadevan-Jansen, Appl. Spectrosc. 57,
|
|
6
|
+
1363 (2003)): fit a model to the working signal, clamp the signal to
|
|
7
|
+
``min(signal, fit)``, and refit until the RMS change relative to the data
|
|
8
|
+
range drops below tolerance. Only the model fit differs between the two —
|
|
9
|
+
a polynomial in normalized sample index vs a linear least-squares solve on
|
|
10
|
+
a hyperbolic basis — so the loop lives here ONCE, parameterized by the fit
|
|
11
|
+
callable. The operation sequence (clip, RMS, ``rms / y_range < tol``
|
|
12
|
+
convergence test, 1-based iteration count) is verbatim from both callers:
|
|
13
|
+
``baseline_modpoly`` is golden-parity-tested against frozen MATLAB output,
|
|
14
|
+
so this loop must stay bit-identical.
|
|
15
|
+
|
|
16
|
+
Pure layer: ndarray in -> results out. No fastapi/pydantic imports.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from typing import NamedTuple
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
__all__ = ["ClipFitResult", "_iterative_clip_fit"]
|
|
29
|
+
|
|
30
|
+
# One model fit over the current working signal: returns ``(coeffs, fit)``
|
|
31
|
+
# where ``fit`` is the model evaluated on the full grid.
|
|
32
|
+
FitFn = Callable[[NDArray[np.float64]], tuple[NDArray[np.float64], NDArray[np.float64]]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClipFitResult(NamedTuple):
|
|
36
|
+
"""Final state of the clip-fit loop."""
|
|
37
|
+
|
|
38
|
+
coeffs: NDArray[np.float64]
|
|
39
|
+
fit: NDArray[np.float64]
|
|
40
|
+
n_iter: int
|
|
41
|
+
converged: bool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _iterative_clip_fit(
|
|
45
|
+
y: NDArray[np.float64],
|
|
46
|
+
fit_fn: FitFn,
|
|
47
|
+
*,
|
|
48
|
+
max_iter: int,
|
|
49
|
+
tol: float,
|
|
50
|
+
y_range: float,
|
|
51
|
+
init: tuple[NDArray[np.float64], NDArray[np.float64]],
|
|
52
|
+
) -> ClipFitResult:
|
|
53
|
+
"""Iteratively fit and clip the working signal to ``min(signal, fit)``.
|
|
54
|
+
|
|
55
|
+
Each iteration fits ``fit_fn`` to the working copy of ``y``, clamps the
|
|
56
|
+
copy to ``min(copy, fit)``, and stops (``converged=True``) once the RMS
|
|
57
|
+
of the clip step falls below ``tol * y_range``. ``init`` supplies the
|
|
58
|
+
``(coeffs, fit)`` returned when the loop body never runs (``max_iter``
|
|
59
|
+
< 1) — callers preserve their exact pre-refactor initial state through
|
|
60
|
+
it (``baseline_modpoly`` a pre-loop polynomial fit,
|
|
61
|
+
``xrd_low_angle_background`` zeros).
|
|
62
|
+
"""
|
|
63
|
+
coeffs, fit = init
|
|
64
|
+
y_work = y.copy()
|
|
65
|
+
converged = False
|
|
66
|
+
n_iter = 0
|
|
67
|
+
for it in range(1, max_iter + 1):
|
|
68
|
+
n_iter = it
|
|
69
|
+
coeffs, fit = fit_fn(y_work)
|
|
70
|
+
y_new = np.asarray(np.minimum(y_work, fit), dtype=float)
|
|
71
|
+
rms = math.sqrt(float(np.mean((y_new - y_work) ** 2)))
|
|
72
|
+
y_work = y_new
|
|
73
|
+
if rms / y_range < tol:
|
|
74
|
+
converged = True
|
|
75
|
+
break
|
|
76
|
+
return ClipFitResult(coeffs, fit, n_iter, converged)
|