quantized-lab 0.8.0__py3-none-any.whl

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