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
quantized/calc/units.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Physical unit conversions. Port of utilities.convertUnits.
|
|
2
|
+
|
|
3
|
+
Pure calc layer. Multiplicative families (magnetic field, magnetic moment, angle,
|
|
4
|
+
length) convert through a shared base unit; temperature is affine (via Kelvin).
|
|
5
|
+
Unit tokens are case-insensitive; ``/`` and other non-alphanumerics are mapped to
|
|
6
|
+
``_`` to match MATLAB's ``makeValidName`` keying (so ``"A/m"`` -> ``a_m``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from numpy.typing import ArrayLike, NDArray
|
|
16
|
+
|
|
17
|
+
__all__ = ["convert_units"]
|
|
18
|
+
|
|
19
|
+
_FOUR_PI = 4.0 * math.pi
|
|
20
|
+
|
|
21
|
+
# value * table[from] -> base unit; base / table[to] -> target.
|
|
22
|
+
_FIELD = {"oe": 1000.0 / _FOUR_PI, "t": 1e4 / _FOUR_PI, "mt": 10.0 / _FOUR_PI, "a_m": 1.0}
|
|
23
|
+
_FIELD_CANON = {"oe": "Oe", "t": "T", "mt": "mT", "a_m": "A/m"}
|
|
24
|
+
_MOMENT = {"emu": 1e-3, "a_m2": 1.0, "j_t": 1.0, "memu": 1e-6}
|
|
25
|
+
_MOMENT_CANON = {"emu": "emu", "a_m2": "A·m²", "j_t": "J/T", "memu": "memu"}
|
|
26
|
+
_ANGLE = {"deg": math.pi / 180.0, "rad": 1.0}
|
|
27
|
+
_ANGLE_CANON = {"deg": "deg", "rad": "rad"}
|
|
28
|
+
_LENGTH = {"nm": 1e-9, "um": 1e-6, "mm": 1e-3, "cm": 1e-2, "m": 1.0, "ang": 1e-10}
|
|
29
|
+
_LENGTH_CANON = {"nm": "nm", "um": "µm", "mm": "mm", "cm": "cm", "m": "m", "ang": "Å"}
|
|
30
|
+
_TABLES = (
|
|
31
|
+
(_FIELD, _FIELD_CANON),
|
|
32
|
+
(_MOMENT, _MOMENT_CANON),
|
|
33
|
+
(_ANGLE, _ANGLE_CANON),
|
|
34
|
+
(_LENGTH, _LENGTH_CANON),
|
|
35
|
+
)
|
|
36
|
+
_TEMP = {"k", "c", "f"}
|
|
37
|
+
_TEMP_CANON = {"k": "K", "c": "°C", "f": "°F"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _key(unit: str) -> str:
|
|
41
|
+
return re.sub(r"[^a-z0-9]", "_", unit.strip().lower())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _to_kelvin(v: NDArray[np.float64], unit: str) -> NDArray[np.float64]:
|
|
45
|
+
if unit == "k":
|
|
46
|
+
return v
|
|
47
|
+
if unit == "c":
|
|
48
|
+
return np.asarray(v + 273.15, dtype=float)
|
|
49
|
+
return np.asarray((v - 32.0) * 5.0 / 9.0 + 273.15, dtype=float) # f
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _from_kelvin(k: NDArray[np.float64], unit: str) -> NDArray[np.float64]:
|
|
53
|
+
if unit == "k":
|
|
54
|
+
return k
|
|
55
|
+
if unit == "c":
|
|
56
|
+
return np.asarray(k - 273.15, dtype=float)
|
|
57
|
+
return np.asarray((k - 273.15) * 9.0 / 5.0 + 32.0, dtype=float) # f
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def convert_units(
|
|
61
|
+
value: ArrayLike, from_unit: str, to_unit: str
|
|
62
|
+
) -> tuple[NDArray[np.float64], str]:
|
|
63
|
+
"""Convert ``value`` from ``from_unit`` to ``to_unit``.
|
|
64
|
+
|
|
65
|
+
Returns ``(converted_array, canonical_unit_string)``. Same-unit conversions
|
|
66
|
+
return the value unchanged with the lowercased target token (matching MATLAB).
|
|
67
|
+
Raises ``ValueError`` if the units are unknown or belong to different families.
|
|
68
|
+
"""
|
|
69
|
+
val = np.asarray(value, dtype=float)
|
|
70
|
+
f = from_unit.strip().lower()
|
|
71
|
+
t = to_unit.strip().lower()
|
|
72
|
+
if f == t:
|
|
73
|
+
return val, t
|
|
74
|
+
if f in _TEMP and t in _TEMP:
|
|
75
|
+
return _from_kelvin(_to_kelvin(val, f), t), _TEMP_CANON[t]
|
|
76
|
+
fk, tk = _key(from_unit), _key(to_unit)
|
|
77
|
+
for table, canon in _TABLES:
|
|
78
|
+
if fk in table and tk in table:
|
|
79
|
+
return np.asarray(val * table[fk] / table[tk], dtype=float), canon[tk]
|
|
80
|
+
raise ValueError(f'cannot convert "{f}" -> "{t}"')
|
quantized/calc/vacuum.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
r"""Vacuum-science calculators (DiraCulator ``buildVacuumTab`` +
|
|
2
|
+
``+calc/+vacuum``).
|
|
3
|
+
|
|
4
|
+
Pure calc layer — closed-form scalars in, result dicts out. No fastapi /
|
|
5
|
+
pydantic imports. Ports the six MATLAB ``calc.vacuum`` functions verbatim:
|
|
6
|
+
|
|
7
|
+
.. math::
|
|
8
|
+
|
|
9
|
+
\lambda = \frac{k_B T}{\sqrt{2}\,\pi d^2 P} \qquad
|
|
10
|
+
t_{\mathrm{mono}} = \frac{1}{J\,A_{\mathrm{site}}},\;
|
|
11
|
+
J = \frac{P}{\sqrt{2\pi m k_B T}} \\
|
|
12
|
+
K_n = \frac{\lambda}{L} \qquad
|
|
13
|
+
t = \frac{V}{S}\,\ln\!\frac{P_0}{P_f} \\
|
|
14
|
+
C_{\mathrm{mol}} = \frac{\pi d^3}{12 L}\sqrt{\frac{8 k_B T}{\pi m}}
|
|
15
|
+
\qquad
|
|
16
|
+
C_{\mathrm{visc}} = \frac{\pi d^4}{128\,\eta L}\,\frac{P_1+P_2}{2}
|
|
17
|
+
|
|
18
|
+
SI units throughout (pressure Pa, length m, mass kg, temperature K), except
|
|
19
|
+
pump-down volume/speed in L and L/s and conductances reported in L/s, matching
|
|
20
|
+
the MATLAB toolbox (the behavioural reference). The sputter-yield lookup is the
|
|
21
|
+
Yamamura & Tawara (1996) / Matsunami approximate table for Ar ions at 200, 500,
|
|
22
|
+
1000, 5000 eV; linear interpolation in-range, NaN outside (no extrapolation).
|
|
23
|
+
|
|
24
|
+
Reference values (closed-form / MATLAB docstring examples):
|
|
25
|
+
- ``mean_free_path(1e-4) -> mfp ≈ 70.36`` m (N2, 300 K)
|
|
26
|
+
- ``knudsen_number(0.05, 0.1) -> Kn = 0.5`` (transition)
|
|
27
|
+
- ``pump_down_time(50, 100, 1e5, 1e-4) -> t ≈ 10.36`` s
|
|
28
|
+
- ``sputter_yield("Cu", 500) -> Y = 3.0`` atoms/ion
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from quantized.calc.constants import constants
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"gas_flow",
|
|
42
|
+
"knudsen_number",
|
|
43
|
+
"mean_free_path",
|
|
44
|
+
"monolayer_time",
|
|
45
|
+
"pump_down_time",
|
|
46
|
+
"sputter_yield",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# N2 molecular diameter (m) — default gas for mean free path / gas flow.
|
|
50
|
+
_D_N2 = 3.64e-10
|
|
51
|
+
# N2 molecular mass (kg, ~28 amu) — default for monolayer time / gas flow.
|
|
52
|
+
_M_N2 = 4.65e-26
|
|
53
|
+
# Default adsorption-site area (m^2) for the Langmuir monolayer model.
|
|
54
|
+
_A_SITE = 1e-19
|
|
55
|
+
# Dynamic viscosity of N2 at 300 K (Pa*s) — constant approximation in gasFlow.
|
|
56
|
+
_ETA_N2 = 1.8e-5
|
|
57
|
+
|
|
58
|
+
# Yamamura & Tawara (1996) / Matsunami approximate Ar-ion sputter yields.
|
|
59
|
+
# Energy grid (eV) shared by every material; do-not-"fix" calibrated values.
|
|
60
|
+
_SPUTTER_ENERGIES = (200.0, 500.0, 1000.0, 5000.0)
|
|
61
|
+
_SPUTTER_YIELDS: dict[str, tuple[float, float, float, float]] = {
|
|
62
|
+
"si": (0.4, 0.9, 1.2, 1.4),
|
|
63
|
+
"cu": (1.5, 3.0, 4.0, 4.5),
|
|
64
|
+
"fe": (0.8, 1.6, 2.2, 2.6),
|
|
65
|
+
"au": (1.5, 3.2, 4.4, 5.0),
|
|
66
|
+
"ti": (0.3, 0.7, 1.1, 1.4),
|
|
67
|
+
"sio2": (0.3, 0.7, 1.0, 1.2),
|
|
68
|
+
"ni": (1.0, 2.2, 3.0, 3.5),
|
|
69
|
+
"al": (0.5, 1.1, 1.6, 1.8),
|
|
70
|
+
"pt": (0.8, 1.8, 2.5, 3.0),
|
|
71
|
+
"w": (0.3, 0.7, 1.0, 1.3),
|
|
72
|
+
"ta": (0.3, 0.6, 0.9, 1.2),
|
|
73
|
+
"cr": (0.7, 1.5, 2.1, 2.5),
|
|
74
|
+
"mo": (0.5, 1.1, 1.5, 1.8),
|
|
75
|
+
"ag": (1.8, 3.5, 4.8, 5.5),
|
|
76
|
+
"gaas": (0.9, 1.8, 2.5, 2.9),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _regime(kn: float) -> str:
|
|
81
|
+
"""Flow regime from the Knudsen number (MATLAB ``knudsenNumber`` thresholds).
|
|
82
|
+
|
|
83
|
+
``Kn > 1`` molecular, ``Kn >= 0.01`` transition, else viscous.
|
|
84
|
+
"""
|
|
85
|
+
if kn > 1:
|
|
86
|
+
return "molecular"
|
|
87
|
+
if kn >= 0.01:
|
|
88
|
+
return "transition"
|
|
89
|
+
return "viscous"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mean_free_path(
|
|
93
|
+
p: float, *, temperature: float = 300.0, d: float = _D_N2
|
|
94
|
+
) -> dict[str, float]:
|
|
95
|
+
"""Mean free path λ = k_B·T / (√2·π·d²·P) (m).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
p: pressure (Pa), > 0.
|
|
99
|
+
temperature: temperature (K), > 0. Default 300.
|
|
100
|
+
d: molecular diameter (m), > 0. Default 3.64e-10 (N2).
|
|
101
|
+
|
|
102
|
+
>>> round(mean_free_path(1e-4)["mfp"], 2)
|
|
103
|
+
70.36
|
|
104
|
+
"""
|
|
105
|
+
if p <= 0 or temperature <= 0 or d <= 0:
|
|
106
|
+
raise ValueError("P, T and d must be positive")
|
|
107
|
+
kb = constants()["kB"]
|
|
108
|
+
mfp = kb * temperature / (math.sqrt(2.0) * math.pi * d**2 * p)
|
|
109
|
+
return {
|
|
110
|
+
"mfp": mfp,
|
|
111
|
+
"mfpMm": mfp * 1e3,
|
|
112
|
+
"mfpUm": mfp * 1e6,
|
|
113
|
+
"P": p,
|
|
114
|
+
"T": temperature,
|
|
115
|
+
"d": d,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def monolayer_time(
|
|
120
|
+
p: float,
|
|
121
|
+
*,
|
|
122
|
+
m: float = _M_N2,
|
|
123
|
+
temperature: float = 300.0,
|
|
124
|
+
a_site: float = _A_SITE,
|
|
125
|
+
) -> dict[str, float]:
|
|
126
|
+
"""Monolayer formation time (Langmuir model), t = 1/(J·A_site) (s).
|
|
127
|
+
|
|
128
|
+
Impingement flux ``J = P / sqrt(2·π·m·k_B·T)`` (molecules/m²/s).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
p: pressure (Pa), > 0.
|
|
132
|
+
m: molecular mass (kg), > 0. Default 4.65e-26 (N2).
|
|
133
|
+
temperature: temperature (K), > 0. Default 300.
|
|
134
|
+
a_site: adsorption-site area (m²), > 0. Default 1e-19.
|
|
135
|
+
|
|
136
|
+
>>> round(monolayer_time(1.33e-4)["tMono"], 4)
|
|
137
|
+
2.6156
|
|
138
|
+
"""
|
|
139
|
+
if p <= 0 or m <= 0 or temperature <= 0 or a_site <= 0:
|
|
140
|
+
raise ValueError("P, m, T and A_site must be positive")
|
|
141
|
+
kb = constants()["kB"]
|
|
142
|
+
flux = p / math.sqrt(2.0 * math.pi * m * kb * temperature)
|
|
143
|
+
t_mono = 1.0 / (flux * a_site)
|
|
144
|
+
return {"tMono": t_mono, "flux": flux, "P": p, "T": temperature}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def knudsen_number(mfp: float, length: float) -> dict[str, Any]:
|
|
148
|
+
"""Knudsen number Kn = λ/L and flow regime.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
mfp: mean free path (m), > 0.
|
|
152
|
+
length: characteristic length (m), > 0.
|
|
153
|
+
|
|
154
|
+
>>> r = knudsen_number(0.05, 0.1)
|
|
155
|
+
>>> round(r["Kn"], 3), r["regime"]
|
|
156
|
+
(0.5, 'transition')
|
|
157
|
+
"""
|
|
158
|
+
if mfp <= 0 or length <= 0:
|
|
159
|
+
raise ValueError("mfp and L must be positive")
|
|
160
|
+
kn = mfp / length
|
|
161
|
+
return {"Kn": kn, "regime": _regime(kn), "mfp": mfp, "L": length}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def pump_down_time(v: float, s: float, p0: float, pf: float) -> dict[str, float]:
|
|
165
|
+
"""Ideal exponential pump-down time t = (V/S)·ln(P0/Pf) (s).
|
|
166
|
+
|
|
167
|
+
Constant pump speed, no outgassing. Time constant τ = V/S.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
v: chamber volume (L), > 0.
|
|
171
|
+
s: pump speed (L/s), > 0.
|
|
172
|
+
p0: initial pressure (Pa), > 0.
|
|
173
|
+
pf: final (target) pressure (Pa), > 0 and < p0.
|
|
174
|
+
|
|
175
|
+
>>> round(pump_down_time(50, 100, 1e5, 1e-4)["time"], 6)
|
|
176
|
+
10.361633
|
|
177
|
+
"""
|
|
178
|
+
if v <= 0 or s <= 0 or p0 <= 0 or pf <= 0:
|
|
179
|
+
raise ValueError("V, S, P0 and Pf must be positive")
|
|
180
|
+
if pf >= p0:
|
|
181
|
+
raise ValueError("Final pressure Pf must be less than initial pressure P0.")
|
|
182
|
+
tau = v / s
|
|
183
|
+
t = tau * math.log(p0 / pf)
|
|
184
|
+
return {
|
|
185
|
+
"time": t,
|
|
186
|
+
"timeMin": t / 60.0,
|
|
187
|
+
"tau": tau,
|
|
188
|
+
"V": v,
|
|
189
|
+
"S": s,
|
|
190
|
+
"P0": p0,
|
|
191
|
+
"Pf": pf,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def sputter_yield(material: str, energy: float, *, ion: str = "Ar") -> dict[str, Any]:
|
|
196
|
+
"""Sputter yield (atoms/ion) from the Ar-ion lookup table.
|
|
197
|
+
|
|
198
|
+
Linear interpolation in energy between the tabulated [200, 500, 1000, 5000]
|
|
199
|
+
eV grid; ``Y = NaN`` outside the range or for unknown material/ion (no
|
|
200
|
+
extrapolation, matching MATLAB ``sputterYield``).
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
material: target symbol (case-insensitive), e.g. ``"Si"``, ``"Cu"``.
|
|
204
|
+
energy: ion energy (eV), > 0.
|
|
205
|
+
ion: ion species. Only ``"Ar"`` is tabulated; others return NaN.
|
|
206
|
+
|
|
207
|
+
>>> sputter_yield("Cu", 500)["Y"]
|
|
208
|
+
3.0
|
|
209
|
+
"""
|
|
210
|
+
if energy <= 0:
|
|
211
|
+
raise ValueError("energy must be positive")
|
|
212
|
+
|
|
213
|
+
def _nan(mat: str, ion_in: str) -> dict[str, Any]:
|
|
214
|
+
return {"Y": float("nan"), "material": mat, "ion": ion_in, "energy": energy}
|
|
215
|
+
|
|
216
|
+
if ion.strip().lower() != "ar":
|
|
217
|
+
return _nan(material, ion)
|
|
218
|
+
|
|
219
|
+
entry = _SPUTTER_YIELDS.get(material.strip().lower())
|
|
220
|
+
if entry is None:
|
|
221
|
+
return _nan(material, ion)
|
|
222
|
+
|
|
223
|
+
grid = _SPUTTER_ENERGIES
|
|
224
|
+
if energy < grid[0] or energy > grid[-1]:
|
|
225
|
+
return _nan(material, ion)
|
|
226
|
+
|
|
227
|
+
y = float(np.interp(energy, grid, entry))
|
|
228
|
+
return {"Y": y, "material": material, "ion": ion, "energy": energy}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def gas_flow(
|
|
232
|
+
p1: float,
|
|
233
|
+
p2: float,
|
|
234
|
+
d: float,
|
|
235
|
+
length: float,
|
|
236
|
+
*,
|
|
237
|
+
temperature: float = 300.0,
|
|
238
|
+
m: float = _M_N2,
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
"""Molecular & viscous gas-flow conductance through a tube (L/s).
|
|
241
|
+
|
|
242
|
+
Molecular (Knudsen): ``C_mol = (π d³/12L)·sqrt(8 k_B T/(π m))``.
|
|
243
|
+
Viscous (Hagen-Poiseuille): ``C_visc = (π d⁴/128 η L)·(P1+P2)/2``.
|
|
244
|
+
The Knudsen number (mean free path at the mean pressure, length = d)
|
|
245
|
+
selects the regime; throughput ``Q = C_eff·(P1-P2)``. In the transition
|
|
246
|
+
regime the additive ``C_mol + C_visc`` is used (matching MATLAB).
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
p1: upstream pressure (Pa), > 0.
|
|
250
|
+
p2: downstream pressure (Pa), > 0.
|
|
251
|
+
d: tube inner diameter (m), > 0.
|
|
252
|
+
length: tube length (m), > 0.
|
|
253
|
+
temperature: temperature (K), > 0. Default 300.
|
|
254
|
+
m: molecular mass (kg), > 0. Default 4.65e-26 (N2).
|
|
255
|
+
|
|
256
|
+
Returns a dict with ``Cmol``/``Cvisc`` (L/s), ``throughput`` (Pa·L/s),
|
|
257
|
+
``Kn`` and ``regime``.
|
|
258
|
+
"""
|
|
259
|
+
if p1 <= 0 or p2 <= 0 or d <= 0 or length <= 0:
|
|
260
|
+
raise ValueError("P1, P2, d and L must be positive")
|
|
261
|
+
if temperature <= 0 or m <= 0:
|
|
262
|
+
raise ValueError("T and m must be positive")
|
|
263
|
+
kb = constants()["kB"]
|
|
264
|
+
|
|
265
|
+
cmol_m3s = (math.pi * d**3 / (12.0 * length)) * math.sqrt(
|
|
266
|
+
8.0 * kb * temperature / (math.pi * m)
|
|
267
|
+
)
|
|
268
|
+
p_mean = (p1 + p2) / 2.0
|
|
269
|
+
cvisc_m3s = (math.pi * d**4 / (128.0 * _ETA_N2 * length)) * p_mean
|
|
270
|
+
|
|
271
|
+
# Knudsen number at mean pressure (N2 diameter), characteristic length = d.
|
|
272
|
+
mfp = mean_free_path(p_mean, temperature=temperature, d=_D_N2)["mfp"]
|
|
273
|
+
kn_res = knudsen_number(mfp, d)
|
|
274
|
+
regime = kn_res["regime"]
|
|
275
|
+
|
|
276
|
+
if regime == "molecular":
|
|
277
|
+
c_eff = cmol_m3s
|
|
278
|
+
elif regime == "viscous":
|
|
279
|
+
c_eff = cvisc_m3s
|
|
280
|
+
else:
|
|
281
|
+
c_eff = cmol_m3s + cvisc_m3s
|
|
282
|
+
throughput = c_eff * (p1 - p2) * 1e3 # Pa*(m^3/s) -> Pa*L/s
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"Cmol": cmol_m3s * 1e3,
|
|
286
|
+
"Cvisc": cvisc_m3s * 1e3,
|
|
287
|
+
"throughput": throughput,
|
|
288
|
+
"Kn": kn_res["Kn"],
|
|
289
|
+
"regime": regime,
|
|
290
|
+
}
|
quantized/calc/xray.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
r"""X-ray / neutron scattering scalar conversions (DiraCulator buildXrayNeutronTab).
|
|
2
|
+
|
|
3
|
+
Pure calc layer. Standard textbook relations between the X-ray (or neutron)
|
|
4
|
+
wavelength :math:`\lambda`, the Bragg angle, the detector angle :math:`2\theta`,
|
|
5
|
+
the interplanar spacing :math:`d`, and the scattering-vector magnitude
|
|
6
|
+
:math:`Q`. All lengths are in angstrom (Å), angles in degrees, :math:`Q` in
|
|
7
|
+
1/Å.
|
|
8
|
+
|
|
9
|
+
Bragg's law (order ``n``):
|
|
10
|
+
|
|
11
|
+
.. math::
|
|
12
|
+
|
|
13
|
+
n\lambda = 2 d \sin\theta, \qquad \theta = \tfrac{1}{2}\,(2\theta)
|
|
14
|
+
|
|
15
|
+
Scattering vector magnitude for an elastic event:
|
|
16
|
+
|
|
17
|
+
.. math::
|
|
18
|
+
|
|
19
|
+
Q = \frac{4\pi}{\lambda}\sin\theta
|
|
20
|
+
= \frac{2\pi n}{d}
|
|
21
|
+
|
|
22
|
+
so ``Q`` and ``d`` are reciprocal (``Q = 2*pi/d`` at first order), a handy
|
|
23
|
+
cross-check. The inverse forms invert ``arcsin`` and therefore require the
|
|
24
|
+
argument to lie in ``[-1, 1]`` — a reflection with ``n*lambda > 2 d`` (or
|
|
25
|
+
``Q*lambda > 4 pi``) is geometrically inaccessible at that wavelength and
|
|
26
|
+
raises ``ValueError``.
|
|
27
|
+
|
|
28
|
+
Reference value: Cu K-alpha (``lambda = 1.5406 Å``) on Si(111)
|
|
29
|
+
(``d = 3.1356 Å``) gives ``2theta ≈ 28.44°`` and ``Q ≈ 2.004 1/Å``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import math
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"bragg_d_spacing",
|
|
40
|
+
"bragg_two_theta",
|
|
41
|
+
"q_from_two_theta",
|
|
42
|
+
"two_theta_from_q",
|
|
43
|
+
"xray_calc",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_wavelength(wavelength_a: float) -> None:
|
|
48
|
+
if not (math.isfinite(wavelength_a) and wavelength_a > 0):
|
|
49
|
+
raise ValueError(f"wavelength must be positive and finite, got {wavelength_a!r}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _check_order(n: int) -> None:
|
|
53
|
+
if n < 1:
|
|
54
|
+
raise ValueError(f"diffraction order n must be a positive integer, got {n!r}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def bragg_d_spacing(wavelength_a: float, two_theta_deg: float, n: int = 1) -> float:
|
|
58
|
+
r"""Interplanar spacing ``d`` (Å) from the detector angle ``2theta`` (deg).
|
|
59
|
+
|
|
60
|
+
:math:`d = n\lambda / (2\sin\theta)` with :math:`\theta = (2\theta)/2`.
|
|
61
|
+
Requires ``0 < two_theta_deg < 180``.
|
|
62
|
+
|
|
63
|
+
>>> round(bragg_d_spacing(1.5406, 28.44), 3)
|
|
64
|
+
3.136
|
|
65
|
+
"""
|
|
66
|
+
_check_wavelength(wavelength_a)
|
|
67
|
+
_check_order(n)
|
|
68
|
+
if not (0.0 < two_theta_deg < 180.0):
|
|
69
|
+
raise ValueError(f"two_theta must be in (0, 180) deg, got {two_theta_deg!r}")
|
|
70
|
+
sin_theta = math.sin(math.radians(two_theta_deg) / 2.0)
|
|
71
|
+
return n * wavelength_a / (2.0 * sin_theta)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def bragg_two_theta(wavelength_a: float, d_a: float, n: int = 1) -> float:
|
|
75
|
+
r"""Detector angle ``2theta`` (deg) from the spacing ``d`` (Å), via Bragg's law.
|
|
76
|
+
|
|
77
|
+
:math:`2\theta = 2\arcsin\!\big(n\lambda / (2d)\big)`. Raises if the
|
|
78
|
+
reflection is inaccessible (``n*lambda > 2 d``).
|
|
79
|
+
|
|
80
|
+
>>> round(bragg_two_theta(1.5406, 3.1356), 2)
|
|
81
|
+
28.44
|
|
82
|
+
"""
|
|
83
|
+
_check_wavelength(wavelength_a)
|
|
84
|
+
_check_order(n)
|
|
85
|
+
if not (math.isfinite(d_a) and d_a > 0):
|
|
86
|
+
raise ValueError(f"d-spacing must be positive and finite, got {d_a!r}")
|
|
87
|
+
arg = n * wavelength_a / (2.0 * d_a)
|
|
88
|
+
if arg > 1.0:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"reflection inaccessible at this wavelength: n*lambda/(2d) = {arg:.4f} > 1"
|
|
91
|
+
)
|
|
92
|
+
return math.degrees(2.0 * math.asin(arg))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def q_from_two_theta(wavelength_a: float, two_theta_deg: float) -> float:
|
|
96
|
+
r"""Scattering-vector magnitude ``Q`` (1/Å) from ``2theta`` (deg).
|
|
97
|
+
|
|
98
|
+
:math:`Q = (4\pi/\lambda)\sin\theta`, :math:`\theta = (2\theta)/2`.
|
|
99
|
+
Requires ``0 <= two_theta_deg <= 180``.
|
|
100
|
+
|
|
101
|
+
>>> round(q_from_two_theta(1.5406, 28.44), 3)
|
|
102
|
+
2.004
|
|
103
|
+
"""
|
|
104
|
+
_check_wavelength(wavelength_a)
|
|
105
|
+
if not (0.0 <= two_theta_deg <= 180.0):
|
|
106
|
+
raise ValueError(f"two_theta must be in [0, 180] deg, got {two_theta_deg!r}")
|
|
107
|
+
return (4.0 * math.pi / wavelength_a) * math.sin(math.radians(two_theta_deg) / 2.0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def two_theta_from_q(wavelength_a: float, q_inv_a: float) -> float:
|
|
111
|
+
r"""Detector angle ``2theta`` (deg) from the scattering vector ``Q`` (1/Å).
|
|
112
|
+
|
|
113
|
+
:math:`2\theta = 2\arcsin\!\big(Q\lambda / (4\pi)\big)`. Raises if ``Q`` is
|
|
114
|
+
too large for the wavelength (``Q*lambda > 4 pi``).
|
|
115
|
+
|
|
116
|
+
>>> round(two_theta_from_q(1.5406, 2.004), 2)
|
|
117
|
+
28.44
|
|
118
|
+
"""
|
|
119
|
+
_check_wavelength(wavelength_a)
|
|
120
|
+
if not (math.isfinite(q_inv_a) and q_inv_a >= 0):
|
|
121
|
+
raise ValueError(f"Q must be non-negative and finite, got {q_inv_a!r}")
|
|
122
|
+
arg = q_inv_a * wavelength_a / (4.0 * math.pi)
|
|
123
|
+
if arg > 1.0:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Q inaccessible at this wavelength: Q*lambda/(4*pi) = {arg:.4f} > 1"
|
|
126
|
+
)
|
|
127
|
+
return math.degrees(2.0 * math.asin(arg))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Uniform (wavelength, value, n) wrappers for the order-free Q conversions so the
|
|
131
|
+
# dispatch table stays a typed dict of defs (no untyped lambdas, no eval).
|
|
132
|
+
def _q_from_2theta(wavelength_a: float, value: float, _n: int) -> float:
|
|
133
|
+
return q_from_two_theta(wavelength_a, value)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _2theta_from_q(wavelength_a: float, value: float, _n: int) -> float:
|
|
137
|
+
return two_theta_from_q(wavelength_a, value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Mode dispatch (no eval): name -> (callable(wavelength, value, n), result unit).
|
|
141
|
+
_MODES: dict[str, tuple[Callable[[float, float, int], float], str]] = {
|
|
142
|
+
"d_from_2theta": (bragg_d_spacing, "Å"),
|
|
143
|
+
"2theta_from_d": (bragg_two_theta, "deg"),
|
|
144
|
+
"q_from_2theta": (_q_from_2theta, "1/Å"),
|
|
145
|
+
"2theta_from_q": (_2theta_from_q, "deg"),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_MODE_DESC: dict[str, str] = {
|
|
149
|
+
"d_from_2theta": "interplanar spacing d from 2θ (Bragg)",
|
|
150
|
+
"2theta_from_d": "2θ from interplanar spacing d (Bragg)",
|
|
151
|
+
"q_from_2theta": "scattering vector |Q| from 2θ",
|
|
152
|
+
"2theta_from_q": "2θ from scattering vector |Q|",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def xray_calc(mode: str, wavelength_a: float, value: float, n: int = 1) -> dict[str, Any]:
|
|
157
|
+
"""Dispatch one X-ray/neutron scalar conversion. Returns result + unit + label.
|
|
158
|
+
|
|
159
|
+
``mode`` is one of ``d_from_2theta``, ``2theta_from_d``, ``q_from_2theta``,
|
|
160
|
+
``2theta_from_q``. ``value`` is the mode's input (2θ in deg, d in Å, or Q in
|
|
161
|
+
1/Å). ``n`` is the diffraction order (Bragg modes only). Raises ``ValueError``
|
|
162
|
+
on an unknown mode or out-of-domain input.
|
|
163
|
+
"""
|
|
164
|
+
entry = _MODES.get(mode)
|
|
165
|
+
if entry is None:
|
|
166
|
+
raise ValueError(f"unknown mode {mode!r}; expected one of {sorted(_MODES)}")
|
|
167
|
+
fn, unit = entry
|
|
168
|
+
result = fn(wavelength_a, value, n)
|
|
169
|
+
return {"result": result, "unit": unit, "description": _MODE_DESC[mode]}
|