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,80 @@
|
|
|
1
|
+
"""Thin crystallography route. Wraps ``calc.crystallography`` (pure formulas).
|
|
2
|
+
|
|
3
|
+
Computes interplanar d-spacing from lattice parameters + Miller indices, and the
|
|
4
|
+
unit-cell volume + theoretical density (from a chemical formula + Z); the math
|
|
5
|
+
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.crystallography import cell_volume, d_spacing, theoretical_density
|
|
16
|
+
from quantized.calc.formula import formula_mass
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/api/crystallography", tags=["crystallography"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DSpacingRequest(BaseModel):
|
|
22
|
+
system: str
|
|
23
|
+
a: float
|
|
24
|
+
b: float = 0.0
|
|
25
|
+
c: float = 0.0
|
|
26
|
+
alpha: float = 90.0
|
|
27
|
+
beta: float = 90.0
|
|
28
|
+
gamma: float = 90.0
|
|
29
|
+
h: int
|
|
30
|
+
k: int
|
|
31
|
+
l: int # noqa: E741 — Miller index, the conventional name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.post("/dspacing")
|
|
35
|
+
def dspacing(req: DSpacingRequest) -> dict[str, Any]:
|
|
36
|
+
"""Interplanar d-spacing for (h,k,l) in the given crystal system."""
|
|
37
|
+
try:
|
|
38
|
+
return d_spacing(
|
|
39
|
+
req.system,
|
|
40
|
+
req.a,
|
|
41
|
+
req.b,
|
|
42
|
+
req.c,
|
|
43
|
+
req.h,
|
|
44
|
+
req.k,
|
|
45
|
+
req.l,
|
|
46
|
+
alpha=req.alpha,
|
|
47
|
+
beta=req.beta,
|
|
48
|
+
gamma=req.gamma,
|
|
49
|
+
)
|
|
50
|
+
except ValueError as exc:
|
|
51
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CellRequest(BaseModel):
|
|
55
|
+
a: float
|
|
56
|
+
b: float = 0.0 # ≤ 0 → defaults to a (cubic / rhombohedral)
|
|
57
|
+
c: float = 0.0 # ≤ 0 → defaults to a
|
|
58
|
+
alpha: float = 90.0
|
|
59
|
+
beta: float = 90.0
|
|
60
|
+
gamma: float = 90.0
|
|
61
|
+
formula: str = "" # optional — enables molar mass + theoretical density
|
|
62
|
+
z: int = 1 # formula units per cell
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.post("/cell")
|
|
66
|
+
def cell(req: CellRequest) -> dict[str, Any]:
|
|
67
|
+
"""Unit-cell volume (ų) and, when a formula is given, molar mass + density."""
|
|
68
|
+
try:
|
|
69
|
+
a = req.a
|
|
70
|
+
b = req.b if req.b > 0 else a
|
|
71
|
+
c = req.c if req.c > 0 else a
|
|
72
|
+
volume = cell_volume(a, b, c, req.alpha, req.beta, req.gamma)
|
|
73
|
+
out: dict[str, Any] = {"volume": volume}
|
|
74
|
+
if req.formula.strip():
|
|
75
|
+
mass = formula_mass(req.formula)
|
|
76
|
+
out["molar_mass"] = mass
|
|
77
|
+
out["density"] = theoretical_density(mass, req.z, volume)
|
|
78
|
+
return out
|
|
79
|
+
except ValueError as exc:
|
|
80
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Thin diffusion routes. Wraps ``calc.diffusion`` (pure formulas): Arrhenius
|
|
2
|
+
diffusion coefficient / diffusion length / Fick's first-law flux. Validate ->
|
|
3
|
+
call the pure fn -> serialize.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, HTTPException
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from quantized.calc import diffusion
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/diffusion", tags=["diffusion"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArrheniusRequest(BaseModel):
|
|
20
|
+
d0: float # pre-exponential factor (cm²/s)
|
|
21
|
+
ea: float # activation energy (eV)
|
|
22
|
+
t: float # temperature (K)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DiffusionLengthRequest(BaseModel):
|
|
26
|
+
d: float # diffusion coefficient (cm²/s)
|
|
27
|
+
t: float # time (s)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FickFluxRequest(BaseModel):
|
|
31
|
+
d: float # diffusion coefficient (cm²/s)
|
|
32
|
+
dc: float # concentration difference (cm⁻³)
|
|
33
|
+
dx: float # distance (cm)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
37
|
+
try:
|
|
38
|
+
return fn(*args, **kwargs)
|
|
39
|
+
except ValueError as exc:
|
|
40
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.post("/arrhenius")
|
|
44
|
+
def arrhenius(req: ArrheniusRequest) -> dict[str, Any]:
|
|
45
|
+
"""D = D0·exp(-Ea/(kB·T)) (cm²/s)."""
|
|
46
|
+
return _call(diffusion.arrhenius, req.d0, req.ea, req.t)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.post("/diffusion-length")
|
|
50
|
+
def diffusion_length(req: DiffusionLengthRequest) -> dict[str, Any]:
|
|
51
|
+
"""L = √(D·t) (cm)."""
|
|
52
|
+
return _call(diffusion.diffusion_length, req.d, req.t)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post("/fick-flux")
|
|
56
|
+
def fick_flux(req: FickFluxRequest) -> dict[str, Any]:
|
|
57
|
+
"""J = -D·ΔC/Δx (atoms/(cm²·s))."""
|
|
58
|
+
return _call(diffusion.fick_flux, req.d, req.dc, req.dx)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Thin electrical-transport routes. Wraps ``calc.electrical`` (pure formulas):
|
|
2
|
+
resistivity / sheet resistance / conductivity / mobility / current density /
|
|
3
|
+
Hall effect / Wiedemann-Franz. Validate -> call the pure fn -> serialize.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, HTTPException
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from quantized.calc import electrical
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/electrical", tags=["electrical"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResistivityRequest(BaseModel):
|
|
20
|
+
rs: float
|
|
21
|
+
t: float # thickness (cm)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SheetResistanceRequest(BaseModel):
|
|
25
|
+
rho: float
|
|
26
|
+
t: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConductivityRequest(BaseModel):
|
|
30
|
+
rho: float
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MobilityRequest(BaseModel):
|
|
34
|
+
rho: float
|
|
35
|
+
n: float
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CurrentDensityRequest(BaseModel):
|
|
39
|
+
i: float
|
|
40
|
+
area: float
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HallRequest(BaseModel):
|
|
44
|
+
v_h: float
|
|
45
|
+
i: float
|
|
46
|
+
b: float
|
|
47
|
+
t: float # thickness (cm)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class WiedemannFranzRequest(BaseModel):
|
|
51
|
+
temperature: float | list[float]
|
|
52
|
+
resistivity: float | list[float]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
56
|
+
try:
|
|
57
|
+
return fn(*args, **kwargs)
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("/resistivity")
|
|
63
|
+
def resistivity(req: ResistivityRequest) -> dict[str, Any]:
|
|
64
|
+
"""ρ = R_s·t (Ω·cm)."""
|
|
65
|
+
return _call(electrical.resistivity, req.rs, req.t)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("/sheet-resistance")
|
|
69
|
+
def sheet_resistance(req: SheetResistanceRequest) -> dict[str, Any]:
|
|
70
|
+
"""R_s = ρ/t (Ω/sq)."""
|
|
71
|
+
return _call(electrical.sheet_resistance, req.rho, req.t)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.post("/conductivity")
|
|
75
|
+
def conductivity(req: ConductivityRequest) -> dict[str, Any]:
|
|
76
|
+
"""σ = 1/ρ (S/cm)."""
|
|
77
|
+
return _call(electrical.conductivity, req.rho)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post("/mobility")
|
|
81
|
+
def mobility(req: MobilityRequest) -> dict[str, Any]:
|
|
82
|
+
"""μ = 1/(q·n·ρ) (cm²/V·s)."""
|
|
83
|
+
return _call(electrical.mobility, req.rho, req.n)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.post("/current-density")
|
|
87
|
+
def current_density(req: CurrentDensityRequest) -> dict[str, Any]:
|
|
88
|
+
"""J = I/A (A/cm²)."""
|
|
89
|
+
return _call(electrical.current_density, req.i, req.area)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@router.post("/hall")
|
|
93
|
+
def hall(req: HallRequest) -> dict[str, Any]:
|
|
94
|
+
"""Single-point Hall: R_H, carrier density, carrier type."""
|
|
95
|
+
return _call(electrical.hall_single_point, req.v_h, req.i, req.b, req.t)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.post("/wiedemann-franz")
|
|
99
|
+
def wiedemann_franz(req: WiedemannFranzRequest) -> dict[str, Any]:
|
|
100
|
+
"""κ_e = L₀·T/ρ (W/(cm·K))."""
|
|
101
|
+
return _call(electrical.wiedemann_franz, req.temperature, req.resistivity)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Thin electrochemistry routes. Wraps ``calc.electrochemistry`` (pure
|
|
2
|
+
formulas): Nernst potential / Butler-Volmer / Tafel slope / ohmic (iR) drop /
|
|
3
|
+
double-layer capacitance. Validate -> call the pure fn -> serialize.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fastapi import APIRouter, HTTPException
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from quantized.calc import electrochemistry
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/electrochemistry", tags=["electrochemistry"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NernstRequest(BaseModel):
|
|
20
|
+
e0: float
|
|
21
|
+
n: float
|
|
22
|
+
q: float
|
|
23
|
+
t: float = 298.15
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ButlerVolmerRequest(BaseModel):
|
|
27
|
+
j0: float
|
|
28
|
+
eta: float
|
|
29
|
+
alpha: float = 0.5
|
|
30
|
+
t: float = 298.15
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TafelRequest(BaseModel):
|
|
34
|
+
alpha: float
|
|
35
|
+
t: float = 298.15
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OhmicDropRequest(BaseModel):
|
|
39
|
+
i: float
|
|
40
|
+
r: float
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DoubleLayerRequest(BaseModel):
|
|
44
|
+
epsilon: float
|
|
45
|
+
d: float # nm
|
|
46
|
+
area: float # cm^2
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
50
|
+
try:
|
|
51
|
+
return fn(*args, **kwargs)
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/nernst")
|
|
57
|
+
def nernst(req: NernstRequest) -> dict[str, Any]:
|
|
58
|
+
"""E = E⁰ − (R·T)/(n·F)·ln(Q) (V)."""
|
|
59
|
+
return _call(electrochemistry.nernst_potential, req.e0, req.n, req.q, t=req.t)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.post("/butler-volmer")
|
|
63
|
+
def butler_volmer(req: ButlerVolmerRequest) -> dict[str, Any]:
|
|
64
|
+
"""j = j₀·[exp(αFη/RT) − exp(−(1−α)Fη/RT)] (A/cm²)."""
|
|
65
|
+
return _call(electrochemistry.butler_volmer, req.j0, req.eta, alpha=req.alpha, t=req.t)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.post("/tafel-slope")
|
|
69
|
+
def tafel_slope(req: TafelRequest) -> dict[str, Any]:
|
|
70
|
+
"""b = 2.303·R·T/(α·F) (V/decade)."""
|
|
71
|
+
return _call(electrochemistry.tafel_slope, req.alpha, t=req.t)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.post("/ohmic-drop")
|
|
75
|
+
def ohmic_drop(req: OhmicDropRequest) -> dict[str, Any]:
|
|
76
|
+
"""V_IR = I·R (V)."""
|
|
77
|
+
return _call(electrochemistry.ohmic_drop, req.i, req.r)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post("/double-layer-capacitance")
|
|
81
|
+
def double_layer_capacitance(req: DoubleLayerRequest) -> dict[str, Any]:
|
|
82
|
+
"""C = ε₀·ε_r·A/d (F)."""
|
|
83
|
+
return _call(electrochemistry.double_layer_capacitance, req.epsilon, req.d, req.area)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Thin export routes: DataStruct -> downloadable file (data formats).
|
|
2
|
+
|
|
3
|
+
Wraps data exporters: ``io.xrd_csv`` (pure in-memory text), ``io.hdf5``
|
|
4
|
+
(writes a binary file; staged in temp dir), and ``io.origin`` (LabTalk scripts
|
|
5
|
+
for Origin). No formatting logic here — writers own it. Figure rendering is
|
|
6
|
+
in ``routes.export_figures``. Filenames are sanitized before the
|
|
7
|
+
Content-Disposition header.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tempfile
|
|
13
|
+
import zipfile
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from io import BytesIO
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from fastapi import APIRouter, HTTPException, Response
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
|
|
22
|
+
from quantized.datastruct import DataStruct
|
|
23
|
+
from quantized.io.consolidated import consolidate_csv
|
|
24
|
+
from quantized.io.hdf5 import write_hdf5
|
|
25
|
+
from quantized.io.origin import GraphSpec, format_origin_project_script, format_origin_script
|
|
26
|
+
from quantized.io.origin_com import com_available, send_to_origin
|
|
27
|
+
from quantized.io.origin_project.writer import opj_bytes
|
|
28
|
+
from quantized.io.xrd_csv import format_xrd_csv
|
|
29
|
+
from quantized.routes._export_common import _attachment, _safe_name
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix="/api/export", tags=["export"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class XrdCsvRequest(BaseModel):
|
|
35
|
+
dataset: dict[str, Any]
|
|
36
|
+
fmt: str = "standard"
|
|
37
|
+
intensity: str = "both"
|
|
38
|
+
include_metadata: bool = True
|
|
39
|
+
filename: str = "export.csv"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Hdf5Request(BaseModel):
|
|
43
|
+
dataset: dict[str, Any]
|
|
44
|
+
corrected: dict[str, Any] | None = None
|
|
45
|
+
corrections: dict[str, float] | None = None
|
|
46
|
+
filename: str = "export.h5"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.post("/xrd-csv")
|
|
50
|
+
def export_xrd_csv(req: XrdCsvRequest) -> Response:
|
|
51
|
+
"""XRD data -> CSV (standard) or Origin ASCII text, as a file download."""
|
|
52
|
+
try:
|
|
53
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
54
|
+
text = format_xrd_csv(
|
|
55
|
+
ds,
|
|
56
|
+
fmt=req.fmt,
|
|
57
|
+
intensity=req.intensity,
|
|
58
|
+
include_metadata=req.include_metadata,
|
|
59
|
+
)
|
|
60
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
61
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
62
|
+
return Response(
|
|
63
|
+
content=text,
|
|
64
|
+
media_type="text/csv",
|
|
65
|
+
headers=_attachment(_safe_name(req.filename, ".csv")),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.post("/hdf5")
|
|
70
|
+
def export_hdf5(req: Hdf5Request) -> Response:
|
|
71
|
+
"""DataStruct (+ optional corrected/corrections) -> self-describing HDF5."""
|
|
72
|
+
try:
|
|
73
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
74
|
+
corr = DataStruct.from_dict(req.corrected) if req.corrected else None
|
|
75
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
76
|
+
out = Path(tmp) / "export.h5"
|
|
77
|
+
write_hdf5(ds, out, corr_data=corr, corrections=req.corrections)
|
|
78
|
+
payload = out.read_bytes()
|
|
79
|
+
except ImportError as exc: # h5py absent (declared runtime dep, but be safe)
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=501, detail="HDF5 export requires h5py"
|
|
82
|
+
) from exc
|
|
83
|
+
except (ValueError, KeyError, IndexError, FileNotFoundError, FileExistsError) as exc:
|
|
84
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
85
|
+
return Response(
|
|
86
|
+
content=payload,
|
|
87
|
+
media_type="application/x-hdf5",
|
|
88
|
+
headers=_attachment(_safe_name(req.filename, ".h5")),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class OriginGraphSpec(BaseModel):
|
|
93
|
+
"""Current plot-state snapshot for the ``.ogs`` GRAPH block (item 26) —
|
|
94
|
+
wire model for ``io.origin.GraphSpec``. Indices are 0-based value-channel
|
|
95
|
+
positions (same as ``PlotState``): ``y_keys=None`` means "all channels"."""
|
|
96
|
+
|
|
97
|
+
y_keys: list[int] | None = None
|
|
98
|
+
x_key: int | None = None
|
|
99
|
+
x_log: bool = False
|
|
100
|
+
y_log: bool = False
|
|
101
|
+
x_lim: tuple[float, float] | None = None
|
|
102
|
+
y_lim: tuple[float, float] | None = None
|
|
103
|
+
y2_keys: list[int] = []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class OriginRequest(BaseModel):
|
|
107
|
+
dataset: dict[str, Any]
|
|
108
|
+
filename: str = "export"
|
|
109
|
+
log_x: bool = False
|
|
110
|
+
log_y: bool = False
|
|
111
|
+
make_graph: bool = True
|
|
112
|
+
graph: OriginGraphSpec | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ConsolidatedItem(BaseModel):
|
|
116
|
+
dataset: dict[str, Any]
|
|
117
|
+
name: str = ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class OpjRequest(BaseModel):
|
|
121
|
+
datasets: list[ConsolidatedItem]
|
|
122
|
+
filename: str = "project"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class OriginComRequest(BaseModel):
|
|
126
|
+
datasets: list[ConsolidatedItem]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ConsolidatedRequest(BaseModel):
|
|
130
|
+
datasets: list[ConsolidatedItem]
|
|
131
|
+
fmt: str = "standard"
|
|
132
|
+
filename: str = "consolidated.csv"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.post("/origin")
|
|
136
|
+
def export_origin(req: OriginRequest) -> Response:
|
|
137
|
+
"""DataStruct -> a ZIP of an Origin LabTalk ``.ogs`` script + its CSV."""
|
|
138
|
+
stem = _safe_name(req.filename, "")
|
|
139
|
+
csv_name = f"{stem}_data.csv"
|
|
140
|
+
created = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
|
|
141
|
+
graph_spec = None
|
|
142
|
+
if req.graph is not None:
|
|
143
|
+
g = req.graph
|
|
144
|
+
graph_spec = GraphSpec(
|
|
145
|
+
y_keys=tuple(g.y_keys) if g.y_keys is not None else None,
|
|
146
|
+
x_key=g.x_key,
|
|
147
|
+
x_log=g.x_log,
|
|
148
|
+
y_log=g.y_log,
|
|
149
|
+
x_lim=g.x_lim,
|
|
150
|
+
y_lim=g.y_lim,
|
|
151
|
+
y2_keys=tuple(g.y2_keys),
|
|
152
|
+
)
|
|
153
|
+
try:
|
|
154
|
+
ds = DataStruct.from_dict(req.dataset)
|
|
155
|
+
csv_text, ogs_text = format_origin_script(
|
|
156
|
+
ds,
|
|
157
|
+
csv_name=csv_name,
|
|
158
|
+
book_name=stem,
|
|
159
|
+
sheet_name=stem,
|
|
160
|
+
log_x=req.log_x,
|
|
161
|
+
log_y=req.log_y,
|
|
162
|
+
make_graph=req.make_graph,
|
|
163
|
+
created=created,
|
|
164
|
+
graph=graph_spec,
|
|
165
|
+
)
|
|
166
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
167
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
168
|
+
|
|
169
|
+
buf = BytesIO()
|
|
170
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
171
|
+
zf.writestr(f"{stem}.ogs", ogs_text)
|
|
172
|
+
zf.writestr(csv_name, csv_text)
|
|
173
|
+
return Response(
|
|
174
|
+
content=buf.getvalue(),
|
|
175
|
+
media_type="application/zip",
|
|
176
|
+
headers=_attachment(_safe_name(stem, ".zip")),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.post("/consolidated")
|
|
181
|
+
def export_consolidated(req: ConsolidatedRequest) -> Response:
|
|
182
|
+
"""Multiple datasets -> one role-based CSV (per-dataset Q + value blocks)."""
|
|
183
|
+
try:
|
|
184
|
+
items = [(DataStruct.from_dict(it.dataset), it.name) for it in req.datasets]
|
|
185
|
+
text = consolidate_csv(items, fmt=req.fmt)
|
|
186
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
187
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
188
|
+
return Response(
|
|
189
|
+
content=text,
|
|
190
|
+
media_type="text/csv",
|
|
191
|
+
headers=_attachment(_safe_name(req.filename, ".csv")),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.post("/opj")
|
|
196
|
+
def export_opj(req: OpjRequest) -> Response:
|
|
197
|
+
"""DataStructs -> a native Origin ``.opj`` project (readable by ANY Origin
|
|
198
|
+
version — Origin ≥2023 dropped writing .opj but still opens it)."""
|
|
199
|
+
try:
|
|
200
|
+
books = []
|
|
201
|
+
for item in req.datasets:
|
|
202
|
+
ds = DataStruct.from_dict(item.dataset)
|
|
203
|
+
if item.name and "origin_book" not in ds.metadata:
|
|
204
|
+
meta = dict(ds.metadata)
|
|
205
|
+
meta["origin_book"] = item.name
|
|
206
|
+
ds = DataStruct(
|
|
207
|
+
time=ds.time,
|
|
208
|
+
values=ds.values,
|
|
209
|
+
labels=ds.labels,
|
|
210
|
+
units=ds.units,
|
|
211
|
+
metadata=meta,
|
|
212
|
+
)
|
|
213
|
+
books.append(ds)
|
|
214
|
+
payload = opj_bytes(books)
|
|
215
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
216
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
217
|
+
stem = _safe_name(req.filename, "")
|
|
218
|
+
return Response(
|
|
219
|
+
content=payload,
|
|
220
|
+
media_type="application/octet-stream",
|
|
221
|
+
headers=_attachment(f"{stem}.opj"),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@router.post("/origin-project")
|
|
226
|
+
def export_origin_project(req: OpjRequest) -> Response:
|
|
227
|
+
"""DataStructs -> a ZIP holding one LabTalk ``.ogs`` + one CSV per book."""
|
|
228
|
+
created = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
|
|
229
|
+
try:
|
|
230
|
+
items = [(DataStruct.from_dict(i.dataset), i.name) for i in req.datasets]
|
|
231
|
+
csvs, ogs_text = format_origin_project_script(items, created=created)
|
|
232
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
233
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
234
|
+
stem = _safe_name(req.filename, "")
|
|
235
|
+
buf = BytesIO()
|
|
236
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
237
|
+
zf.writestr(f"{stem}.ogs", ogs_text)
|
|
238
|
+
for csv_name, csv_text in csvs:
|
|
239
|
+
zf.writestr(csv_name, csv_text)
|
|
240
|
+
return Response(
|
|
241
|
+
content=buf.getvalue(),
|
|
242
|
+
media_type="application/zip",
|
|
243
|
+
headers=_attachment(_safe_name(stem, ".zip")),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@router.get("/origin-com/status")
|
|
248
|
+
def origin_com_status() -> dict[str, bool]:
|
|
249
|
+
"""Whether COM "Send to Origin" (item 25) is usable right now: Windows +
|
|
250
|
+
pywin32 + ``QZ_ORIGIN_COM=1`` + (unverified here) a running OriginPro
|
|
251
|
+
instance. The UI uses this to show/hide the action; everywhere it is
|
|
252
|
+
False, use ``/origin`` or ``/origin-project`` instead."""
|
|
253
|
+
return {"available": com_available()}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@router.post("/origin-com")
|
|
257
|
+
def export_origin_com(req: OriginComRequest) -> dict[str, Any]:
|
|
258
|
+
"""DataStructs -> new workbooks in a RUNNING Origin instance via COM
|
|
259
|
+
(item 25, Windows-only optional). 409 when COM is unavailable or Origin
|
|
260
|
+
rejects the push — use ``/origin`` or ``/origin-project`` instead."""
|
|
261
|
+
if not req.datasets:
|
|
262
|
+
raise HTTPException(status_code=422, detail="datasets must be non-empty")
|
|
263
|
+
if not com_available():
|
|
264
|
+
raise HTTPException(
|
|
265
|
+
status_code=409,
|
|
266
|
+
detail=(
|
|
267
|
+
"Origin COM is unavailable on this machine (needs Windows, "
|
|
268
|
+
"pywin32, QZ_ORIGIN_COM=1, and a running OriginPro instance). "
|
|
269
|
+
"Use POST /api/export/origin or /api/export/origin-project instead."
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
items = [DataStruct.from_dict(i.dataset) for i in req.datasets]
|
|
274
|
+
book_names = [i.name for i in req.datasets]
|
|
275
|
+
result = send_to_origin(items, book_names=book_names)
|
|
276
|
+
except (ValueError, KeyError, IndexError) as exc:
|
|
277
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
278
|
+
except RuntimeError as exc:
|
|
279
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
280
|
+
return result
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Faceted (small-multiples) figure export route (ORIGIN_GAP_PLAN #21).
|
|
2
|
+
|
|
3
|
+
Split into its own router file (rather than joining `routes/export_figures.py`)
|
|
4
|
+
purely to keep that file under the 500-line god-module ceiling — it was
|
|
5
|
+
already at 421 lines before this endpoint. Wraps `calc.figure_facets`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Response
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from quantized.routes._export_common import (
|
|
16
|
+
_DPI_MAX,
|
|
17
|
+
_DPI_MIN,
|
|
18
|
+
_FIGURE_MIME,
|
|
19
|
+
_attachment,
|
|
20
|
+
_safe_name,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/api/export", tags=["export"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FacetSeries(BaseModel):
|
|
27
|
+
label: str
|
|
28
|
+
y: list[float]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FacetPanel(BaseModel):
|
|
32
|
+
label: str
|
|
33
|
+
x: list[float]
|
|
34
|
+
series: list[FacetSeries]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FacetsFigureRequest(BaseModel):
|
|
38
|
+
panels: list[FacetPanel] # one per facet-column level (frontend lib/facet.facetPayloads)
|
|
39
|
+
x_log: bool = False
|
|
40
|
+
y_log: bool = False
|
|
41
|
+
fmt: str = "pdf"
|
|
42
|
+
style: str = "default"
|
|
43
|
+
title: str = ""
|
|
44
|
+
x_label: str = ""
|
|
45
|
+
y_label: str = ""
|
|
46
|
+
dpi: int = 200
|
|
47
|
+
width_in: float | None = None
|
|
48
|
+
height_in: float | None = None
|
|
49
|
+
filename: str = "facets"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/facets-figure")
|
|
53
|
+
def export_facets_figure(req: FacetsFigureRequest) -> Response:
|
|
54
|
+
"""Render a small-multiples (faceted) figure (gap #21): one panel per
|
|
55
|
+
facet-column level, sharing scales, tiled into a grid (PDF/SVG/PNG/TIFF)."""
|
|
56
|
+
if req.fmt not in _FIGURE_MIME:
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=422, detail=f"fmt must be one of {sorted(_FIGURE_MIME)}"
|
|
59
|
+
)
|
|
60
|
+
dpi = max(_DPI_MIN, min(_DPI_MAX, req.dpi))
|
|
61
|
+
from quantized.calc.figure_facets import render_facets_figure # lazy: matplotlib is heavy
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
panels: list[dict[str, Any]] = [
|
|
65
|
+
{
|
|
66
|
+
"label": p.label,
|
|
67
|
+
"x": p.x,
|
|
68
|
+
"series": [{"label": s.label, "y": s.y} for s in p.series],
|
|
69
|
+
}
|
|
70
|
+
for p in req.panels
|
|
71
|
+
]
|
|
72
|
+
img = render_facets_figure(
|
|
73
|
+
panels, x_log=req.x_log, y_log=req.y_log, fmt=req.fmt, style=req.style,
|
|
74
|
+
title=req.title, x_label=req.x_label, y_label=req.y_label, dpi=dpi,
|
|
75
|
+
width_in=req.width_in, height_in=req.height_in,
|
|
76
|
+
)
|
|
77
|
+
except (ValueError, KeyError, IndexError, TypeError) as exc:
|
|
78
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
79
|
+
return Response(
|
|
80
|
+
content=img,
|
|
81
|
+
media_type=_FIGURE_MIME[req.fmt],
|
|
82
|
+
headers=_attachment(_safe_name(req.filename, f".{req.fmt}")),
|
|
83
|
+
)
|