quantized-lab 0.8.0__py3-none-any.whl

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