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
|
+
"""Thin reference-data routes: physical constants, element table, unit convert.
|
|
2
|
+
|
|
3
|
+
Wraps the finished W4 backend helpers (``calc.constants``, ``calc.element_data``,
|
|
4
|
+
``calc.unit_convert``). Pure lookups + a unit-expression converter; no logic 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, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from quantized.calc.constants import constants
|
|
15
|
+
from quantized.calc.element_data import by_symbol, element_data
|
|
16
|
+
from quantized.calc.unit_convert import unit_convert
|
|
17
|
+
from quantized.routes._payload import to_jsonable
|
|
18
|
+
|
|
19
|
+
router = APIRouter(prefix="/api/reference", tags=["reference"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConvertRequest(BaseModel):
|
|
23
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
24
|
+
|
|
25
|
+
value: float | list[float]
|
|
26
|
+
from_unit: str = Field(alias="from")
|
|
27
|
+
to_unit: str = Field(alias="to")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("/constants")
|
|
31
|
+
def get_constants() -> dict[str, Any]:
|
|
32
|
+
"""CODATA physical constants (name -> value)."""
|
|
33
|
+
return {"constants": to_jsonable(constants())}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.get("/elements")
|
|
37
|
+
def get_elements() -> dict[str, Any]:
|
|
38
|
+
"""The full 118-element table."""
|
|
39
|
+
return {"elements": to_jsonable(element_data())}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/elements/{symbol}")
|
|
43
|
+
def get_element(symbol: str) -> dict[str, Any]:
|
|
44
|
+
"""One element by symbol (e.g. ``Fe``)."""
|
|
45
|
+
try:
|
|
46
|
+
return to_jsonable(by_symbol(symbol)) # type: ignore[no-any-return]
|
|
47
|
+
except ValueError as exc:
|
|
48
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post("/convert")
|
|
52
|
+
def convert(req: ConvertRequest) -> dict[str, Any]:
|
|
53
|
+
"""Convert a value between unit expressions (e.g. ``Oe`` -> ``T``)."""
|
|
54
|
+
try:
|
|
55
|
+
result, info = unit_convert(req.value, req.from_unit, req.to_unit)
|
|
56
|
+
except (ValueError, KeyError) as exc:
|
|
57
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
58
|
+
return {"result": to_jsonable(result), "info": to_jsonable(info)}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Thin specular-reflectivity routes.
|
|
2
|
+
|
|
3
|
+
Wraps the finished W3 calc helpers (``calc.reflectivity.parratt_refl`` — golden vs
|
|
4
|
+
MATLAB parrattRefl — and ``calc.sld`` SLD profile / presets). The route builds the
|
|
5
|
+
Q grid, validates the layer stack, calls the pure functions, and serializes. No
|
|
6
|
+
physics here; the recursion + Névot-Croce roughness live in ``calc/``.
|
|
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, Field
|
|
16
|
+
|
|
17
|
+
from quantized.calc.reflectivity import parratt_refl
|
|
18
|
+
from quantized.calc.sld import refl_sld_presets, sld_profile
|
|
19
|
+
from quantized.routes._payload import to_jsonable
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/api/reflectivity", tags=["reflectivity"])
|
|
22
|
+
|
|
23
|
+
# A layer row is [thickness Å, SLD_real Å⁻², SLD_imag Å⁻², roughness Å].
|
|
24
|
+
Layer = list[float]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimulateRequest(BaseModel):
|
|
28
|
+
"""Simulate R(Q) from a layer stack over a linear Q grid."""
|
|
29
|
+
|
|
30
|
+
layers: list[Layer] = Field(min_length=2)
|
|
31
|
+
q_min: float = Field(default=0.005, gt=0.0)
|
|
32
|
+
q_max: float = Field(default=0.25, gt=0.0)
|
|
33
|
+
n_points: int = Field(default=400, ge=2, le=20000)
|
|
34
|
+
roughness: bool = True
|
|
35
|
+
scale: float = 1.0
|
|
36
|
+
background: float = 0.0
|
|
37
|
+
resolution: float | None = None # dQ/Q (constant relative resolution)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SldProfileRequest(BaseModel):
|
|
41
|
+
"""SLD(z) depth profile from a layer stack (error-function interfaces)."""
|
|
42
|
+
|
|
43
|
+
layers: list[Layer] = Field(min_length=2)
|
|
44
|
+
n_points: int = Field(default=500, ge=2, le=20000)
|
|
45
|
+
padding: float = Field(default=50.0, ge=0.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_layers(layers: list[Layer]) -> None:
|
|
49
|
+
if any(len(row) != 4 for row in layers):
|
|
50
|
+
raise HTTPException(
|
|
51
|
+
status_code=422,
|
|
52
|
+
detail="each layer must be [thickness, sld_real, sld_imag, roughness]",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("/presets")
|
|
57
|
+
def get_presets() -> dict[str, Any]:
|
|
58
|
+
"""Material SLD presets (name/formula/sldX/sldN/sldImag/density)."""
|
|
59
|
+
return {"presets": to_jsonable(refl_sld_presets())}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("/simulate")
|
|
63
|
+
def simulate(req: SimulateRequest) -> dict[str, Any]:
|
|
64
|
+
"""Specular reflectivity R(Q) for the layer stack over [q_min, q_max]."""
|
|
65
|
+
_validate_layers(req.layers)
|
|
66
|
+
if req.q_max <= req.q_min:
|
|
67
|
+
raise HTTPException(status_code=422, detail="q_max must exceed q_min")
|
|
68
|
+
q = np.linspace(req.q_min, req.q_max, req.n_points)
|
|
69
|
+
try:
|
|
70
|
+
r = parratt_refl(
|
|
71
|
+
q,
|
|
72
|
+
req.layers,
|
|
73
|
+
roughness=req.roughness,
|
|
74
|
+
scale=req.scale,
|
|
75
|
+
background=req.background,
|
|
76
|
+
resolution=req.resolution,
|
|
77
|
+
)
|
|
78
|
+
except (ValueError, IndexError) as exc:
|
|
79
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
80
|
+
return {"q": to_jsonable(q), "r": to_jsonable(r)}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post("/sld-profile")
|
|
84
|
+
def sld_profile_route(req: SldProfileRequest) -> dict[str, Any]:
|
|
85
|
+
"""SLD(z) depth profile (error-function interfaces) for the layer stack."""
|
|
86
|
+
_validate_layers(req.layers)
|
|
87
|
+
try:
|
|
88
|
+
z, sld = sld_profile(req.layers, n_points=req.n_points, padding=req.padding)
|
|
89
|
+
except (ValueError, IndexError) as exc:
|
|
90
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
91
|
+
return {"z": to_jsonable(z), "sld": to_jsonable(sld)}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Thin routes: emit + render report sheets (#36/#37/#38).
|
|
2
|
+
|
|
3
|
+
``/emit`` maps an analysis result dict onto the #36 schema via the pure
|
|
4
|
+
``calc.report_emit`` emitters (one emission source of truth — the frontend
|
|
5
|
+
never re-shapes results itself). ``/export`` validates a posted report and
|
|
6
|
+
streams the rendered LaTeX / HTML / Word / PowerPoint file back as an
|
|
7
|
+
attachment. Missing optional office libraries surface as 501; unknown
|
|
8
|
+
formats/kinds as 422.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, HTTPException, Response
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from quantized.calc import report_emit
|
|
21
|
+
from quantized.calc.report import ReportSheet, validate_report
|
|
22
|
+
from quantized.io.report_export import ReportExportError, render_report
|
|
23
|
+
|
|
24
|
+
router = APIRouter(prefix="/api/report", tags=["report"])
|
|
25
|
+
|
|
26
|
+
_EXT = {"latex": ".tex", "html": ".html", "docx": ".docx", "pptx": ".pptx"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _safe_name(name: str, ext: str) -> str:
|
|
30
|
+
stem = re.sub(r"[^A-Za-z0-9._-]", "_", name).strip("._") or "report"
|
|
31
|
+
return stem if stem.endswith(ext) else stem + ext
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ReportEmitRequest(BaseModel):
|
|
35
|
+
"""An analysis result + which emitter should shape it into a report."""
|
|
36
|
+
|
|
37
|
+
kind: str # curve_fit | multipeak_fit | integrate | batch_integrate | anova | stats_table
|
|
38
|
+
result: dict[str, Any] | None = None
|
|
39
|
+
records: list[dict[str, Any]] | None = None # stats_table input
|
|
40
|
+
title: str | None = None
|
|
41
|
+
model_name: str | None = None
|
|
42
|
+
param_names: list[str] | None = None
|
|
43
|
+
param_units: list[str] | None = None
|
|
44
|
+
columns: list[str] | None = None
|
|
45
|
+
caption: str | None = None
|
|
46
|
+
source_refs: list[dict[str, Any]] = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _emit_sheet(req: ReportEmitRequest) -> ReportSheet:
|
|
50
|
+
"""Dispatch to the matching pure emitter (no eval — explicit table)."""
|
|
51
|
+
kind = req.kind
|
|
52
|
+
refs = req.source_refs
|
|
53
|
+
if kind == "stats_table":
|
|
54
|
+
if not req.records:
|
|
55
|
+
raise ValueError("stats_table needs non-empty 'records'")
|
|
56
|
+
return report_emit.from_stats_table(
|
|
57
|
+
req.records, title=req.title or "Statistics",
|
|
58
|
+
columns=req.columns, caption=req.caption, source_refs=refs,
|
|
59
|
+
)
|
|
60
|
+
if req.result is None:
|
|
61
|
+
raise ValueError(f"kind {kind!r} needs a 'result' object")
|
|
62
|
+
if kind == "curve_fit":
|
|
63
|
+
return report_emit.from_curve_fit(
|
|
64
|
+
req.result, param_names=req.param_names or [],
|
|
65
|
+
param_units=req.param_units, title=req.title or "Curve fit",
|
|
66
|
+
model_name=req.model_name, source_refs=refs,
|
|
67
|
+
)
|
|
68
|
+
simple = {
|
|
69
|
+
"multipeak_fit": report_emit.from_multipeak_fit,
|
|
70
|
+
"integrate": report_emit.from_integrate,
|
|
71
|
+
"batch_integrate": report_emit.from_batch_integrate,
|
|
72
|
+
"anova": report_emit.from_anova,
|
|
73
|
+
}
|
|
74
|
+
if kind not in simple:
|
|
75
|
+
raise ValueError(f"unknown report kind {kind!r}")
|
|
76
|
+
kwargs: dict[str, Any] = {"source_refs": refs}
|
|
77
|
+
if req.title:
|
|
78
|
+
kwargs["title"] = req.title
|
|
79
|
+
return simple[kind](req.result, **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.post("/emit")
|
|
83
|
+
def emit_report(req: ReportEmitRequest) -> dict[str, Any]:
|
|
84
|
+
"""Result dict + kind -> a validated #36 report sheet (JSON)."""
|
|
85
|
+
try:
|
|
86
|
+
sheet = _emit_sheet(req)
|
|
87
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
88
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
89
|
+
payload = sheet.to_dict()
|
|
90
|
+
# calc stays deterministic/pure; the route stamps the creation time.
|
|
91
|
+
payload["created"] = datetime.now(UTC).isoformat(timespec="seconds")
|
|
92
|
+
return {"report": payload}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ReportExportRequest(BaseModel):
|
|
96
|
+
report: dict[str, Any]
|
|
97
|
+
format: str = "html"
|
|
98
|
+
filename: str = "report"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.post("/export")
|
|
102
|
+
def export_report(req: ReportExportRequest) -> Response:
|
|
103
|
+
"""Report dict + format -> downloadable file (.tex/.html/.docx/.pptx)."""
|
|
104
|
+
try:
|
|
105
|
+
validate_report(req.report)
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
108
|
+
try:
|
|
109
|
+
data, mime, _is_text = render_report(req.report, req.format)
|
|
110
|
+
except ReportExportError as exc:
|
|
111
|
+
# unknown format -> 422; a missing optional office lib -> 501
|
|
112
|
+
code = 501 if "needs" in str(exc) else 422
|
|
113
|
+
raise HTTPException(status_code=code, detail=str(exc)) from exc
|
|
114
|
+
filename = _safe_name(req.filename, _EXT.get(req.format, ""))
|
|
115
|
+
return Response(
|
|
116
|
+
content=data,
|
|
117
|
+
media_type=mime,
|
|
118
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
119
|
+
)
|
quantized/routes/rsm.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Thin RSM route: substrate/film reciprocal-space peaks -> strain + relaxation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from quantized.calc.linecut import cut_segment, line_cut, projection
|
|
11
|
+
from quantized.calc.rsm import rsm_strain
|
|
12
|
+
from quantized.calc.rsm_analyze import rsm_analyze, rsm_grids_from_datastruct
|
|
13
|
+
from quantized.datastruct import DataStruct
|
|
14
|
+
from quantized.routes._payload import datastruct_payload, to_jsonable
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/rsm", tags=["rsm"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StrainRequest(BaseModel):
|
|
20
|
+
"""Reciprocal-space peak centres ``(Qx, Qz)`` in Ang^-1."""
|
|
21
|
+
|
|
22
|
+
q_sub: tuple[float, float]
|
|
23
|
+
q_film: tuple[float, float]
|
|
24
|
+
bulk: tuple[float, float] | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.post("/strain")
|
|
28
|
+
def strain(req: StrainRequest) -> dict[str, Any]:
|
|
29
|
+
"""In-plane / out-of-plane strain + relaxation from an RSM peak pair."""
|
|
30
|
+
try:
|
|
31
|
+
result = rsm_strain(req.q_sub, req.q_film, bulk=req.bulk)
|
|
32
|
+
except ValueError as exc:
|
|
33
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
34
|
+
# NaN (symmetric reflection / no bulk) -> null for valid wire JSON.
|
|
35
|
+
return to_jsonable(result) # type: ignore[no-any-return]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AnalyzeRequest(BaseModel):
|
|
39
|
+
"""A scattered 2D RSM dataset (from the XRDML 2D parser) + detection options."""
|
|
40
|
+
|
|
41
|
+
dataset: dict[str, Any]
|
|
42
|
+
n_peaks: int = Field(default=2, ge=1, le=20)
|
|
43
|
+
threshold: float = Field(default=0.01, ge=0.0)
|
|
44
|
+
smooth_sigma: float = Field(default=1.5, gt=0.0)
|
|
45
|
+
min_separation: int = Field(default=4, ge=1)
|
|
46
|
+
fit_window: int = Field(default=6, ge=1)
|
|
47
|
+
fit_model: str = "2D Gaussian"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.post("/analyze")
|
|
51
|
+
def analyze(req: AnalyzeRequest) -> dict[str, Any]:
|
|
52
|
+
"""Extract + fit peaks from a 2D RSM dataset (centres/FWHM in angle + Q-space)."""
|
|
53
|
+
try:
|
|
54
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
55
|
+
grids = rsm_grids_from_datastruct(ds)
|
|
56
|
+
result = rsm_analyze(
|
|
57
|
+
grids["intensity"],
|
|
58
|
+
grids["axis1"],
|
|
59
|
+
grids["axis2"],
|
|
60
|
+
qx=grids["qx"],
|
|
61
|
+
qz=grids["qz"],
|
|
62
|
+
n_peaks=req.n_peaks,
|
|
63
|
+
threshold=req.threshold,
|
|
64
|
+
smooth_sigma=req.smooth_sigma,
|
|
65
|
+
min_separation=req.min_separation,
|
|
66
|
+
fit_window=req.fit_window,
|
|
67
|
+
fit_model=req.fit_model,
|
|
68
|
+
intensity_unit=grids["intensity_unit"],
|
|
69
|
+
)
|
|
70
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
71
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
72
|
+
return to_jsonable(result) # type: ignore[no-any-return]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LineCutRequest(BaseModel):
|
|
76
|
+
"""Fixed-axis cut through a 2-D map dataset (H/V, angular or Q space)."""
|
|
77
|
+
|
|
78
|
+
dataset: dict[str, Any]
|
|
79
|
+
direction: str # "h" | "v"
|
|
80
|
+
value: float
|
|
81
|
+
space: str = "angular"
|
|
82
|
+
width: float = Field(default=0.0, ge=0.0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.post("/linecut")
|
|
86
|
+
def linecut(req: LineCutRequest) -> dict[str, Any]:
|
|
87
|
+
"""H/V line cut -> a 1-D DataStruct (width>0 averages a swath)."""
|
|
88
|
+
try:
|
|
89
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
90
|
+
out = line_cut(
|
|
91
|
+
ds, direction=req.direction, value=req.value, space=req.space, width=req.width
|
|
92
|
+
)
|
|
93
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
94
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
95
|
+
return datastruct_payload(out)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CutSegmentRequest(BaseModel):
|
|
99
|
+
"""Arbitrary straight cut p0 -> p1 through the scattered 2-D cloud."""
|
|
100
|
+
|
|
101
|
+
dataset: dict[str, Any]
|
|
102
|
+
p0: tuple[float, float]
|
|
103
|
+
p1: tuple[float, float]
|
|
104
|
+
n: int = Field(default=200, ge=2, le=1_000_000)
|
|
105
|
+
width: float = Field(default=0.0, ge=0.0)
|
|
106
|
+
space: str = "angular"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.post("/cut-segment")
|
|
110
|
+
def cut_segment_route(req: CutSegmentRequest) -> dict[str, Any]:
|
|
111
|
+
"""Arbitrary-angle segment cut -> a distance-parametrized 1-D DataStruct."""
|
|
112
|
+
try:
|
|
113
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
114
|
+
out = cut_segment(ds, p0=req.p0, p1=req.p1, n=req.n, width=req.width, space=req.space)
|
|
115
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
116
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
117
|
+
return datastruct_payload(out)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ProjectionRequest(BaseModel):
|
|
121
|
+
"""Integrate the whole map onto one axis."""
|
|
122
|
+
|
|
123
|
+
dataset: dict[str, Any]
|
|
124
|
+
axis: str = "pixels" # "pixels" (I vs 2theta) | "frames" (I vs omega)
|
|
125
|
+
space: str = "angular"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.post("/projection")
|
|
129
|
+
def projection_route(req: ProjectionRequest) -> dict[str, Any]:
|
|
130
|
+
"""Full-map integrated profile -> a 1-D DataStruct."""
|
|
131
|
+
try:
|
|
132
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
133
|
+
out = projection(ds, axis=req.axis, space=req.space)
|
|
134
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
135
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
136
|
+
return datastruct_payload(out)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Thin route serving the bundled first-run demo dataset.
|
|
2
|
+
|
|
3
|
+
A tiny synthetic hysteresis-loop CSV (``samples/demo_vsm.csv`` — generated,
|
|
4
|
+
never real instrument data) ships inside the package so a fresh install has
|
|
5
|
+
something to plot with zero setup. :func:`quantized.io.import_auto` parses it
|
|
6
|
+
through the exact same path as any user file, so the response is an ordinary
|
|
7
|
+
DataStruct import payload, not a special-cased shape.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter, HTTPException
|
|
16
|
+
|
|
17
|
+
from quantized.io import import_auto
|
|
18
|
+
from quantized.routes._payload import datastruct_payload
|
|
19
|
+
|
|
20
|
+
router = APIRouter(prefix="/api/samples", tags=["samples"])
|
|
21
|
+
|
|
22
|
+
_DEMO_FILE = Path(__file__).resolve().parent.parent / "samples" / "demo_vsm.csv"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/demo")
|
|
26
|
+
def get_demo() -> dict[str, Any]:
|
|
27
|
+
"""The bundled first-run demo dataset (a synthetic VSM-like hysteresis loop)."""
|
|
28
|
+
if not _DEMO_FILE.is_file():
|
|
29
|
+
# Only possible on a broken install (samples/ stripped from the
|
|
30
|
+
# wheel) — fail loudly instead of silently returning nothing.
|
|
31
|
+
raise HTTPException(status_code=500, detail=f"demo sample missing: {_DEMO_FILE.name}")
|
|
32
|
+
return datastruct_payload(import_auto(_DEMO_FILE))
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Thin semiconductor device-physics routes. Wraps ``calc.semiconductor`` (pure
|
|
2
|
+
formulas): intrinsic carrier concentration / carrier concentrations / depletion
|
|
3
|
+
width / diffusion coefficient + length / Fermi level / Debye length / built-in
|
|
4
|
+
potential / sheet carrier density / thermal velocity / Hall coefficient /
|
|
5
|
+
mobility model. Validate -> call the pure fn -> serialize.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter, HTTPException
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from quantized.calc import semiconductor
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/api/semiconductor", tags=["semiconductor"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IntrinsicRequest(BaseModel):
|
|
22
|
+
eg: float | None = None
|
|
23
|
+
me_star: float | None = None
|
|
24
|
+
mh_star: float | None = None
|
|
25
|
+
t: float = 300.0
|
|
26
|
+
material: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CarrierConcRequest(BaseModel):
|
|
30
|
+
nd: float
|
|
31
|
+
na: float
|
|
32
|
+
ni: float
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BuiltInPotentialRequest(BaseModel):
|
|
36
|
+
na: float
|
|
37
|
+
nd: float
|
|
38
|
+
ni: float
|
|
39
|
+
t: float = 300.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DepletionWidthRequest(BaseModel):
|
|
43
|
+
vbi: float
|
|
44
|
+
na: float
|
|
45
|
+
nd: float
|
|
46
|
+
epsilon_r: float | None = None
|
|
47
|
+
material: str | None = None
|
|
48
|
+
t: float = 300.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DiffusionCoeffRequest(BaseModel):
|
|
52
|
+
mu: float
|
|
53
|
+
t: float = 300.0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DiffusionLengthRequest(BaseModel):
|
|
57
|
+
d: float
|
|
58
|
+
tau: float
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FermiLevelRequest(BaseModel):
|
|
62
|
+
eg: float | None = None
|
|
63
|
+
me_star: float | None = None
|
|
64
|
+
mh_star: float | None = None
|
|
65
|
+
nd: float = 0.0
|
|
66
|
+
na: float = 0.0
|
|
67
|
+
t: float = 300.0
|
|
68
|
+
material: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DebyeLengthRequest(BaseModel):
|
|
72
|
+
n: float
|
|
73
|
+
epsilon_r: float | None = None
|
|
74
|
+
t: float = 300.0
|
|
75
|
+
material: str | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SheetCarrierRequest(BaseModel):
|
|
79
|
+
n: float
|
|
80
|
+
t: float
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ThermalVelocityRequest(BaseModel):
|
|
84
|
+
m_star: float
|
|
85
|
+
t: float = 300.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class HallCoefficientRequest(BaseModel):
|
|
89
|
+
n: float
|
|
90
|
+
p: float
|
|
91
|
+
mu_e: float
|
|
92
|
+
mu_h: float
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class MobilityModelRequest(BaseModel):
|
|
96
|
+
material: str = "Si"
|
|
97
|
+
t: float = 300.0
|
|
98
|
+
n: float = 0.0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
102
|
+
try:
|
|
103
|
+
return fn(*args, **kwargs)
|
|
104
|
+
except ValueError as exc:
|
|
105
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.get("/materials")
|
|
109
|
+
def materials() -> dict[str, Any]:
|
|
110
|
+
"""Material-parameter presets (Eg, εᵣ, mₑ*, m_h*)."""
|
|
111
|
+
return {"materials": semiconductor.material_presets()}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.post("/intrinsic")
|
|
115
|
+
def intrinsic(req: IntrinsicRequest) -> dict[str, Any]:
|
|
116
|
+
"""n_i = √(N_c N_v)·exp(−E_g/2k_BT) (cm⁻³)."""
|
|
117
|
+
return _call(
|
|
118
|
+
semiconductor.intrinsic_carrier_conc,
|
|
119
|
+
req.eg,
|
|
120
|
+
req.me_star,
|
|
121
|
+
req.mh_star,
|
|
122
|
+
req.t,
|
|
123
|
+
req.material,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.post("/carrier-concentration")
|
|
128
|
+
def carrier_concentration(req: CarrierConcRequest) -> dict[str, Any]:
|
|
129
|
+
"""n, p from charge-neutrality + mass-action; doping type."""
|
|
130
|
+
return _call(semiconductor.carrier_concentration, req.nd, req.na, req.ni)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@router.post("/built-in-potential")
|
|
134
|
+
def built_in_potential(req: BuiltInPotentialRequest) -> dict[str, Any]:
|
|
135
|
+
"""V_bi = (k_BT/q)·ln(N_a N_d / n_i²) (V)."""
|
|
136
|
+
return _call(semiconductor.built_in_potential, req.na, req.nd, req.ni, req.t)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.post("/depletion-width")
|
|
140
|
+
def depletion_width(req: DepletionWidthRequest) -> dict[str, Any]:
|
|
141
|
+
"""Depletion width W, x_n, x_p (nm)."""
|
|
142
|
+
return _call(
|
|
143
|
+
semiconductor.depletion_width,
|
|
144
|
+
req.vbi,
|
|
145
|
+
req.na,
|
|
146
|
+
req.nd,
|
|
147
|
+
req.epsilon_r,
|
|
148
|
+
req.material,
|
|
149
|
+
req.t,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@router.post("/diffusion-coeff")
|
|
154
|
+
def diffusion_coeff(req: DiffusionCoeffRequest) -> dict[str, Any]:
|
|
155
|
+
"""D = μ·k_BT/q (cm²/s)."""
|
|
156
|
+
return _call(semiconductor.diffusion_coeff, req.mu, req.t)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@router.post("/diffusion-length")
|
|
160
|
+
def diffusion_length(req: DiffusionLengthRequest) -> dict[str, Any]:
|
|
161
|
+
"""L = √(D·τ) (cm / µm)."""
|
|
162
|
+
return _call(semiconductor.diffusion_length, req.d, req.tau)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@router.post("/fermi-level")
|
|
166
|
+
def fermi_level(req: FermiLevelRequest) -> dict[str, Any]:
|
|
167
|
+
"""E_F − E_i = k_BT·asinh(Δ/2n_i) (eV)."""
|
|
168
|
+
return _call(
|
|
169
|
+
semiconductor.fermi_level,
|
|
170
|
+
req.eg,
|
|
171
|
+
req.me_star,
|
|
172
|
+
req.mh_star,
|
|
173
|
+
req.nd,
|
|
174
|
+
req.na,
|
|
175
|
+
req.t,
|
|
176
|
+
req.material,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.post("/debye-length")
|
|
181
|
+
def debye_length(req: DebyeLengthRequest) -> dict[str, Any]:
|
|
182
|
+
"""L_D = √(ε₀εᵣk_BT/(q²n)) (nm)."""
|
|
183
|
+
return _call(semiconductor.debye_length, req.n, req.epsilon_r, req.t, req.material)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.post("/sheet-carrier-density")
|
|
187
|
+
def sheet_carrier_density(req: SheetCarrierRequest) -> dict[str, Any]:
|
|
188
|
+
"""n_s = n·t (cm⁻²)."""
|
|
189
|
+
return _call(semiconductor.sheet_carrier_density, req.n, req.t)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.post("/thermal-velocity")
|
|
193
|
+
def thermal_velocity(req: ThermalVelocityRequest) -> dict[str, Any]:
|
|
194
|
+
"""v_th = √(3k_BT/(m* m₀)) (cm/s)."""
|
|
195
|
+
return _call(semiconductor.thermal_velocity, req.m_star, req.t)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@router.post("/hall-coefficient")
|
|
199
|
+
def hall_coefficient(req: HallCoefficientRequest) -> dict[str, Any]:
|
|
200
|
+
"""R_H = (1/q)(pμ_h² − nμ_e²)/(pμ_h + nμ_e)² (cm³/C)."""
|
|
201
|
+
return _call(semiconductor.hall_coefficient, req.n, req.p, req.mu_e, req.mu_h)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.post("/mobility-model")
|
|
205
|
+
def mobility_model(req: MobilityModelRequest) -> dict[str, Any]:
|
|
206
|
+
"""Caughey-Thomas μ_e, μ_h (cm²/V·s)."""
|
|
207
|
+
return _call(semiconductor.mobility_model, req.material, req.t, req.n)
|
quantized/routes/sld.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Thin SLD-from-formula route. Wraps ``calc.sld_formula`` (pure).
|
|
2
|
+
|
|
3
|
+
One endpoint computes neutron + X-ray scattering-length densities (with the
|
|
4
|
+
imaginary/absorption parts) from a chemical formula, mass density, and probe
|
|
5
|
+
wavelengths. The physics lives in calc; this only validates + serializes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from quantized.calc.sld_formula import (
|
|
16
|
+
NEUTRON_WAVELENGTH,
|
|
17
|
+
XRAY_WAVELENGTH,
|
|
18
|
+
sld_from_formula,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/api/sld", tags=["sld"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SldRequest(BaseModel):
|
|
25
|
+
formula: str
|
|
26
|
+
density: float
|
|
27
|
+
neutron_wavelength: float = NEUTRON_WAVELENGTH
|
|
28
|
+
xray_wavelength: float = XRAY_WAVELENGTH
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post("/formula")
|
|
32
|
+
def formula(req: SldRequest) -> dict[str, Any]:
|
|
33
|
+
"""Neutron + X-ray SLD (real + imaginary) for a formula at a mass density."""
|
|
34
|
+
try:
|
|
35
|
+
return sld_from_formula(
|
|
36
|
+
req.formula,
|
|
37
|
+
req.density,
|
|
38
|
+
neutron_wavelength=req.neutron_wavelength,
|
|
39
|
+
xray_wavelength=req.xray_wavelength,
|
|
40
|
+
)
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|