quantized-lab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,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)
|
quantized/routes/calc.py
ADDED
|
@@ -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)
|