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.
Files changed (233) hide show
  1. quantized/__init__.py +11 -0
  2. quantized/__main__.py +14 -0
  3. quantized/api.py +220 -0
  4. quantized/app.py +206 -0
  5. quantized/calc/__init__.py +8 -0
  6. quantized/calc/_clipfit.py +76 -0
  7. quantized/calc/_natural_neighbor.py +219 -0
  8. quantized/calc/aggregate.py +159 -0
  9. quantized/calc/backgrounds.py +353 -0
  10. quantized/calc/baseline.py +349 -0
  11. quantized/calc/batch_fit.py +148 -0
  12. quantized/calc/constants.py +27 -0
  13. quantized/calc/corrections.py +192 -0
  14. quantized/calc/crystallography.py +400 -0
  15. quantized/calc/diffusion.py +120 -0
  16. quantized/calc/electrical.py +248 -0
  17. quantized/calc/electrochemistry.py +176 -0
  18. quantized/calc/element_data.json +1 -0
  19. quantized/calc/element_data.py +59 -0
  20. quantized/calc/errors.py +246 -0
  21. quantized/calc/figure.py +417 -0
  22. quantized/calc/figure_break.py +152 -0
  23. quantized/calc/figure_categorical.py +156 -0
  24. quantized/calc/figure_corner.py +229 -0
  25. quantized/calc/figure_facets.py +127 -0
  26. quantized/calc/figure_field.py +137 -0
  27. quantized/calc/figure_hitmap.py +116 -0
  28. quantized/calc/figure_labels.py +62 -0
  29. quantized/calc/figure_map.py +287 -0
  30. quantized/calc/figure_overrides.py +159 -0
  31. quantized/calc/figure_page.py +266 -0
  32. quantized/calc/figure_scale.py +125 -0
  33. quantized/calc/figure_statplots.py +167 -0
  34. quantized/calc/figure_styles.py +131 -0
  35. quantized/calc/figure_ternary.py +239 -0
  36. quantized/calc/figure_ticks.py +217 -0
  37. quantized/calc/fit_autoguess.py +156 -0
  38. quantized/calc/fit_bootstrap.py +163 -0
  39. quantized/calc/fit_bumps.py +258 -0
  40. quantized/calc/fit_constraints.py +114 -0
  41. quantized/calc/fit_equation.py +264 -0
  42. quantized/calc/fit_findxy.py +80 -0
  43. quantized/calc/fit_models.py +195 -0
  44. quantized/calc/fit_models_special.py +189 -0
  45. quantized/calc/fit_odr.py +99 -0
  46. quantized/calc/fit_scan.py +243 -0
  47. quantized/calc/fit_stats.py +215 -0
  48. quantized/calc/fitting.py +199 -0
  49. quantized/calc/formula.py +90 -0
  50. quantized/calc/global_curve_fit.py +305 -0
  51. quantized/calc/global_fit.py +181 -0
  52. quantized/calc/interp2d.py +260 -0
  53. quantized/calc/linecut.py +269 -0
  54. quantized/calc/magnetic.py +414 -0
  55. quantized/calc/magnetometry.py +464 -0
  56. quantized/calc/map.py +228 -0
  57. quantized/calc/mcmc.py +177 -0
  58. quantized/calc/optics.py +228 -0
  59. quantized/calc/pawley.py +251 -0
  60. quantized/calc/peak_batch.py +128 -0
  61. quantized/calc/peak_fit.py +259 -0
  62. quantized/calc/peak_integrate.py +104 -0
  63. quantized/calc/peak_multifit.py +260 -0
  64. quantized/calc/peak_track.py +134 -0
  65. quantized/calc/peaks.py +298 -0
  66. quantized/calc/peakshapes.py +85 -0
  67. quantized/calc/plotting.py +147 -0
  68. quantized/calc/processing.py +232 -0
  69. quantized/calc/qspace.py +48 -0
  70. quantized/calc/reductions.py +155 -0
  71. quantized/calc/reductions_fft.py +383 -0
  72. quantized/calc/refl_sld_presets.json +1 -0
  73. quantized/calc/reflectivity.py +80 -0
  74. quantized/calc/registry.py +303 -0
  75. quantized/calc/relaxation.py +119 -0
  76. quantized/calc/report.py +253 -0
  77. quantized/calc/report_emit.py +227 -0
  78. quantized/calc/resample.py +142 -0
  79. quantized/calc/rsm.py +91 -0
  80. quantized/calc/rsm_analyze.py +245 -0
  81. quantized/calc/semiconductor.py +488 -0
  82. quantized/calc/sld.py +131 -0
  83. quantized/calc/sld_formula.py +138 -0
  84. quantized/calc/spectral.py +357 -0
  85. quantized/calc/statplots.py +214 -0
  86. quantized/calc/stats.py +399 -0
  87. quantized/calc/stats_anova2.py +202 -0
  88. quantized/calc/stats_anova_ext.py +338 -0
  89. quantized/calc/stats_dist.py +196 -0
  90. quantized/calc/stats_glm.py +245 -0
  91. quantized/calc/stats_multivar.py +289 -0
  92. quantized/calc/stats_roc.py +157 -0
  93. quantized/calc/stats_survival.py +261 -0
  94. quantized/calc/stats_tests.py +380 -0
  95. quantized/calc/substrates.py +181 -0
  96. quantized/calc/superconductor.py +359 -0
  97. quantized/calc/surface_fit.py +290 -0
  98. quantized/calc/surface_models.py +156 -0
  99. quantized/calc/thermal.py +119 -0
  100. quantized/calc/thin_film.py +425 -0
  101. quantized/calc/unit_convert.py +259 -0
  102. quantized/calc/units.py +80 -0
  103. quantized/calc/vacuum.py +290 -0
  104. quantized/calc/xray.py +169 -0
  105. quantized/cli.py +214 -0
  106. quantized/datastruct.py +153 -0
  107. quantized/io/__init__.py +11 -0
  108. quantized/io/_hdf5_layout.py +308 -0
  109. quantized/io/_jcamp_asdf.py +135 -0
  110. quantized/io/_xrdml_scan.py +291 -0
  111. quantized/io/base.py +82 -0
  112. quantized/io/bruker_brml.py +177 -0
  113. quantized/io/bruker_raw.py +158 -0
  114. quantized/io/cif.py +266 -0
  115. quantized/io/consolidated.py +122 -0
  116. quantized/io/delimited.py +222 -0
  117. quantized/io/excel.py +135 -0
  118. quantized/io/hdf5.py +192 -0
  119. quantized/io/import_filters.py +178 -0
  120. quantized/io/import_preview.py +262 -0
  121. quantized/io/jcamp.py +179 -0
  122. quantized/io/lakeshore.py +163 -0
  123. quantized/io/ncnr.py +278 -0
  124. quantized/io/netcdf.py +195 -0
  125. quantized/io/opus.py +231 -0
  126. quantized/io/origin.py +346 -0
  127. quantized/io/origin_com.py +194 -0
  128. quantized/io/origin_project/__init__.py +221 -0
  129. quantized/io/origin_project/annotation_marks.py +288 -0
  130. quantized/io/origin_project/container.py +262 -0
  131. quantized/io/origin_project/curve_style_color.py +359 -0
  132. quantized/io/origin_project/figure_geometry.py +108 -0
  133. quantized/io/origin_project/figure_layers.py +333 -0
  134. quantized/io/origin_project/figure_text.py +258 -0
  135. quantized/io/origin_project/figures.py +210 -0
  136. quantized/io/origin_project/figures_opju.py +440 -0
  137. quantized/io/origin_project/notes.py +302 -0
  138. quantized/io/origin_project/opj.py +459 -0
  139. quantized/io/origin_project/opj_curves.py +297 -0
  140. quantized/io/origin_project/opj_shapes.py +148 -0
  141. quantized/io/origin_project/opju.py +146 -0
  142. quantized/io/origin_project/opju_axis_real_form.py +418 -0
  143. quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
  144. quantized/io/origin_project/opju_codec.py +370 -0
  145. quantized/io/origin_project/opju_curves.py +497 -0
  146. quantized/io/origin_project/opju_curves_allcols.py +258 -0
  147. quantized/io/origin_project/opju_figure_curves.py +302 -0
  148. quantized/io/origin_project/opju_figure_text.py +245 -0
  149. quantized/io/origin_project/opju_reports.py +129 -0
  150. quantized/io/origin_project/origin_richtext.py +145 -0
  151. quantized/io/origin_project/preview.py +132 -0
  152. quantized/io/origin_project/templates.py +314 -0
  153. quantized/io/origin_project/tree.py +379 -0
  154. quantized/io/origin_project/tree_opju.py +228 -0
  155. quantized/io/origin_project/windows.py +238 -0
  156. quantized/io/origin_project/windows_opju.py +393 -0
  157. quantized/io/origin_project/writer.py +156 -0
  158. quantized/io/origin_project/writer_blocks.py +282 -0
  159. quantized/io/qd.py +380 -0
  160. quantized/io/refl1d.py +132 -0
  161. quantized/io/registry.py +210 -0
  162. quantized/io/report_export.py +347 -0
  163. quantized/io/rigaku.py +100 -0
  164. quantized/io/sims.py +398 -0
  165. quantized/io/spc.py +311 -0
  166. quantized/io/xrd_csv.py +308 -0
  167. quantized/io/xrdml.py +394 -0
  168. quantized/jobs.py +173 -0
  169. quantized/plugins/__init__.py +50 -0
  170. quantized/plugins/contract.py +111 -0
  171. quantized/plugins/loader.py +394 -0
  172. quantized/plugins/steps.py +90 -0
  173. quantized/routes/__init__.py +7 -0
  174. quantized/routes/_bookcache.py +62 -0
  175. quantized/routes/_export_common.py +27 -0
  176. quantized/routes/_payload.py +58 -0
  177. quantized/routes/_uploadcache.py +59 -0
  178. quantized/routes/aggregate.py +46 -0
  179. quantized/routes/baseline.py +210 -0
  180. quantized/routes/books.py +117 -0
  181. quantized/routes/calc.py +56 -0
  182. quantized/routes/corrections.py +78 -0
  183. quantized/routes/crystallography.py +80 -0
  184. quantized/routes/diffusion.py +58 -0
  185. quantized/routes/electrical.py +101 -0
  186. quantized/routes/electrochemistry.py +83 -0
  187. quantized/routes/export.py +280 -0
  188. quantized/routes/export_facets.py +83 -0
  189. quantized/routes/export_figures.py +471 -0
  190. quantized/routes/export_page.py +125 -0
  191. quantized/routes/fitting.py +379 -0
  192. quantized/routes/fitting_bumps.py +97 -0
  193. quantized/routes/import_template.py +97 -0
  194. quantized/routes/import_wizard.py +150 -0
  195. quantized/routes/jobs_api.py +59 -0
  196. quantized/routes/magnetic.py +135 -0
  197. quantized/routes/magnetometry.py +133 -0
  198. quantized/routes/optics.py +98 -0
  199. quantized/routes/parsers.py +281 -0
  200. quantized/routes/peaks.py +184 -0
  201. quantized/routes/plot.py +103 -0
  202. quantized/routes/reductions.py +121 -0
  203. quantized/routes/reference.py +58 -0
  204. quantized/routes/reflectivity.py +91 -0
  205. quantized/routes/report_export.py +119 -0
  206. quantized/routes/rsm.py +136 -0
  207. quantized/routes/samples.py +32 -0
  208. quantized/routes/semiconductor.py +207 -0
  209. quantized/routes/sld.py +42 -0
  210. quantized/routes/spectral.py +54 -0
  211. quantized/routes/statplots.py +99 -0
  212. quantized/routes/stats.py +418 -0
  213. quantized/routes/stats_design.py +321 -0
  214. quantized/routes/substrates.py +46 -0
  215. quantized/routes/superconductor.py +139 -0
  216. quantized/routes/thermal.py +57 -0
  217. quantized/routes/thin_film.py +153 -0
  218. quantized/routes/vacuum.py +113 -0
  219. quantized/routes/xray.py +32 -0
  220. quantized/samples/demo_vsm.csv +42 -0
  221. quantized/server_launch.py +251 -0
  222. quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  223. quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  224. quantized/web/assets/index-BHmmCL-x.js +27 -0
  225. quantized/web/assets/index-BiZzN7J6.css +1 -0
  226. quantized/web/index.html +13 -0
  227. quantized/web/loading.html +69 -0
  228. quantized_lab-0.8.0.dist-info/METADATA +122 -0
  229. quantized_lab-0.8.0.dist-info/RECORD +233 -0
  230. quantized_lab-0.8.0.dist-info/WHEEL +4 -0
  231. quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
  232. quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
  233. 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)