quantized-lab 0.8.0__py3-none-any.whl

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