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
@@ -0,0 +1,58 @@
1
+ """Wire-serialization helpers for the route layer.
2
+
3
+ The pure DataStruct keeps NaN/Inf (they're real in scientific data); valid wire
4
+ JSON has no representation for them, so the HTTP boundary maps non-finite floats
5
+ to ``null`` (which uPlot renders as a gap). Lives in routes/ — it's transport,
6
+ not domain.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+ from numpy.typing import NDArray
16
+
17
+ from quantized.datastruct import DataStruct
18
+
19
+ __all__ = ["jsonify", "datastruct_payload", "to_jsonable"]
20
+
21
+
22
+ def jsonify(arr: NDArray[np.float64]) -> list[Any]:
23
+ """ndarray -> JSON-safe (nested) list; non-finite floats -> ``None``."""
24
+ obj = arr.astype(object)
25
+ obj[~np.isfinite(arr)] = None
26
+ return obj.tolist() # type: ignore[no-any-return]
27
+
28
+
29
+ def to_jsonable(obj: Any) -> Any:
30
+ """Recursively make a calc result JSON-safe.
31
+
32
+ Calc functions return dicts/tuples that may nest ndarrays and non-finite
33
+ floats (real in scientific data, illegal in wire JSON). Arrays of floats go
34
+ through ``jsonify`` (non-finite -> ``None``); numpy scalars unwrap to Python
35
+ scalars; nested dicts/lists/tuples recurse. Lives in routes/ — transport.
36
+ """
37
+ if isinstance(obj, np.ndarray):
38
+ return jsonify(obj) if obj.dtype.kind == "f" else obj.tolist()
39
+ if isinstance(obj, np.generic):
40
+ obj = obj.item()
41
+ if isinstance(obj, float):
42
+ return obj if math.isfinite(obj) else None
43
+ if isinstance(obj, dict):
44
+ return {k: to_jsonable(v) for k, v in obj.items()}
45
+ if isinstance(obj, (list, tuple)):
46
+ return [to_jsonable(v) for v in obj]
47
+ return obj
48
+
49
+
50
+ def datastruct_payload(ds: DataStruct) -> dict[str, Any]:
51
+ """DataStruct -> JSON-safe dict for the import response."""
52
+ return {
53
+ "time": jsonify(ds.time),
54
+ "values": jsonify(ds.values),
55
+ "labels": list(ds.labels),
56
+ "units": list(ds.units),
57
+ "metadata": dict(ds.metadata),
58
+ }
@@ -0,0 +1,59 @@
1
+ """Bounded, on-disk staging for multi-book Origin project uploads (lazy
2
+ per-book transport, ``ORIGIN_FILE_DECODE_PLAN`` #38).
3
+
4
+ ``/api/parsers/upload``'s ordinary path stages the file in an ephemeral
5
+ ``tempfile.TemporaryDirectory`` that is deleted before the response returns --
6
+ fine for a one-shot full import, but a lazy import needs the bytes to survive
7
+ until the browser later activates a non-primary book and fetches its full
8
+ data (``routes/books.py``). Origin project uploads are staged here instead: a
9
+ stable temp path plus an opaque token the frontend echoes back on that fetch.
10
+
11
+ Bounded to the last few uploads (LRU eviction, by unlinking the oldest staged
12
+ file once the count is exceeded) -- this is a single-user desktop tool, not a
13
+ multi-tenant server, so keeping a handful of recent uploads on disk is cheap
14
+ and avoids unbounded growth from repeated re-imports.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import secrets
20
+ import tempfile
21
+ from collections import OrderedDict
22
+ from pathlib import Path
23
+
24
+ __all__ = ["stage_upload", "resolve_upload_token"]
25
+
26
+ _MAX_STAGED = 8
27
+ _root = Path(tempfile.gettempdir()) / "qz_origin_uploads"
28
+ _tokens: OrderedDict[str, Path] = OrderedDict()
29
+
30
+
31
+ def stage_upload(name: str, content: bytes) -> tuple[Path, str]:
32
+ """Persist ``content`` under a fresh token's own subdirectory (so same-name
33
+ re-uploads never collide) and return ``(path, token)``. Evicts the oldest
34
+ staged upload once more than ``_MAX_STAGED`` are held."""
35
+ _root.mkdir(parents=True, exist_ok=True)
36
+ token = secrets.token_hex(8)
37
+ staged_dir = _root / token
38
+ staged_dir.mkdir(parents=True, exist_ok=True)
39
+ dest = staged_dir / name
40
+ dest.write_bytes(content)
41
+ _tokens[token] = dest
42
+ _tokens.move_to_end(token)
43
+ while len(_tokens) > _MAX_STAGED:
44
+ _, old_path = _tokens.popitem(last=False)
45
+ old_path.unlink(missing_ok=True)
46
+ try:
47
+ old_path.parent.rmdir()
48
+ except OSError:
49
+ pass # not empty / already gone -- best-effort cleanup only
50
+ return dest, token
51
+
52
+
53
+ def resolve_upload_token(token: str) -> Path | None:
54
+ """The staged file path for ``token``, or ``None`` if unknown, expired
55
+ (evicted), or the file has since been removed out-of-band."""
56
+ path = _tokens.get(token)
57
+ if path is None or not path.is_file():
58
+ return None
59
+ return path
@@ -0,0 +1,46 @@
1
+ """Thin dataset-algebra route. Wraps ``calc.aggregate.dataset_algebra``.
2
+
3
+ Combine two posted datasets pointwise on A's x-grid (B interpolated). Validate,
4
+ call the pure golden function, serialize — no math here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from fastapi import APIRouter, HTTPException
12
+ from pydantic import BaseModel
13
+
14
+ from quantized.calc.aggregate import dataset_algebra
15
+ from quantized.datastruct import DataStruct
16
+ from quantized.routes._payload import datastruct_payload
17
+
18
+ router = APIRouter(prefix="/api/aggregate", tags=["aggregate"])
19
+
20
+
21
+ class AlgebraRequest(BaseModel):
22
+ dataset_a: dict[str, Any]
23
+ dataset_b: dict[str, Any]
24
+ operation: str
25
+ interp_method: str = "pchip"
26
+ channel_a: int = 0
27
+ channel_b: int = 0
28
+
29
+
30
+ @router.post("/algebra")
31
+ def algebra(req: AlgebraRequest) -> dict[str, Any]:
32
+ """Combine two datasets via A+B / A-B / A*B / A/B / (A-B)/(A+B)."""
33
+ try:
34
+ a = DataStruct.from_dict(req.dataset_a)
35
+ b = DataStruct.from_dict(req.dataset_b)
36
+ out = dataset_algebra(
37
+ a,
38
+ b,
39
+ req.operation,
40
+ interp_method=req.interp_method,
41
+ channel_a=req.channel_a,
42
+ channel_b=req.channel_b,
43
+ )
44
+ except (ValueError, KeyError, IndexError) as exc:
45
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
46
+ return datastruct_payload(out)
@@ -0,0 +1,210 @@
1
+ """Thin baseline routes: estimate a slowly-varying background under a signal.
2
+
3
+ Wraps ``calc.baseline`` + ``calc.backgrounds``. ALS / rolling-ball / modpoly
4
+ operate on ``y`` alone; ``estimate`` (SNIP / polynomial), Shirley, the anchor
5
+ baseline and the XRD low-angle model need ``x`` too. Methods with iteration
6
+ state also return an ``info`` dict (chosen window / iteration count / coeffs).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ from fastapi import APIRouter, HTTPException
15
+ from pydantic import BaseModel
16
+
17
+ from quantized.calc.backgrounds import (
18
+ anchor_baseline,
19
+ shirley_background,
20
+ xrd_low_angle_background,
21
+ )
22
+ from quantized.calc.baseline import (
23
+ baseline_als,
24
+ baseline_modpoly,
25
+ baseline_rolling_ball,
26
+ estimate_background,
27
+ fit_region_background,
28
+ )
29
+ from quantized.routes._payload import jsonify, to_jsonable
30
+
31
+ router = APIRouter(prefix="/api/baseline", tags=["baseline"])
32
+
33
+
34
+ class EstimateRequest(BaseModel):
35
+ x: list[float]
36
+ y: list[float]
37
+ method: str = "snip"
38
+ max_window_deg: float = 2.0
39
+ smooth_passes: int = 3
40
+ poly_degree: int = 4
41
+ iterative: bool = False
42
+ iter_max_passes: int = 3
43
+ iter_sigma: float = 3.0
44
+
45
+
46
+ class ALSRequest(BaseModel):
47
+ y: list[float]
48
+ lam: float = 1e6
49
+ p: float = 0.01
50
+ max_iter: int = 20
51
+ tol: float = 1e-6
52
+
53
+
54
+ class RollingBallRequest(BaseModel):
55
+ y: list[float]
56
+ radius: int = 100
57
+ smooth: int = -1
58
+
59
+
60
+ class ModPolyRequest(BaseModel):
61
+ y: list[float]
62
+ order: int = 5
63
+ max_iter: int = 100
64
+ tol: float = 1e-6
65
+
66
+
67
+ class RegionBackgroundRequest(BaseModel):
68
+ x: list[float]
69
+ y: list[float]
70
+ x_min: float
71
+ x_max: float
72
+ y_min: float | None = None
73
+ y_max: float | None = None
74
+ order: int = 1
75
+
76
+
77
+ class AnchorRequest(BaseModel):
78
+ x: list[float]
79
+ y: list[float]
80
+ anchors: list[list[float]] # (x, y) pairs picked on the plot
81
+ method: str = "pchip"
82
+
83
+
84
+ class ShirleyRequest(BaseModel):
85
+ x: list[float]
86
+ y: list[float]
87
+ max_iter: int = 50
88
+ tol: float = 1e-6
89
+ edge_average: int = 1
90
+
91
+
92
+ class XrdLowAngleRequest(BaseModel):
93
+ x: list[float]
94
+ y: list[float]
95
+ include_x2: bool = True
96
+ max_iter: int = 100
97
+ tol: float = 1e-6
98
+
99
+
100
+ @router.post("/estimate")
101
+ def estimate(req: EstimateRequest) -> dict[str, Any]:
102
+ """SNIP / polynomial background, optionally peak-masked and refined."""
103
+ try:
104
+ bg = estimate_background(
105
+ req.x,
106
+ req.y,
107
+ method=req.method,
108
+ max_window_deg=req.max_window_deg,
109
+ smooth_passes=req.smooth_passes,
110
+ poly_degree=req.poly_degree,
111
+ iterative=req.iterative,
112
+ iter_max_passes=req.iter_max_passes,
113
+ iter_sigma=req.iter_sigma,
114
+ )
115
+ except (ValueError, KeyError, IndexError) as exc:
116
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
117
+ return {"baseline": jsonify(bg)}
118
+
119
+
120
+ @router.post("/als")
121
+ def als(req: ALSRequest) -> dict[str, Any]:
122
+ """Asymmetric least-squares (Eilers/Whittaker) baseline."""
123
+ try:
124
+ bg = baseline_als(
125
+ np.asarray(req.y, dtype=float),
126
+ lam=req.lam,
127
+ p=req.p,
128
+ max_iter=req.max_iter,
129
+ tol=req.tol,
130
+ )
131
+ except (ValueError, KeyError, IndexError) as exc:
132
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
133
+ return {"baseline": jsonify(bg)}
134
+
135
+
136
+ @router.post("/rollingball")
137
+ def rollingball(req: RollingBallRequest) -> dict[str, Any]:
138
+ """Rolling-ball (grayscale morphological opening) baseline."""
139
+ try:
140
+ bg, info = baseline_rolling_ball(req.y, radius=req.radius, smooth=req.smooth)
141
+ except (ValueError, KeyError, IndexError) as exc:
142
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
143
+ return {"baseline": jsonify(bg), "info": to_jsonable(info)}
144
+
145
+
146
+ @router.post("/modpoly")
147
+ def modpoly(req: ModPolyRequest) -> dict[str, Any]:
148
+ """Modified-polynomial (Lieber) baseline."""
149
+ try:
150
+ bg, info = baseline_modpoly(
151
+ req.y, order=req.order, max_iter=req.max_iter, tol=req.tol
152
+ )
153
+ except (ValueError, KeyError, IndexError) as exc:
154
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
155
+ return {"baseline": jsonify(bg), "info": to_jsonable(info)}
156
+
157
+
158
+ @router.post("/anchor")
159
+ def anchor(req: AnchorRequest) -> dict[str, Any]:
160
+ """Baseline through user-picked (x, y) anchors (GOTO #2); extrapolation
161
+ clamps to the end anchors."""
162
+ try:
163
+ bg = anchor_baseline(req.x, req.y, req.anchors, method=req.method)
164
+ except (ValueError, KeyError, IndexError) as exc:
165
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
166
+ return {"baseline": jsonify(bg)}
167
+
168
+
169
+ @router.post("/shirley")
170
+ def shirley(req: ShirleyRequest) -> dict[str, Any]:
171
+ """Iterative Shirley step background (GOTO #3). Non-convergence is a 422
172
+ (the calc raises ValueError), never a 500."""
173
+ try:
174
+ bg, info = shirley_background(
175
+ req.x, req.y, max_iter=req.max_iter, tol=req.tol,
176
+ edge_average=req.edge_average,
177
+ )
178
+ except (ValueError, KeyError, IndexError) as exc:
179
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
180
+ return {"baseline": jsonify(bg), "info": to_jsonable(info)}
181
+
182
+
183
+ @router.post("/xrdlowangle")
184
+ def xrd_low_angle(req: XrdLowAngleRequest) -> dict[str, Any]:
185
+ """Hyperbolic (One_on_X) low-angle air-scatter background (GOTO #7a)."""
186
+ try:
187
+ bg, info = xrd_low_angle_background(
188
+ req.x, req.y, include_x2=req.include_x2,
189
+ max_iter=req.max_iter, tol=req.tol,
190
+ )
191
+ except (ValueError, KeyError, IndexError) as exc:
192
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
193
+ return {"baseline": jsonify(bg), "info": to_jsonable(info)}
194
+
195
+
196
+ @router.post("/region")
197
+ def region(req: RegionBackgroundRequest) -> dict[str, Any]:
198
+ """Fit a polynomial background from a boxed x/y region (BosonPlotter
199
+ "Fit BG from Box"); returns coeffs + the full-range background + region stats."""
200
+ try:
201
+ result = fit_region_background(
202
+ req.x, req.y, req.x_min, req.x_max,
203
+ y_min=req.y_min, y_max=req.y_max, order=req.order,
204
+ )
205
+ except (ValueError, KeyError, IndexError) as exc:
206
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
207
+ bg = result.pop("background")
208
+ out: dict[str, Any] = to_jsonable(result)
209
+ out["background"] = jsonify(bg)
210
+ return out
@@ -0,0 +1,117 @@
1
+ """Thin route: fetch one Origin book's full data on demand (lazy per-book
2
+ transport, ORIGIN_FILE_DECODE_PLAN #38).
3
+
4
+ ``/api/parsers/import``/``upload`` return the primary book's full data plus a
5
+ lightweight inventory + preview for every OTHER book
6
+ (``routes.parsers._import_with_books``'s default, non-``full_books`` path).
7
+ This route is where the frontend's first activation of one of those other
8
+ books fetches its full ``DataStruct``. A cache hit (the common case — the
9
+ import route already parsed the whole project and primed ``_bookcache`` with
10
+ the SAME book list) is a plain dict lookup; a miss (server restart, cache
11
+ eviction, or a book requested well after the cache's LRU bound was exceeded)
12
+ re-parses the source file once and re-primes the cache.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import struct
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from fastapi import APIRouter, HTTPException
23
+ from pydantic import BaseModel
24
+
25
+ from quantized.io.origin_project import (
26
+ OriginProjectError,
27
+ drop_empty_library_books,
28
+ read_origin_project_all,
29
+ )
30
+ from quantized.routes._bookcache import cache_project_books, get_cached_book
31
+ from quantized.routes._uploadcache import resolve_upload_token
32
+ from quantized.routes.parsers import _allowed_roots, _book_payload
33
+
34
+ router = APIRouter(prefix="/api/parsers", tags=["parsers"])
35
+
36
+
37
+ class BookDataRequest(BaseModel):
38
+ book_id: str
39
+ # Exactly one of these — mirrors the two ways a project got imported
40
+ # (routes.parsers._book_source_ref): a path /import already validated, or
41
+ # an /upload's staged-file token (_uploadcache).
42
+ path: str | None = None
43
+ token: str | None = None
44
+
45
+
46
+ def _resolve_book_path(raw_path: str) -> Path:
47
+ """The SAME realpath+commonpath containment guard as
48
+ ``routes.parsers.import_file`` (reusing its ``_allowed_roots``), kept
49
+ inline here rather than factored into a shared function that both routes
50
+ call: that function's own docstring notes the guard is deliberately
51
+ inline so static analysis (CodeQL) can see the taint→sink path sit
52
+ entirely within one function body — the same reasoning applies here.
53
+ """
54
+ try:
55
+ resolved = os.path.realpath(raw_path)
56
+ except (OSError, ValueError) as exc:
57
+ raise HTTPException(status_code=400, detail="invalid path") from exc
58
+ within_allowed = False
59
+ for root in _allowed_roots():
60
+ try:
61
+ if os.path.commonpath((root, resolved)) == root:
62
+ within_allowed = True
63
+ break
64
+ except ValueError:
65
+ continue # different drives (Windows) -> not under this root
66
+ if not within_allowed:
67
+ raise HTTPException(
68
+ status_code=403,
69
+ detail="path is outside the allowed roots (set QZ_DATA_ROOTS to widen)",
70
+ )
71
+ if not os.path.isfile(resolved):
72
+ raise HTTPException(status_code=404, detail=f"file not found: {raw_path}")
73
+ return Path(resolved)
74
+
75
+
76
+ @router.post("/books/data")
77
+ def book_data(req: BookDataRequest) -> dict[str, Any]:
78
+ """One book's full DataStruct payload, by the id + source reference an
79
+ import response's ``book_source``/lazy inventory entry gave the caller."""
80
+ if not req.book_id:
81
+ raise HTTPException(status_code=400, detail="book_id is required")
82
+ if bool(req.path) == bool(req.token):
83
+ raise HTTPException(
84
+ status_code=400, detail="exactly one of path or token is required"
85
+ )
86
+
87
+ if req.token:
88
+ resolved = resolve_upload_token(req.token)
89
+ if resolved is None:
90
+ raise HTTPException(
91
+ status_code=404,
92
+ detail="upload expired — re-import the file to fetch this book",
93
+ )
94
+ else:
95
+ assert req.path is not None
96
+ resolved = _resolve_book_path(req.path)
97
+
98
+ cached = get_cached_book(resolved, req.book_id)
99
+ if cached is not None:
100
+ return _book_payload(cached)
101
+
102
+ # Cache miss: re-parse once and re-prime the cache for any sibling books
103
+ # activated next.
104
+ try:
105
+ raw = resolved.read_bytes()
106
+ _primary, all_books = read_origin_project_all(resolved, raw=raw)
107
+ except (OriginProjectError, ValueError, KeyError, struct.error, OSError) as exc:
108
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
109
+ books = drop_empty_library_books(all_books)
110
+ cache_project_books(resolved, books)
111
+ match = next(
112
+ (b for b in books if str(b.metadata.get("origin_book", "")) == req.book_id),
113
+ None,
114
+ )
115
+ if match is None:
116
+ raise HTTPException(status_code=404, detail=f"book not found: {req.book_id}")
117
+ return _book_payload(match)
@@ -0,0 +1,56 @@
1
+ """Thin headless-calculator routes — an HTTP surface for the calc registry.
2
+
3
+ Exposes the same discoverable, name-addressed calculator catalog as
4
+ ``calc.registry`` (the DiraCulator headless API) over HTTP: list the operations,
5
+ describe one's parameters, or invoke one by name with a params dict. Validate →
6
+ call the pure registry → JSON-serialize. No business logic here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from fastapi import APIRouter, HTTPException
14
+ from pydantic import BaseModel, Field
15
+
16
+ from quantized.calc.registry import call_calculator, describe_calculator, list_calculators
17
+ from quantized.routes._payload import to_jsonable
18
+
19
+ router = APIRouter(prefix="/api/calc", tags=["calc"])
20
+
21
+
22
+ class CallRequest(BaseModel):
23
+ name: str
24
+ params: dict[str, Any] = Field(default_factory=dict)
25
+
26
+
27
+ def _detail(exc: Exception) -> str:
28
+ """Clean message text (KeyError.__str__ wraps its arg in quotes)."""
29
+ return str(exc.args[0]) if exc.args else str(exc)
30
+
31
+
32
+ @router.get("/catalog")
33
+ def catalog(domain: str | None = None) -> dict[str, Any]:
34
+ """List calculator operations (``{name, domain, summary}``); optionally one domain."""
35
+ return {"calculators": list_calculators(domain)}
36
+
37
+
38
+ @router.get("/describe/{name}")
39
+ def describe(name: str) -> dict[str, Any]:
40
+ """Describe one operation: name, domain, summary, and its signature params."""
41
+ try:
42
+ return describe_calculator(name)
43
+ except KeyError as exc:
44
+ raise HTTPException(status_code=404, detail=_detail(exc)) from exc
45
+
46
+
47
+ @router.post("/call")
48
+ def call(req: CallRequest) -> dict[str, Any]:
49
+ """Invoke a calculator by name with a params dict; JSON-serialize the result."""
50
+ try:
51
+ result = call_calculator(req.name, req.params)
52
+ except KeyError as exc:
53
+ raise HTTPException(status_code=404, detail=_detail(exc)) from exc
54
+ except ValueError as exc:
55
+ raise HTTPException(status_code=422, detail=_detail(exc)) from exc
56
+ return {"name": req.name, "result": to_jsonable(result)}
@@ -0,0 +1,78 @@
1
+ """Thin corrections route: DataStruct + params -> corrected DataStruct.
2
+
3
+ Validate, call ``calc.corrections.apply_corrections`` (the pure 8-step pipeline),
4
+ serialize. No algorithms here — the math lives in calc/. The request ``params``
5
+ mirror the MATLAB ``correctionParams`` struct (camelCase on the wire); the typed
6
+ model below is the API contract and is dumped back to that exact key set.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from fastapi import APIRouter, HTTPException
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+ from quantized.calc.corrections import apply_corrections
17
+ from quantized.datastruct import DataStruct
18
+ from quantized.routes._payload import datastruct_payload
19
+
20
+ router = APIRouter(prefix="/api/corrections", tags=["corrections"])
21
+
22
+
23
+ class CorrectionParams(BaseModel):
24
+ """Correction-pipeline parameters (all optional; mirror MATLAB ``params``).
25
+
26
+ Wire names are camelCase (aliases); unset fields are dropped so the calc
27
+ layer falls back to its own defaults (e.g. NaN ``xTrim*`` = no trim).
28
+ """
29
+
30
+ model_config = ConfigDict(populate_by_name=True)
31
+
32
+ x_off: float | None = Field(default=None, alias="xOff")
33
+ y_off: float | None = Field(default=None, alias="yOff")
34
+ bg_slope: float | None = Field(default=None, alias="bgSlope")
35
+ bg_int: float | None = Field(default=None, alias="bgInt")
36
+ bg_poly: list[float] | None = Field(default=None, alias="bgPoly")
37
+ x_trim_min: float | None = Field(default=None, alias="xTrimMin")
38
+ x_trim_max: float | None = Field(default=None, alias="xTrimMax")
39
+ is_neutron: bool | None = Field(default=None, alias="isNeutron")
40
+ is_mag: bool | None = Field(default=None, alias="isMag")
41
+ field_unit: str | None = Field(default=None, alias="fieldUnit")
42
+ moment_unit: str | None = Field(default=None, alias="momentUnit")
43
+ sample_mass: float | None = Field(default=None, alias="sampleMass")
44
+ sample_volume: float | None = Field(default=None, alias="sampleVolume")
45
+ smooth_enabled: bool | None = Field(default=None, alias="smoothEnabled")
46
+ smooth_window: int | None = Field(default=None, alias="smoothWindow")
47
+ smooth_method: str | None = Field(default=None, alias="smoothMethod")
48
+ norm_method: str | None = Field(default=None, alias="normMethod")
49
+ derivative_mode: str | None = Field(default=None, alias="derivativeMode")
50
+ # GOTO additions (new features beyond MATLAB parity):
51
+ # #2 anchor-point baseline subtraction ((x, y) pairs + interp method).
52
+ bg_anchors: list[list[float]] | None = Field(default=None, alias="bgAnchors")
53
+ bg_anchor_method: str | None = Field(default=None, alias="bgAnchorMethod")
54
+ # #7b XRR/NR beam-footprint correction (beam width / sample length share
55
+ # one length unit; footprintTwoTheta reads x as the detector angle 2theta).
56
+ footprint_w: float | None = Field(default=None, alias="footprintW")
57
+ footprint_l: float | None = Field(default=None, alias="footprintL")
58
+ footprint_two_theta: bool | None = Field(default=None, alias="footprintTwoTheta")
59
+
60
+
61
+ class CorrectionsRequest(BaseModel):
62
+ dataset: dict[str, Any]
63
+ params: CorrectionParams = Field(default_factory=CorrectionParams)
64
+ bg_dataset: dict[str, Any] | None = None
65
+ bg_interp: str = "linear"
66
+
67
+
68
+ @router.post("/apply")
69
+ def apply(req: CorrectionsRequest) -> dict[str, Any]:
70
+ """Apply the correction pipeline to a posted DataStruct."""
71
+ try:
72
+ ds = DataStruct.from_dict(req.dataset)
73
+ bg = DataStruct.from_dict(req.bg_dataset) if req.bg_dataset else None
74
+ params = req.params.model_dump(by_alias=True, exclude_none=True)
75
+ out = apply_corrections(ds, params, bg_dataset=bg, bg_interp=req.bg_interp)
76
+ except (ValueError, KeyError, IndexError) as exc:
77
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
78
+ return datastruct_payload(out)