quantized-lab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,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)
|