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,150 @@
1
+ """Thin routes for the interactive import wizard (ORIGIN_GAP_PLAN #40).
2
+
3
+ The browser sends raw file text; these adapters guess/preview/parse it under
4
+ adjustable :class:`~quantized.io.import_preview.ImportSettings` and return the
5
+ wizard's preview table or the imported ``DataStruct``. All logic lives in
6
+ ``io.import_preview``.
7
+
8
+ The ``/filters`` routes are CRUD over saved :class:`~quantized.io.import_filters.
9
+ ImportFilter` records (name + glob + settings), persisted server-side so the
10
+ registry (``io.registry.resolve_parser``) can consult them headlessly, not just
11
+ from this wizard. All logic lives in ``io.import_filters``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ from fastapi import APIRouter, HTTPException
19
+ from pydantic import BaseModel
20
+
21
+ from quantized.io.import_filters import (
22
+ ImportFilter,
23
+ delete_filter,
24
+ load_filters,
25
+ match_filter,
26
+ save_filter,
27
+ )
28
+ from quantized.io.import_preview import (
29
+ ImportSettings,
30
+ guess_settings,
31
+ parse_import,
32
+ preview_import,
33
+ )
34
+ from quantized.routes._payload import datastruct_payload
35
+
36
+ router = APIRouter(prefix="/api/import", tags=["import"])
37
+
38
+
39
+ class GuessRequest(BaseModel):
40
+ text: str
41
+
42
+
43
+ @router.post("/guess")
44
+ def guess_route(req: GuessRequest) -> dict[str, Any]:
45
+ """Best-effort starting settings for a pasted/uploaded file's text."""
46
+ try:
47
+ return guess_settings(req.text).to_dict()
48
+ except (ValueError, IndexError) as exc:
49
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
50
+
51
+
52
+ class PreviewRequest(BaseModel):
53
+ text: str
54
+ settings: dict[str, Any] | None = None # None -> auto-guess
55
+ max_rows: int = 20
56
+
57
+
58
+ @router.post("/preview")
59
+ def preview_route(req: PreviewRequest) -> dict[str, Any]:
60
+ """Parse the first rows under the given settings for the wizard to render."""
61
+ try:
62
+ settings = (
63
+ guess_settings(req.text) if req.settings is None
64
+ else ImportSettings.from_dict(req.settings)
65
+ )
66
+ return preview_import(req.text, settings, max_rows=max(1, min(200, req.max_rows)))
67
+ except (ValueError, IndexError, TypeError) as exc:
68
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
69
+
70
+
71
+ class ParseRequest(BaseModel):
72
+ text: str
73
+ settings: dict[str, Any]
74
+
75
+
76
+ @router.post("/parse")
77
+ def parse_route(req: ParseRequest) -> dict[str, Any]:
78
+ """Import the full text under confirmed settings into a DataStruct."""
79
+ try:
80
+ ds = parse_import(req.text, ImportSettings.from_dict(req.settings))
81
+ except (ValueError, IndexError, TypeError) as exc:
82
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
83
+ return datastruct_payload(ds)
84
+
85
+
86
+ # ── Saved import filters (gap #40 persistence) ───────────────────────────────
87
+
88
+
89
+ @router.get("/filters")
90
+ def list_filters_route() -> list[dict[str, Any]]:
91
+ """All saved import filters, most-recently-saved information included."""
92
+ return [f.to_dict() for f in load_filters()]
93
+
94
+
95
+ class SaveFilterRequest(BaseModel):
96
+ name: str
97
+ glob: str
98
+ settings: dict[str, Any]
99
+
100
+
101
+ @router.post("/filters")
102
+ def save_filter_route(req: SaveFilterRequest) -> dict[str, Any]:
103
+ """Save (upsert by name) a filter binding a glob to import settings."""
104
+ try:
105
+ filt = ImportFilter(
106
+ name=req.name, glob=req.glob, settings=ImportSettings.from_dict(req.settings)
107
+ )
108
+ saved = save_filter(filt)
109
+ except (ValueError, TypeError) as exc:
110
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
111
+ return saved.to_dict()
112
+
113
+
114
+ @router.delete("/filters/{name}")
115
+ def delete_filter_route(name: str) -> dict[str, str]:
116
+ """Delete the saved filter named ``name``."""
117
+ if not delete_filter(name):
118
+ raise HTTPException(status_code=404, detail=f"no saved filter named '{name}'")
119
+ return {"deleted": name}
120
+
121
+
122
+ class ImportWithFilterRequest(BaseModel):
123
+ text: str
124
+ filename: str | None = None # match the best saved filter for this name
125
+ filter_name: str | None = None # or use one specific saved filter by name
126
+
127
+
128
+ @router.post("/filters/parse")
129
+ def parse_with_filter_route(req: ImportWithFilterRequest) -> dict[str, Any]:
130
+ """Import ``text`` under a saved filter — by name, or the best glob match
131
+ for ``filename`` — so a returning file imports with zero dialogs."""
132
+ if req.filter_name is not None:
133
+ filt = next((f for f in load_filters() if f.name == req.filter_name), None)
134
+ if filt is None:
135
+ raise HTTPException(
136
+ status_code=404, detail=f"no saved filter named '{req.filter_name}'"
137
+ )
138
+ elif req.filename is not None:
139
+ filt = match_filter(req.filename)
140
+ if filt is None:
141
+ raise HTTPException(
142
+ status_code=404, detail=f"no saved filter matches '{req.filename}'"
143
+ )
144
+ else:
145
+ raise HTTPException(status_code=422, detail="filename or filter_name is required")
146
+ try:
147
+ ds = parse_import(req.text, filt.settings)
148
+ except (ValueError, IndexError, TypeError) as exc:
149
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
150
+ return datastruct_payload(ds)
@@ -0,0 +1,59 @@
1
+ """Poll-model background-job routes (GOTO #9): status / result / cancel / list.
2
+
3
+ Thin adapters over ``quantized.jobs``. There is deliberately NO generic
4
+ submit endpoint — jobs are submitted internally by other routes (e.g.
5
+ ``POST /api/fitting/bumps`` with ``engine='dream'``), never from
6
+ client-supplied code. The SPA GET-polls ``/api/jobs/{id}`` (~1 s) only
7
+ while a job is live; no WebSocket.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from fastapi import APIRouter, HTTPException
15
+
16
+ from quantized.jobs import jobs
17
+ from quantized.routes._payload import to_jsonable
18
+
19
+ router = APIRouter(prefix="/api/jobs", tags=["jobs"])
20
+
21
+
22
+ @router.get("")
23
+ def list_jobs() -> dict[str, Any]:
24
+ """Snapshots of every registered job (results omitted — poll /result)."""
25
+ return {"jobs": jobs.list()}
26
+
27
+
28
+ @router.get("/{job_id}")
29
+ def job_status(job_id: str) -> dict[str, Any]:
30
+ """Poll one job: status / progress / message (+ error when failed)."""
31
+ job = jobs.get(job_id)
32
+ if job is None:
33
+ raise HTTPException(status_code=404, detail=f"unknown job id: {job_id}")
34
+ return job.snapshot()
35
+
36
+
37
+ @router.get("/{job_id}/result")
38
+ def job_result(job_id: str) -> dict[str, Any]:
39
+ """The completed job's result. 409 until the job reaches ``done``."""
40
+ job = jobs.get(job_id)
41
+ if job is None:
42
+ raise HTTPException(status_code=404, detail=f"unknown job id: {job_id}")
43
+ snap = job.snapshot(include_result=True)
44
+ if snap["status"] == "error":
45
+ raise HTTPException(status_code=422, detail=f"job failed: {snap.get('error', '')}")
46
+ if snap["status"] != "done":
47
+ raise HTTPException(
48
+ status_code=409, detail=f"job not finished (status: {snap['status']})"
49
+ )
50
+ return {"id": job_id, "status": "done", "result": to_jsonable(snap["result"])}
51
+
52
+
53
+ @router.post("/{job_id}/cancel")
54
+ def job_cancel(job_id: str) -> dict[str, Any]:
55
+ """Request cooperative cancellation; returns the (possibly updated) snapshot."""
56
+ job = jobs.cancel(job_id)
57
+ if job is None:
58
+ raise HTTPException(status_code=404, detail=f"unknown job id: {job_id}")
59
+ return job.snapshot()
@@ -0,0 +1,135 @@
1
+ """Thin magnetic-calculator routes. Wraps ``calc.magnetic`` (pure formulas):
2
+ moment conversion / magnetization / demagnetizing factors / Curie-Weiss /
3
+ Langevin / domain wall. 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 magnetic
15
+
16
+ router = APIRouter(prefix="/api/magnetic", tags=["magnetic"])
17
+
18
+
19
+ class MomentConvertRequest(BaseModel):
20
+ value: float
21
+ unit: str = "emu"
22
+ volume: float | None = None
23
+ atoms: float | None = None
24
+
25
+
26
+ class BohrMagnetonRequest(BaseModel):
27
+ moment: float
28
+ unit: str = "emu"
29
+
30
+
31
+ class MagnetizationRequest(BaseModel):
32
+ moment: float
33
+ volume: float
34
+
35
+
36
+ class MomentPerAtomRequest(BaseModel):
37
+ total_moment: float
38
+ volume: float
39
+ atom_density: float
40
+
41
+
42
+ class DemagRequest(BaseModel):
43
+ shape: str # GUI dropdown label (e.g. "Sphere", "Thin film (in-plane)")
44
+
45
+
46
+ class CurieWeissRequest(BaseModel):
47
+ C: float
48
+ theta: float
49
+
50
+
51
+ class CurieWeissFitRequest(BaseModel):
52
+ temperature: list[float]
53
+ susceptibility: list[float]
54
+ fit_range: tuple[float, float] | None = None
55
+
56
+
57
+ class LangevinRequest(BaseModel):
58
+ mu: float
59
+ field_oe: float
60
+ temperature: float
61
+
62
+
63
+ class DomainWallRequest(BaseModel):
64
+ exchange_a: float
65
+ anisotropy_k: float
66
+
67
+
68
+ def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
69
+ try:
70
+ return fn(*args, **kwargs)
71
+ except ValueError as exc:
72
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
73
+
74
+
75
+ @router.post("/moment-convert")
76
+ def moment_convert(req: MomentConvertRequest) -> dict[str, Any]:
77
+ """Convert a moment to emu / A·m² / µ_B (+ magnetization, µ_B/atom)."""
78
+ return _call(
79
+ magnetic.moment_convert, req.value, req.unit, volume=req.volume, atoms=req.atoms
80
+ )
81
+
82
+
83
+ @router.post("/bohr-magneton")
84
+ def bohr_magneton(req: BohrMagnetonRequest) -> dict[str, Any]:
85
+ """Moment → number of Bohr magnetons."""
86
+ return _call(magnetic.bohr_magneton_convert, req.moment, req.unit)
87
+
88
+
89
+ @router.post("/magnetization")
90
+ def magnetization(req: MagnetizationRequest) -> dict[str, Any]:
91
+ """M = m/V (emu/cm³, A/m, kA/m)."""
92
+ return _call(magnetic.magnetization, req.moment, req.volume)
93
+
94
+
95
+ @router.post("/moment-per-atom")
96
+ def moment_per_atom(req: MomentPerAtomRequest) -> dict[str, Any]:
97
+ """Per-atom moment in Bohr magnetons."""
98
+ return _call(
99
+ magnetic.moment_per_atom, req.total_moment, req.volume, req.atom_density
100
+ )
101
+
102
+
103
+ @router.post("/demag")
104
+ def demag(req: DemagRequest) -> dict[str, Any]:
105
+ """Demagnetizing factors Nz, Nxy, 4πNz from a geometry label."""
106
+ return _call(magnetic.demag_named, req.shape)
107
+
108
+
109
+ @router.post("/curie-weiss")
110
+ def curie_weiss(req: CurieWeissRequest) -> dict[str, Any]:
111
+ """µ_eff and order type from Curie constant C and Weiss temperature θ."""
112
+ return _call(magnetic.curie_weiss_moment, req.C, req.theta)
113
+
114
+
115
+ @router.post("/curie-weiss-fit")
116
+ def curie_weiss_fit(req: CurieWeissFitRequest) -> dict[str, Any]:
117
+ """Curie-Weiss fit of 1/χ vs T → θ_CW, C, µ_eff, R²."""
118
+ return _call(
119
+ magnetic.curie_weiss_fit,
120
+ req.temperature,
121
+ req.susceptibility,
122
+ fit_range=req.fit_range,
123
+ )
124
+
125
+
126
+ @router.post("/langevin")
127
+ def langevin(req: LangevinRequest) -> dict[str, Any]:
128
+ """Langevin L(x) = coth(x) − 1/x for a superparamagnet."""
129
+ return _call(magnetic.langevin, req.mu, req.field_oe, req.temperature)
130
+
131
+
132
+ @router.post("/domain-wall")
133
+ def domain_wall(req: DomainWallRequest) -> dict[str, Any]:
134
+ """Domain-wall width δ = π√(A/K) and energy E = 4√(AK)."""
135
+ return _call(magnetic.domain_wall, req.exchange_a, req.anisotropy_k)
@@ -0,0 +1,133 @@
1
+ """Thin magnetometry routes. Wraps ``calc.magnetometry`` (golden vs MATLAB):
2
+ hysteresis-loop analysis, high-T background subtraction, sample-aware unit
3
+ conversion. Validate -> call the pure fn -> serialize; no analysis here.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from fastapi import APIRouter, HTTPException
12
+ from pydantic import BaseModel
13
+
14
+ from quantized.calc.magnetometry import (
15
+ convert_mag_units,
16
+ hysteresis_analysis,
17
+ subtract_hysteresis_background,
18
+ subtract_mag_background,
19
+ )
20
+ from quantized.routes._payload import to_jsonable
21
+
22
+ router = APIRouter(prefix="/api/magnetometry", tags=["magnetometry"])
23
+
24
+
25
+ class HysteresisRequest(BaseModel):
26
+ h: list[float]
27
+ m: list[float]
28
+ saturation_fraction: float = 0.8
29
+ pre_smooth: int = 0
30
+ virgin_detect: bool = True
31
+
32
+
33
+ class SubtractBgRequest(BaseModel):
34
+ temperature: list[float]
35
+ moment: list[float]
36
+ fit_range: tuple[float, float] | None = None
37
+ auto_fraction: float = 0.1
38
+
39
+
40
+ class HysteresisBgRequest(BaseModel):
41
+ h: list[float]
42
+ m: list[float]
43
+ hi_fraction: float = 0.7
44
+ min_points: int = 4
45
+
46
+
47
+ class ConvertUnitsRequest(BaseModel):
48
+ x: list[float]
49
+ y: list[float]
50
+ from_field: str = "Oe"
51
+ to_field: str = "Oe"
52
+ from_moment: str = "emu"
53
+ to_moment: str = "emu"
54
+ sample_mass: float = 0.0
55
+ sample_volume: float = 0.0
56
+
57
+
58
+ @router.post("/hysteresis")
59
+ def hysteresis(req: HysteresisRequest) -> dict[str, Any]:
60
+ """Analyze an M-H loop -> Hc / Mr / Ms / squareness / loop area / SFD."""
61
+ try:
62
+ result = hysteresis_analysis(
63
+ req.h,
64
+ req.m,
65
+ saturation_fraction=req.saturation_fraction,
66
+ pre_smooth=req.pre_smooth,
67
+ virgin_detect=req.virgin_detect,
68
+ )
69
+ except (ValueError, IndexError) as exc:
70
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
71
+ return to_jsonable(result) # type: ignore[no-any-return]
72
+
73
+
74
+ @router.post("/subtract-background")
75
+ def subtract_background(req: SubtractBgRequest) -> dict[str, Any]:
76
+ """Subtract a linear high-T background from M(T)."""
77
+ try:
78
+ corrected, slope, intercept = subtract_mag_background(
79
+ req.temperature,
80
+ req.moment,
81
+ fit_range=req.fit_range,
82
+ auto_fraction=req.auto_fraction,
83
+ )
84
+ except (ValueError, IndexError) as exc:
85
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
86
+ return {
87
+ "corrected": to_jsonable(corrected),
88
+ "slope": to_jsonable(slope),
89
+ "intercept": to_jsonable(intercept),
90
+ }
91
+
92
+
93
+ @router.post("/subtract-hysteresis-background")
94
+ def subtract_hysteresis_bg(req: HysteresisBgRequest) -> dict[str, Any]:
95
+ """Remove a linear dia/paramagnetic background from an M-H loop and centre it
96
+ vertically (per-tail slope + saturation-midpoint offset, so no vertical shift
97
+ remains)."""
98
+ try:
99
+ corrected, slope, offset = subtract_hysteresis_background(
100
+ req.h, req.m, hi_fraction=req.hi_fraction, min_points=req.min_points
101
+ )
102
+ except (ValueError, IndexError) as exc:
103
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
104
+ return {
105
+ "corrected": to_jsonable(corrected),
106
+ "slope": to_jsonable(slope),
107
+ "offset": to_jsonable(offset),
108
+ }
109
+
110
+
111
+ @router.post("/convert-units")
112
+ def convert_units(req: ConvertUnitsRequest) -> dict[str, Any]:
113
+ """Convert field (x) + moment (y) units (sample-aware)."""
114
+ try:
115
+ x_out, y_out, x_unit, y_unit, warning = convert_mag_units(
116
+ req.x,
117
+ req.y,
118
+ from_field=req.from_field,
119
+ to_field=req.to_field,
120
+ from_moment=req.from_moment,
121
+ to_moment=req.to_moment,
122
+ sample_mass=req.sample_mass,
123
+ sample_volume=req.sample_volume,
124
+ )
125
+ except (ValueError, IndexError) as exc:
126
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
127
+ return {
128
+ "x": to_jsonable(np.asarray(x_out, dtype=float)),
129
+ "y": to_jsonable(np.asarray(y_out, dtype=float)),
130
+ "x_unit": x_unit,
131
+ "y_unit": y_unit,
132
+ "warning": warning,
133
+ }
@@ -0,0 +1,98 @@
1
+ """Thin optics routes. Wraps ``calc.optics`` (pure formulas): Fresnel
2
+ coefficients / critical angle / Brewster angle / penetration depth / skin depth
3
+ / refractive-index <-> dielectric conversion. Validate -> call pure fn ->
4
+ serialize.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ from fastapi import APIRouter, HTTPException
13
+ from pydantic import BaseModel
14
+
15
+ from quantized.calc import optics
16
+
17
+ router = APIRouter(prefix="/api/optics", tags=["optics"])
18
+
19
+
20
+ class FresnelRequest(BaseModel):
21
+ n1: float
22
+ n2: float
23
+ theta: float # angle of incidence (deg)
24
+
25
+
26
+ class AngleRequest(BaseModel):
27
+ n1: float
28
+ n2: float
29
+
30
+
31
+ class PenetrationDepthRequest(BaseModel):
32
+ n: float
33
+ k: float
34
+ wavelength: float
35
+
36
+
37
+ class SkinDepthRequest(BaseModel):
38
+ rho: float # resistivity (Ohm*m, SI)
39
+ f: float # frequency (Hz)
40
+
41
+
42
+ class RefractiveToDielectricRequest(BaseModel):
43
+ n: float
44
+ k: float = 0.0
45
+
46
+
47
+ class DielectricToRefractiveRequest(BaseModel):
48
+ eps1: float
49
+ eps2: float = 0.0
50
+
51
+
52
+ def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
53
+ try:
54
+ return fn(*args, **kwargs)
55
+ except ValueError as exc:
56
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
57
+
58
+
59
+ @router.post("/fresnel")
60
+ def fresnel(req: FresnelRequest) -> dict[str, Any]:
61
+ """Rs, Rp, Ts, Tp at an interface."""
62
+ return _call(optics.fresnel_coefficients, req.n1, req.n2, req.theta)
63
+
64
+
65
+ @router.post("/critical-angle")
66
+ def critical_angle(req: AngleRequest) -> dict[str, Any]:
67
+ """θ_c = arcsin(n₂/n₁) (deg); NaN if n₂ >= n₁."""
68
+ return _call(optics.critical_angle, req.n1, req.n2)
69
+
70
+
71
+ @router.post("/brewster-angle")
72
+ def brewster_angle(req: AngleRequest) -> dict[str, Any]:
73
+ """θ_B = arctan(n₂/n₁) (deg)."""
74
+ return _call(optics.brewster_angle, req.n1, req.n2)
75
+
76
+
77
+ @router.post("/penetration-depth")
78
+ def penetration_depth(req: PenetrationDepthRequest) -> dict[str, Any]:
79
+ """δ = λ/(4πk) (same unit as λ)."""
80
+ return _call(optics.penetration_depth, req.n, req.k, req.wavelength)
81
+
82
+
83
+ @router.post("/skin-depth")
84
+ def skin_depth(req: SkinDepthRequest) -> dict[str, Any]:
85
+ """δ = √(2ρ/(ωμ₀)) (m)."""
86
+ return _call(optics.skin_depth, req.rho, req.f)
87
+
88
+
89
+ @router.post("/refractive-to-dielectric")
90
+ def refractive_to_dielectric(req: RefractiveToDielectricRequest) -> dict[str, Any]:
91
+ """ε₁ = n²−k², ε₂ = 2nk."""
92
+ return _call(optics.refractive_to_dielectric, req.n, req.k)
93
+
94
+
95
+ @router.post("/dielectric-to-refractive")
96
+ def dielectric_to_refractive(req: DielectricToRefractiveRequest) -> dict[str, Any]:
97
+ """n, k from (ε₁, ε₂) via the physical square root."""
98
+ return _call(optics.dielectric_to_refractive, req.eps1, req.eps2)