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,153 @@
|
|
|
1
|
+
"""Thin-film calculator routes. Wraps ``calc.thin_film`` (pure formulas):
|
|
2
|
+
deposition / sputter rate, diffusion length, implant dose + peak concentration,
|
|
3
|
+
Kiessig thickness, multilayer thermal conductivity, projected range, Stoney
|
|
4
|
+
stress, thermal-mismatch strain. Validate -> call the pure fn -> 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 thin_film
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/api/thin-film", tags=["thin-film"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DepositionRateRequest(BaseModel):
|
|
21
|
+
thickness: float # Å
|
|
22
|
+
time: float # s
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DiffusionLengthRequest(BaseModel):
|
|
26
|
+
d: float # cm^2/s
|
|
27
|
+
t: float # s
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DoseFromCurrentRequest(BaseModel):
|
|
31
|
+
current: float # A
|
|
32
|
+
time: float # s
|
|
33
|
+
area: float # cm^2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DoseToConcentrationRequest(BaseModel):
|
|
37
|
+
dose: float # ions/cm^2
|
|
38
|
+
rp: float # nm
|
|
39
|
+
delta_rp: float # nm
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KiessigRequest(BaseModel):
|
|
43
|
+
delta_q: float # Å^-1
|
|
44
|
+
sld: float | None = None # Å^-2
|
|
45
|
+
qc: float | None = None # Å^-1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MultilayerThermalRequest(BaseModel):
|
|
49
|
+
thicknesses: list[float] # nm
|
|
50
|
+
kappas: list[float] # W/m/K
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ProjectedRangeRequest(BaseModel):
|
|
54
|
+
ion: str
|
|
55
|
+
target: str
|
|
56
|
+
energy: float # keV
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SputterRateRequest(BaseModel):
|
|
60
|
+
y: float # atoms/ion
|
|
61
|
+
j: float # mA/cm^2
|
|
62
|
+
rho: float # g/cm^3
|
|
63
|
+
m: float # g/mol
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class StoneyStressRequest(BaseModel):
|
|
67
|
+
es: float # Pa
|
|
68
|
+
nus: float
|
|
69
|
+
ts: float # m
|
|
70
|
+
tf: float # m
|
|
71
|
+
r: float # m
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ThermalMismatchRequest(BaseModel):
|
|
75
|
+
alpha_film: float # 1/K
|
|
76
|
+
alpha_sub: float # 1/K
|
|
77
|
+
delta_t: float # K
|
|
78
|
+
e: float | None = None # Pa
|
|
79
|
+
nu: float = 0.3
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
83
|
+
try:
|
|
84
|
+
return fn(*args, **kwargs)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@router.post("/deposition-rate")
|
|
90
|
+
def deposition_rate(req: DepositionRateRequest) -> dict[str, Any]:
|
|
91
|
+
"""r = thickness/time (Å/s, nm/min)."""
|
|
92
|
+
return _call(thin_film.deposition_rate, req.thickness, req.time)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.post("/diffusion-length")
|
|
96
|
+
def diffusion_length(req: DiffusionLengthRequest) -> dict[str, Any]:
|
|
97
|
+
"""L = √(D·t) (cm/nm/µm)."""
|
|
98
|
+
return _call(thin_film.diffusion_length_thermal, req.d, req.t)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.post("/dose-from-current")
|
|
102
|
+
def dose_from_current(req: DoseFromCurrentRequest) -> dict[str, Any]:
|
|
103
|
+
"""Φ = I·t/(q·A) (ions/cm²)."""
|
|
104
|
+
return _call(thin_film.dose_from_current, req.current, req.time, req.area)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.post("/dose-to-concentration")
|
|
108
|
+
def dose_to_concentration(req: DoseToConcentrationRequest) -> dict[str, Any]:
|
|
109
|
+
"""C_peak = dose/(√(2π)·ΔRp) (atoms/cm³)."""
|
|
110
|
+
return _call(thin_film.dose_to_concentration, req.dose, req.rp, req.delta_rp)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@router.post("/kiessig-thickness")
|
|
114
|
+
def kiessig_thickness(req: KiessigRequest) -> dict[str, Any]:
|
|
115
|
+
"""t = 2π/ΔQ (refraction-corrected when SLD/Qc supplied)."""
|
|
116
|
+
return _call(thin_film.kiessig_thickness, req.delta_q, sld=req.sld, qc=req.qc)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@router.post("/multilayer-thermal")
|
|
120
|
+
def multilayer_thermal(req: MultilayerThermalRequest) -> dict[str, Any]:
|
|
121
|
+
"""Series / parallel effective thermal conductivity (W/m/K)."""
|
|
122
|
+
return _call(thin_film.multilayer_thermal_conductivity, req.thicknesses, req.kappas)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@router.post("/projected-range")
|
|
126
|
+
def projected_range(req: ProjectedRangeRequest) -> dict[str, Any]:
|
|
127
|
+
"""LSS projected range Rp + straggle ΔRp (nm)."""
|
|
128
|
+
return _call(thin_film.projected_range, req.ion, req.target, req.energy)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@router.post("/sputter-rate")
|
|
132
|
+
def sputter_rate(req: SputterRateRequest) -> dict[str, Any]:
|
|
133
|
+
"""Sputter erosion rate (nm/s, nm/min)."""
|
|
134
|
+
return _call(thin_film.sputter_rate, req.y, req.j, req.rho, req.m)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router.post("/stoney-stress")
|
|
138
|
+
def stoney_stress(req: StoneyStressRequest) -> dict[str, Any]:
|
|
139
|
+
"""σ = E_s·t_s²/(6(1−ν_s)·t_f·R) (Pa)."""
|
|
140
|
+
return _call(thin_film.stoney_stress, req.es, req.nus, req.ts, req.tf, req.r)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.post("/thermal-mismatch")
|
|
144
|
+
def thermal_mismatch(req: ThermalMismatchRequest) -> dict[str, Any]:
|
|
145
|
+
"""ε = (α_f−α_s)·ΔT, σ = E·ε/(1−ν) (MPa)."""
|
|
146
|
+
return _call(
|
|
147
|
+
thin_film.thermal_mismatch_strain,
|
|
148
|
+
req.alpha_film,
|
|
149
|
+
req.alpha_sub,
|
|
150
|
+
req.delta_t,
|
|
151
|
+
e=req.e,
|
|
152
|
+
nu=req.nu,
|
|
153
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Thin vacuum-science routes. Wraps ``calc.vacuum`` (pure formulas): mean free
|
|
2
|
+
path / monolayer time / sputter yield / pump-down time / Knudsen number / gas
|
|
3
|
+
flow conductance. 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 vacuum
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/vacuum", tags=["vacuum"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MeanFreePathRequest(BaseModel):
|
|
20
|
+
p: float
|
|
21
|
+
temperature: float = 300.0
|
|
22
|
+
d: float = 3.64e-10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MonolayerTimeRequest(BaseModel):
|
|
26
|
+
p: float
|
|
27
|
+
m: float = 4.65e-26
|
|
28
|
+
temperature: float = 300.0
|
|
29
|
+
a_site: float = 1e-19
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KnudsenRequest(BaseModel):
|
|
33
|
+
mfp: float
|
|
34
|
+
length: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PumpDownRequest(BaseModel):
|
|
38
|
+
v: float
|
|
39
|
+
s: float
|
|
40
|
+
p0: float
|
|
41
|
+
pf: float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SputterYieldRequest(BaseModel):
|
|
45
|
+
material: str
|
|
46
|
+
energy: float
|
|
47
|
+
ion: str = "Ar"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GasFlowRequest(BaseModel):
|
|
51
|
+
p1: float
|
|
52
|
+
p2: float
|
|
53
|
+
d: float
|
|
54
|
+
length: float
|
|
55
|
+
temperature: float = 300.0
|
|
56
|
+
m: float = 4.65e-26
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _call(fn: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
60
|
+
try:
|
|
61
|
+
return fn(*args, **kwargs)
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.post("/mean-free-path")
|
|
67
|
+
def mean_free_path(req: MeanFreePathRequest) -> dict[str, Any]:
|
|
68
|
+
"""λ = k_B·T / (√2·π·d²·P) (m)."""
|
|
69
|
+
return _call(vacuum.mean_free_path, req.p, temperature=req.temperature, d=req.d)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.post("/monolayer-time")
|
|
73
|
+
def monolayer_time(req: MonolayerTimeRequest) -> dict[str, Any]:
|
|
74
|
+
"""t_mono = 1/(J·A_site) (s)."""
|
|
75
|
+
return _call(
|
|
76
|
+
vacuum.monolayer_time,
|
|
77
|
+
req.p,
|
|
78
|
+
m=req.m,
|
|
79
|
+
temperature=req.temperature,
|
|
80
|
+
a_site=req.a_site,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.post("/knudsen")
|
|
85
|
+
def knudsen(req: KnudsenRequest) -> dict[str, Any]:
|
|
86
|
+
"""Kn = λ/L plus flow regime."""
|
|
87
|
+
return _call(vacuum.knudsen_number, req.mfp, req.length)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.post("/pump-down")
|
|
91
|
+
def pump_down(req: PumpDownRequest) -> dict[str, Any]:
|
|
92
|
+
"""t = (V/S)·ln(P0/Pf) (s)."""
|
|
93
|
+
return _call(vacuum.pump_down_time, req.v, req.s, req.p0, req.pf)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.post("/sputter-yield")
|
|
97
|
+
def sputter_yield(req: SputterYieldRequest) -> dict[str, Any]:
|
|
98
|
+
"""Y (atoms/ion) from the Ar-ion lookup table; NaN outside range."""
|
|
99
|
+
return _call(vacuum.sputter_yield, req.material, req.energy, ion=req.ion)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.post("/gas-flow")
|
|
103
|
+
def gas_flow(req: GasFlowRequest) -> dict[str, Any]:
|
|
104
|
+
"""Molecular & viscous gas-flow conductance through a tube (L/s)."""
|
|
105
|
+
return _call(
|
|
106
|
+
vacuum.gas_flow,
|
|
107
|
+
req.p1,
|
|
108
|
+
req.p2,
|
|
109
|
+
req.d,
|
|
110
|
+
req.length,
|
|
111
|
+
temperature=req.temperature,
|
|
112
|
+
m=req.m,
|
|
113
|
+
)
|
quantized/routes/xray.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Thin X-ray / neutron calculator route. Wraps ``calc.xray`` (pure formulas).
|
|
2
|
+
|
|
3
|
+
One endpoint dispatches the Bragg / Q↔2θ scalar conversions by ``mode``; the
|
|
4
|
+
math lives in calc, this only validates + serializes.
|
|
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
|
|
13
|
+
|
|
14
|
+
from quantized.calc.xray import xray_calc
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/xray", tags=["xray"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class XrayCalcRequest(BaseModel):
|
|
20
|
+
mode: str
|
|
21
|
+
wavelength: float
|
|
22
|
+
value: float
|
|
23
|
+
n: int = 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.post("/calc")
|
|
27
|
+
def calc(req: XrayCalcRequest) -> dict[str, Any]:
|
|
28
|
+
"""Bragg / Q↔2θ conversion. ``mode`` selects the quantity (see calc.xray)."""
|
|
29
|
+
try:
|
|
30
|
+
return xray_calc(req.mode, req.wavelength, req.value, req.n)
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Field (Oe),Moment (emu)
|
|
2
|
+
-5000.0,-4.0000
|
|
3
|
+
-4750.0,-3.9500
|
|
4
|
+
-4500.0,-3.9000
|
|
5
|
+
-4250.0,-3.8500
|
|
6
|
+
-4000.0,-3.8000
|
|
7
|
+
-3750.0,-3.7500
|
|
8
|
+
-3500.0,-3.7000
|
|
9
|
+
-3250.0,-3.6500
|
|
10
|
+
-3000.0,-3.6000
|
|
11
|
+
-2750.0,-3.5500
|
|
12
|
+
-2500.0,-3.5000
|
|
13
|
+
-2250.0,-3.4500
|
|
14
|
+
-2000.0,-3.4000
|
|
15
|
+
-1750.0,-3.3500
|
|
16
|
+
-1500.0,-3.2999
|
|
17
|
+
-1250.0,-3.2498
|
|
18
|
+
-1000.0,-3.1993
|
|
19
|
+
-750.0,-3.1474
|
|
20
|
+
-500.0,-3.0910
|
|
21
|
+
-250.0,-3.0187
|
|
22
|
+
0.0,-2.8921
|
|
23
|
+
250.0,-2.5895
|
|
24
|
+
500.0,-1.8054
|
|
25
|
+
750.0,-0.2231
|
|
26
|
+
1000.0,1.5864
|
|
27
|
+
1250.0,2.6779
|
|
28
|
+
1500.0,3.1241
|
|
29
|
+
1750.0,3.2985
|
|
30
|
+
2000.0,3.3852
|
|
31
|
+
2250.0,3.4457
|
|
32
|
+
2500.0,3.4988
|
|
33
|
+
2750.0,3.5497
|
|
34
|
+
3000.0,3.5999
|
|
35
|
+
3250.0,3.6500
|
|
36
|
+
3500.0,3.7000
|
|
37
|
+
3750.0,3.7500
|
|
38
|
+
4000.0,3.8000
|
|
39
|
+
4250.0,3.8500
|
|
40
|
+
4500.0,3.9000
|
|
41
|
+
4750.0,3.9500
|
|
42
|
+
5000.0,4.0000
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Desktop (``--desktop``, pywebview) and dev (``--dev``, Vite HMR) launch paths.
|
|
2
|
+
|
|
3
|
+
Adapted from fermiviewer ``server_launch.py`` + ``netprobe.py`` (shared
|
|
4
|
+
platform code — keep in sync). Split out of ``cli.py`` to respect the
|
|
5
|
+
500-line god-module ceiling; ``main()``/arg parsing stay in cli.py and only
|
|
6
|
+
its ``--dev`` / ``--desktop`` branches (plus the health-polled browser open
|
|
7
|
+
on the default path) reach into this module.
|
|
8
|
+
|
|
9
|
+
The probe helpers (``_health_ok``/``_bind``) are pure stdlib so importing
|
|
10
|
+
this module stays cheap — uvicorn/pywebview are imported inside the launch
|
|
11
|
+
functions that need them.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import socket as _socket
|
|
23
|
+
|
|
24
|
+
__all__ = ["_open_when_healthy", "_resolve_port", "_run_desktop", "_run_dev"]
|
|
25
|
+
|
|
26
|
+
# Built SPA — same resolution as quantized.app._WEB_DIR / cli._WEB_DIR.
|
|
27
|
+
_WEB_DIR = Path(__file__).parent / "web"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _frontend_dir() -> Path:
|
|
31
|
+
"""The repo-checkout ``frontend/`` (``--dev`` needs sources, not a build)."""
|
|
32
|
+
return Path(__file__).resolve().parents[2] / "frontend"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _health_ok(host: str, port: int, timeout: float = 0.4) -> bool:
|
|
36
|
+
"""True iff a *quantized* server answers /api/health with ``status: ok`` —
|
|
37
|
+
tells our own instance apart from a foreign app on the port, and gates
|
|
38
|
+
the browser/window open on the server actually being up."""
|
|
39
|
+
import json
|
|
40
|
+
import urllib.request
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with urllib.request.urlopen(
|
|
44
|
+
f"http://{host}:{port}/api/health", timeout=timeout
|
|
45
|
+
) as r:
|
|
46
|
+
if r.status != 200:
|
|
47
|
+
return False
|
|
48
|
+
data = json.loads(r.read())
|
|
49
|
+
return bool(isinstance(data, dict) and data.get("status") == "ok")
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _bind(host: str, port: int) -> _socket.socket | None:
|
|
55
|
+
"""Bind + listen on host:port, returning the live socket, or None if the
|
|
56
|
+
port is taken. Binding up front turns a busy port into a value we can
|
|
57
|
+
branch on (reuse a healthy sibling, refuse a foreign app) instead of an
|
|
58
|
+
'address already in use' traceback; the socket is handed to
|
|
59
|
+
``Server.run(sockets=[...])``, closing the check->bind race."""
|
|
60
|
+
import os
|
|
61
|
+
import socket
|
|
62
|
+
|
|
63
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
64
|
+
try:
|
|
65
|
+
# SO_REUSEADDR everywhere EXCEPT Windows, where it acts like
|
|
66
|
+
# SO_REUSEPORT and would let us bind a port a foreign server owns.
|
|
67
|
+
if os.name != "nt":
|
|
68
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
69
|
+
s.bind((host, port))
|
|
70
|
+
s.listen()
|
|
71
|
+
return s
|
|
72
|
+
except OSError:
|
|
73
|
+
s.close()
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve_port(host: str, port: int, *, explicit: bool) -> int:
|
|
78
|
+
"""Return the port to actually bind: ``port`` unchanged if it's free (or
|
|
79
|
+
if the caller passed ``--port`` explicitly — an explicit port that's busy
|
|
80
|
+
still errors the same way it does today, via the caller's own bind /
|
|
81
|
+
``uvicorn.run``). Otherwise (default port, busy — the main app is often
|
|
82
|
+
already running on 8000, MAIN_PLAN #22) probe for a free OS-assigned
|
|
83
|
+
ephemeral port and return that instead, printing a note so the user
|
|
84
|
+
knows where the server actually came up.
|
|
85
|
+
|
|
86
|
+
Skips the bind probe entirely when ``explicit`` is set, so passing an
|
|
87
|
+
explicit ``--port`` never touches a socket here.
|
|
88
|
+
"""
|
|
89
|
+
if explicit:
|
|
90
|
+
return port
|
|
91
|
+
|
|
92
|
+
sock = _bind(host, port)
|
|
93
|
+
if sock is not None:
|
|
94
|
+
sock.close()
|
|
95
|
+
return port
|
|
96
|
+
|
|
97
|
+
import socket
|
|
98
|
+
|
|
99
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as free:
|
|
100
|
+
free.bind((host, 0))
|
|
101
|
+
fallback = int(free.getsockname()[1])
|
|
102
|
+
print(f"[qz] port {port} is busy - using {fallback} instead")
|
|
103
|
+
return fallback
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _open_browser_later(url: str, delay: float = 2.0) -> None:
|
|
107
|
+
"""Fixed-delay browser open — used only for the dev path, where the
|
|
108
|
+
target is the Vite server (no /api/health to poll)."""
|
|
109
|
+
import webbrowser
|
|
110
|
+
|
|
111
|
+
timer = threading.Timer(delay, webbrowser.open, [url])
|
|
112
|
+
timer.daemon = True # don't keep the process alive on Ctrl+C in --dev
|
|
113
|
+
timer.start()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _open_when_healthy(url: str, host: str, port: int, timeout: float = 30.0) -> None:
|
|
117
|
+
"""Open the browser only once the server answers — a fixed delay races a
|
|
118
|
+
cold numpy/scipy/matplotlib import and shows 'can't reach this page'.
|
|
119
|
+
Polls /api/health in a daemon thread; opens anyway after the timeout."""
|
|
120
|
+
import webbrowser
|
|
121
|
+
|
|
122
|
+
def _wait_then_open() -> None:
|
|
123
|
+
deadline = time.monotonic() + timeout
|
|
124
|
+
while time.monotonic() < deadline:
|
|
125
|
+
if _health_ok(host, port):
|
|
126
|
+
webbrowser.open(url)
|
|
127
|
+
return
|
|
128
|
+
time.sleep(0.25)
|
|
129
|
+
webbrowser.open(url) # last resort: open anyway after the timeout
|
|
130
|
+
|
|
131
|
+
threading.Thread(target=_wait_then_open, daemon=True).start()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_WEBVIEW_HINT = (
|
|
135
|
+
"--desktop needs pywebview: pip install quantized[desktop]\n"
|
|
136
|
+
"On Linux it also needs a native GUI backend (PyGObject + WebKitGTK, "
|
|
137
|
+
"e.g. `sudo apt install python3-gi gir1.2-webkit2-4.1`, or PyQt5/PySide2)."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _run_desktop(
|
|
142
|
+
host: str,
|
|
143
|
+
port: int,
|
|
144
|
+
*,
|
|
145
|
+
title: str = "Quantized",
|
|
146
|
+
width: int = 1440,
|
|
147
|
+
height: int = 920,
|
|
148
|
+
path: str = "",
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Native window: uvicorn in a thread, pywebview on top — pure Python, no
|
|
151
|
+
Rust toolchain (the Tauri shell in src-tauri/ is the packaged path).
|
|
152
|
+
Closing the window stops the server.
|
|
153
|
+
|
|
154
|
+
``title``/``width``/``height``/``path`` let the ``--calc`` combo
|
|
155
|
+
(MAIN_PLAN #22) open a smaller window titled "DiraCulator" at
|
|
156
|
+
``/?view=calc`` instead of the default full-app window; defaults are
|
|
157
|
+
the plain ``qz --desktop`` window unchanged."""
|
|
158
|
+
if not _WEB_DIR.is_dir():
|
|
159
|
+
print(
|
|
160
|
+
f"[qz] UI not built ({_WEB_DIR} missing). "
|
|
161
|
+
"Run: cd frontend && npm run build (or use the run launcher)."
|
|
162
|
+
)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
import webview
|
|
167
|
+
except ImportError as e:
|
|
168
|
+
print(f"[qz] {_WEBVIEW_HINT}\n(import error: {e})")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
import uvicorn
|
|
172
|
+
|
|
173
|
+
from quantized.app import app
|
|
174
|
+
|
|
175
|
+
# Bind up front so a taken port is a clean branch, not a crashed server
|
|
176
|
+
# thread: reuse our own healthy instance (point the window at it), or
|
|
177
|
+
# refuse a foreign app instead of hanging 30 s on a dead window.
|
|
178
|
+
sock = _bind(host, port)
|
|
179
|
+
server: uvicorn.Server | None = None
|
|
180
|
+
t: threading.Thread | None = None
|
|
181
|
+
if sock is None:
|
|
182
|
+
if not _health_ok(host, port):
|
|
183
|
+
print(f"[qz] port {port} is in use by another app - close it and retry")
|
|
184
|
+
return
|
|
185
|
+
else:
|
|
186
|
+
server = uvicorn.Server(
|
|
187
|
+
uvicorn.Config(app, host=host, port=port, log_level="warning")
|
|
188
|
+
)
|
|
189
|
+
s = sock # bound socket handed to uvicorn — closes the bind race
|
|
190
|
+
t = threading.Thread(target=lambda: server.run(sockets=[s]), daemon=True)
|
|
191
|
+
t.start()
|
|
192
|
+
|
|
193
|
+
# Wait for the server to answer before pointing the window at it, else
|
|
194
|
+
# the webview shows a connection-refused page and never retries.
|
|
195
|
+
deadline = time.monotonic() + 30.0
|
|
196
|
+
while time.monotonic() < deadline and not _health_ok(host, port):
|
|
197
|
+
time.sleep(0.25)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
webview.create_window(
|
|
201
|
+
title,
|
|
202
|
+
f"http://{host}:{port}{path}",
|
|
203
|
+
width=width,
|
|
204
|
+
height=height,
|
|
205
|
+
background_color="#121116", # dark --surface-0 (oklch 0.16 0.008 280)
|
|
206
|
+
)
|
|
207
|
+
webview.start()
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f"[qz] {_WEBVIEW_HINT}\n(error: {e})")
|
|
210
|
+
finally:
|
|
211
|
+
if server is not None:
|
|
212
|
+
server.should_exit = True
|
|
213
|
+
if t is not None:
|
|
214
|
+
t.join(timeout=5)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _run_dev(host: str, port: int) -> None:
|
|
218
|
+
"""Vite dev server (HMR) + auto-reloading uvicorn in one terminal."""
|
|
219
|
+
import os
|
|
220
|
+
import subprocess
|
|
221
|
+
|
|
222
|
+
import uvicorn
|
|
223
|
+
|
|
224
|
+
frontend = _frontend_dir()
|
|
225
|
+
if not frontend.is_dir():
|
|
226
|
+
print(f"[qz] --dev requires a source checkout; frontend/ not found at {frontend}")
|
|
227
|
+
raise SystemExit(2)
|
|
228
|
+
npm = "npm.cmd" if os.name == "nt" else "npm"
|
|
229
|
+
# Tell the Vite proxy which backend port to target (vite.config.ts reads
|
|
230
|
+
# QZ_BACKEND_PORT; review 2026-07-11: it was hardcoded to 8000, so
|
|
231
|
+
# `qz --dev --port 9000` silently proxied /api to the WRONG server).
|
|
232
|
+
env = dict(os.environ, QZ_BACKEND_PORT=str(port))
|
|
233
|
+
vite = subprocess.Popen([npm, "run", "dev"], cwd=frontend, env=env)
|
|
234
|
+
_open_browser_later("http://localhost:5173")
|
|
235
|
+
try:
|
|
236
|
+
uvicorn.run("quantized.app:app", host=host, port=port, reload=True)
|
|
237
|
+
finally:
|
|
238
|
+
# Windows: terminate() only kills the npm.cmd wrapper and orphans the
|
|
239
|
+
# node/Vite child holding :5173 (review 2026-07-11) - kill the tree.
|
|
240
|
+
if os.name == "nt":
|
|
241
|
+
subprocess.run(
|
|
242
|
+
["taskkill", "/T", "/F", "/PID", str(vite.pid)],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
check=False,
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
vite.terminate()
|
|
248
|
+
try:
|
|
249
|
+
vite.wait(timeout=10)
|
|
250
|
+
except subprocess.TimeoutExpired:
|
|
251
|
+
pass # never mask the real exit path from inside finally
|
|
Binary file
|
|
Binary file
|