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,400 @@
1
+ r"""Crystallographic geometry from lattice parameters (DiraCulator buildCrystalTab).
2
+
3
+ Pure calc layer. Three families of formula:
4
+
5
+ **Interplanar d-spacing** — the reciprocal quadratic forms ``1/d^2`` per crystal
6
+ system (lengths in Å, angles in degrees, ``h, k, l`` integers):
7
+
8
+ .. math::
9
+
10
+ \text{cubic:} \quad & \frac{1}{d^2} = \frac{h^2+k^2+l^2}{a^2} \\
11
+ \text{tetragonal:} \quad & \frac{1}{d^2} = \frac{h^2+k^2}{a^2} + \frac{l^2}{c^2} \\
12
+ \text{orthorhombic:} \quad & \frac{1}{d^2} = \frac{h^2}{a^2} + \frac{k^2}{b^2}
13
+ + \frac{l^2}{c^2} \\
14
+ \text{hexagonal:} \quad & \frac{1}{d^2}
15
+ = \frac{4}{3}\,\frac{h^2+hk+k^2}{a^2} + \frac{l^2}{c^2}
16
+
17
+ and the low-symmetry systems — rhombohedral (``a``, ``α``), monoclinic
18
+ (``a,b,c``, ``β``; unique axis b), and the general triclinic form via the
19
+ reciprocal metric tensor (covers every system as a special case).
20
+
21
+ **Cell volume** — the general triclinic
22
+ ``V = abc·sqrt(1 − cos²α − cos²β − cos²γ + 2 cosα cosβ cosγ)``.
23
+
24
+ **Theoretical (X-ray) density** — ``ρ = Z·M / (N_A · V)`` from the formula molar
25
+ mass ``M`` and formula units per cell ``Z``.
26
+
27
+ Pairs with :mod:`quantized.calc.xray`: once ``d`` is known, ``2θ`` follows from
28
+ Bragg's law (``xray.bragg_two_theta``).
29
+
30
+ Reference: Si (cubic, ``a = 5.4309 Å``), reflection (111) → ``d = 3.1356 Å``;
31
+ NaCl (cubic, ``a = 5.6402 Å``, ``Z = 4``) → ``ρ ≈ 2.16 g/cm³``.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import math
37
+ from collections.abc import Callable
38
+ from typing import Any
39
+
40
+ from quantized.calc.constants import constants
41
+
42
+ __all__ = [
43
+ "CRYSTAL_SYSTEMS",
44
+ "cell_volume",
45
+ "d_spacing",
46
+ "plane_spacings",
47
+ "theoretical_density",
48
+ ]
49
+
50
+
51
+ def _cos(deg: float) -> float:
52
+ return math.cos(math.radians(deg))
53
+
54
+
55
+ def _sin(deg: float) -> float:
56
+ return math.sin(math.radians(deg))
57
+
58
+
59
+ def _from_inv_d2(inv_d2: float) -> float:
60
+ """1/d^2 -> d, rejecting the all-zero (h,k,l) case (inv_d2 <= 0)."""
61
+ if inv_d2 <= 0:
62
+ raise ValueError("Miller indices (h, k, l) must not all be zero")
63
+ return 1.0 / math.sqrt(inv_d2)
64
+
65
+
66
+ def cell_volume(
67
+ a: float, b: float, c: float, alpha: float = 90.0, beta: float = 90.0, gamma: float = 90.0
68
+ ) -> float:
69
+ """Unit-cell volume (ų) from lattice lengths (Å) + angles (degrees).
70
+
71
+ The general triclinic form; every higher-symmetry system is a special case
72
+ (cubic ``a³``; hexagonal ``a²c·√3/2`` at ``γ=120``). Raises if the angles are
73
+ non-physical (the radicand ``≤ 0``).
74
+
75
+ >>> round(cell_volume(4.0, 4.0, 4.0), 6)
76
+ 64.0
77
+ """
78
+ for name, val in (("a", a), ("b", b), ("c", c)):
79
+ if not (math.isfinite(val) and val > 0):
80
+ raise ValueError(f"lattice length {name} must be positive and finite")
81
+ ca, cb, cg = _cos(alpha), _cos(beta), _cos(gamma)
82
+ radicand = 1.0 - ca * ca - cb * cb - cg * cg + 2.0 * ca * cb * cg
83
+ if radicand <= 0:
84
+ raise ValueError("non-physical cell angles (cell volume would be ≤ 0)")
85
+ return a * b * c * math.sqrt(radicand)
86
+
87
+
88
+ def theoretical_density(molar_mass: float, z: int, volume_a3: float) -> float:
89
+ r"""Theoretical (X-ray) density ``ρ = Z·M / (N_A · V)`` in g/cm³.
90
+
91
+ ``molar_mass`` in g/mol, ``z`` formula units per cell, ``volume_a3`` the cell
92
+ volume in ų (``1 ų = 10⁻²⁴ cm³``).
93
+
94
+ >>> round(theoretical_density(58.44, 4, 5.6402 ** 3), 3)
95
+ 2.163
96
+ """
97
+ if not (math.isfinite(molar_mass) and molar_mass > 0):
98
+ raise ValueError("molar mass must be positive and finite")
99
+ if z < 1:
100
+ raise ValueError("formula units per cell Z must be ≥ 1")
101
+ if not (math.isfinite(volume_a3) and volume_a3 > 0):
102
+ raise ValueError("cell volume must be positive and finite")
103
+ na = constants()["NA"]
104
+ return z * molar_mass / (na * volume_a3 * 1e-24)
105
+
106
+
107
+ # ── per-system 1/d² forms (all take the full cell; simple systems ignore angles)
108
+ def _cubic(
109
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
110
+ ) -> float:
111
+ return _from_inv_d2((h * h + k * k + l * l) / (a * a))
112
+
113
+
114
+ def _tetragonal(
115
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
116
+ ) -> float:
117
+ return _from_inv_d2((h * h + k * k) / (a * a) + (l * l) / (c * c))
118
+
119
+
120
+ def _orthorhombic(
121
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
122
+ ) -> float:
123
+ return _from_inv_d2((h * h) / (a * a) + (k * k) / (b * b) + (l * l) / (c * c))
124
+
125
+
126
+ def _hexagonal(
127
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
128
+ ) -> float:
129
+ return _from_inv_d2((4.0 / 3.0) * (h * h + h * k + k * k) / (a * a) + (l * l) / (c * c))
130
+
131
+
132
+ def _triclinic(
133
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
134
+ ) -> float:
135
+ """General reciprocal-metric-tensor form — exact for every system."""
136
+ ca, cb, cg = _cos(al), _cos(be), _cos(ga)
137
+ sa, sb, sg = _sin(al), _sin(be), _sin(ga)
138
+ vol = cell_volume(a, b, c, al, be, ga)
139
+ s11 = (b * c * sa) ** 2
140
+ s22 = (a * c * sb) ** 2
141
+ s33 = (a * b * sg) ** 2
142
+ s12 = a * b * c * c * (ca * cb - cg)
143
+ s23 = a * a * b * c * (cb * cg - ca)
144
+ s13 = a * b * b * c * (cg * ca - cb)
145
+ inv = (
146
+ s11 * h * h
147
+ + s22 * k * k
148
+ + s33 * l * l
149
+ + 2 * s12 * h * k
150
+ + 2 * s23 * k * l
151
+ + 2 * s13 * h * l
152
+ ) / (vol * vol)
153
+ return _from_inv_d2(inv)
154
+
155
+
156
+ def _rhombohedral(
157
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
158
+ ) -> float:
159
+ """Rhombohedral (a=b=c, α=β=γ) closed form (uses ``a`` and ``α`` only)."""
160
+ ca = _cos(al)
161
+ sin2 = _sin(al) ** 2
162
+ num = (h * h + k * k + l * l) * sin2 + 2.0 * (h * k + k * l + h * l) * (ca * ca - ca)
163
+ den = a * a * (1.0 - 3.0 * ca * ca + 2.0 * ca * ca * ca)
164
+ if den <= 0.0:
165
+ # den -> 0 at alpha = 120 deg (and < 0 past it): the rhombohedral cell
166
+ # volume collapses. Raise a specific error instead of silently returning
167
+ # d -> 0 or tripping _from_inv_d2's unrelated "hkl all zero" guard.
168
+ # (ASCII message so Windows cp1252 console/log handlers never choke.)
169
+ raise ValueError(
170
+ "degenerate rhombohedral cell: the angle alpha is at or beyond the "
171
+ "120 deg singularity (cell volume -> 0); require 0 < alpha < 120 deg"
172
+ )
173
+ return _from_inv_d2(num / den)
174
+
175
+
176
+ def _monoclinic(
177
+ a: float, b: float, c: float, al: float, be: float, ga: float, h: int, k: int, l: int
178
+ ) -> float:
179
+ """Monoclinic, unique axis b (α=γ=90, β free); uses ``a,b,c`` and ``β``."""
180
+ cb = _cos(be)
181
+ sin2 = _sin(be) ** 2
182
+ inv = (1.0 / sin2) * (
183
+ (h * h) / (a * a)
184
+ + (k * k * sin2) / (b * b)
185
+ + (l * l) / (c * c)
186
+ - (2.0 * h * l * cb) / (a * c)
187
+ )
188
+ return _from_inv_d2(inv)
189
+
190
+
191
+ # system -> (formula, lattice lengths used, lattice angles used). No eval; pure
192
+ # def dispatch. Only the listed params are validated/required for that system.
193
+ _Formula = Callable[[float, float, float, float, float, float, int, int, int], float]
194
+ _SYSTEMS: dict[str, tuple[_Formula, tuple[str, ...], tuple[str, ...]]] = {
195
+ "cubic": (_cubic, ("a",), ()),
196
+ "tetragonal": (_tetragonal, ("a", "c"), ()),
197
+ "orthorhombic": (_orthorhombic, ("a", "b", "c"), ()),
198
+ "hexagonal": (_hexagonal, ("a", "c"), ()),
199
+ "rhombohedral": (_rhombohedral, ("a",), ("alpha",)),
200
+ "monoclinic": (_monoclinic, ("a", "b", "c"), ("beta",)),
201
+ "triclinic": (_triclinic, ("a", "b", "c"), ("alpha", "beta", "gamma")),
202
+ }
203
+
204
+ CRYSTAL_SYSTEMS: tuple[str, ...] = tuple(_SYSTEMS)
205
+
206
+
207
+ def d_spacing(
208
+ system: str,
209
+ a: float,
210
+ b: float,
211
+ c: float,
212
+ h: int,
213
+ k: int,
214
+ l: int,
215
+ alpha: float = 90.0,
216
+ beta: float = 90.0,
217
+ gamma: float = 90.0,
218
+ ) -> dict[str, Any]:
219
+ """Interplanar spacing ``d`` (Å) for a reflection ``(h,k,l)`` in ``system``.
220
+
221
+ Only the lattice parameters relevant to ``system`` are used (and required to
222
+ be positive); the rest are ignored. ``system`` is one of ``cubic`` /
223
+ ``tetragonal`` / ``orthorhombic`` / ``hexagonal`` / ``rhombohedral`` /
224
+ ``monoclinic`` / ``triclinic`` (the last three take the relevant angle(s)).
225
+
226
+ >>> round(d_spacing("cubic", 4.0, 4.0, 4.0, 2, 0, 0)["d"], 6)
227
+ 2.0
228
+ """
229
+ entry = _SYSTEMS.get(system)
230
+ if entry is None:
231
+ raise ValueError(f"unknown crystal system {system!r}; expected one of {sorted(_SYSTEMS)}")
232
+ fn, needed_lengths, needed_angles = entry
233
+ lengths = {"a": a, "b": b, "c": c}
234
+ for name in needed_lengths:
235
+ val = lengths[name]
236
+ if not (math.isfinite(val) and val > 0):
237
+ raise ValueError(f"lattice parameter {name} must be positive and finite for {system}")
238
+ angles = {"alpha": alpha, "beta": beta, "gamma": gamma}
239
+ for name in needed_angles:
240
+ val = angles[name]
241
+ if not (math.isfinite(val) and 0.0 < val < 180.0):
242
+ raise ValueError(f"lattice angle {name} must be in (0, 180) degrees for {system}")
243
+ d = fn(a, b, c, alpha, beta, gamma, h, k, l)
244
+ return {"d": d, "system": system}
245
+
246
+
247
+ # ── Reflection enumeration (calc.crystal.planeSpacings) ───────────────────────
248
+ _CENTERINGS: tuple[str, ...] = ("P", "F", "I", "A", "B", "C", "R")
249
+
250
+
251
+ def _infer_system(a: float, b: float, c: float, alpha: float, beta: float, gamma: float) -> str:
252
+ """Crystal-system label from the cell (mirrors MATLAB ``dSpacing/inferSystem``).
253
+
254
+ Uses exact equality on the supplied lengths/angles, as MATLAB does — the
255
+ defaults ``b=c=a`` and ``α=β=γ=90`` reproduce cubic/tetragonal/hexagonal
256
+ exactly; anything else with right angles is orthorhombic, otherwise triclinic.
257
+ """
258
+ right_angles = alpha == 90 and beta == 90 and gamma == 90
259
+ b_is_a = b == a
260
+ c_is_a = c == a
261
+ if right_angles and b_is_a and c_is_a:
262
+ return "cubic"
263
+ if right_angles and b_is_a and not c_is_a:
264
+ return "tetragonal"
265
+ if alpha == 90 and beta == 90 and gamma == 120 and b_is_a:
266
+ return "hexagonal"
267
+ if right_angles:
268
+ return "orthorhombic"
269
+ return "triclinic"
270
+
271
+
272
+ def _centering_allowed(h: int, k: int, l: int, centering: str) -> bool:
273
+ """Systematic-absence rule for a Bravais centering (``P/F/I/A/B/C/R``)."""
274
+ if centering == "F": # all-odd or all-even
275
+ parity = (h % 2, k % 2, l % 2)
276
+ return parity == (0, 0, 0) or parity == (1, 1, 1)
277
+ if centering == "I":
278
+ return (h + k + l) % 2 == 0
279
+ if centering == "A":
280
+ return (k + l) % 2 == 0
281
+ if centering == "B":
282
+ return (h + l) % 2 == 0
283
+ if centering == "C":
284
+ return (h + k) % 2 == 0
285
+ if centering == "R": # obverse setting (IUCr standard)
286
+ return (h - k + l) % 3 == 0
287
+ return True # 'P' and any unknown → primitive (all allowed)
288
+
289
+
290
+ def plane_spacings(
291
+ a: float,
292
+ *,
293
+ b: float | None = None,
294
+ c: float | None = None,
295
+ alpha: float = 90.0,
296
+ beta: float = 90.0,
297
+ gamma: float = 90.0,
298
+ max_hkl: int = 5,
299
+ lambda_: float = 1.5406,
300
+ centering: str = "P",
301
+ min_d: float = 0.0,
302
+ ) -> dict[str, Any]:
303
+ r"""Enumerate allowed ``(hkl)`` reflections with d-spacings and ``2θ``.
304
+
305
+ Ports ``calc.crystal.planeSpacings``: enumerate every ``(h,k,l)`` in
306
+ ``[-max_hkl, max_hkl]³`` (excluding ``000``), drop those forbidden by the
307
+ ``centering`` absence rule, compute ``d`` via the general triclinic reciprocal
308
+ metric tensor, group symmetry-equivalent planes by ``round(d, 8)`` (their count
309
+ is the multiplicity), pick a canonical representative per group, and sort by
310
+ descending ``d`` (ascending ``2θ``). ``2θ = 2·asin(λ/2d)`` in degrees, with
311
+ physically unreachable reflections (``λ/2d > 1``) marked ``NaN``.
312
+
313
+ ``b``/``c`` default to ``a``. Returns a dict with ``hkl`` (list of ``[h,k,l]``),
314
+ ``d``, ``two_theta``, ``multiplicity``, ``centering``, ``system``, ``lambda``,
315
+ ``n_reflections``.
316
+
317
+ >>> r = plane_spacings(5.431, centering="F", max_hkl=3)
318
+ >>> r["hkl"][0], round(r["d"][0], 4) # FCC: first reflection is (111)
319
+ ([1, 1, 1], 3.1356)
320
+ """
321
+ if not (math.isfinite(a) and a > 0):
322
+ raise ValueError("lattice parameter a must be positive and finite")
323
+ if max_hkl < 1:
324
+ raise ValueError("max_hkl must be a positive integer")
325
+ bb = a if b is None else b
326
+ cc = a if c is None else c
327
+ cen = centering.upper()
328
+
329
+ # Enumerate + filter, preserving MATLAB's ih/ik/il order (matters for
330
+ # the canonical-representative tie-break, which is order-stable).
331
+ h_range = range(-max_hkl, max_hkl + 1)
332
+ groups: dict[float, list[tuple[int, int, int]]] = {}
333
+ group_d: dict[float, list[float]] = {}
334
+ for hh in h_range:
335
+ for kk in h_range:
336
+ for ll in h_range:
337
+ if hh == 0 and kk == 0 and ll == 0:
338
+ continue
339
+ if not _centering_allowed(hh, kk, ll, cen):
340
+ continue
341
+ d = _triclinic(a, bb, cc, alpha, beta, gamma, hh, kk, ll)
342
+ if d < min_d:
343
+ continue
344
+ key = round(d, 8)
345
+ groups.setdefault(key, []).append((hh, kk, ll))
346
+ group_d.setdefault(key, []).append(d)
347
+
348
+ # Collapse each group → (canonical hkl, mean d, multiplicity).
349
+ reps: list[tuple[list[int], float, int]] = []
350
+ for key, members in groups.items():
351
+ ds = group_d[key]
352
+ mult = len(members)
353
+ d_mean = sum(ds) / mult
354
+ # Prefer "positive-first" indices (h>0, or h==0&k>0, or h==0&k==0&l>0).
355
+ pos = [
356
+ m
357
+ for m in members
358
+ if m[0] > 0 or (m[0] == 0 and m[1] > 0) or (m[0] == 0 and m[1] == 0 and m[2] > 0)
359
+ ]
360
+ cand = pos if pos else members
361
+ # sortrows([nNegs, -sum, |h|, |k|, |l|]) ascending → first row.
362
+ chosen = min(
363
+ cand,
364
+ key=lambda m: (
365
+ sum(1 for x in m if x < 0),
366
+ -(m[0] + m[1] + m[2]),
367
+ abs(m[0]),
368
+ abs(m[1]),
369
+ abs(m[2]),
370
+ ),
371
+ )
372
+ reps.append(([chosen[0], chosen[1], chosen[2]], d_mean, mult))
373
+
374
+ # Sort by descending d (ascending 2θ). Python sort is stable, matching MATLAB.
375
+ reps.sort(key=lambda r: -r[1])
376
+
377
+ hkl_out = [r[0] for r in reps]
378
+ d_out = [r[1] for r in reps]
379
+ mult_out = [r[2] for r in reps]
380
+
381
+ if math.isnan(lambda_):
382
+ two_theta = [math.nan] * len(d_out)
383
+ else:
384
+ two_theta = []
385
+ for d in d_out:
386
+ sin_theta = lambda_ / (2.0 * d)
387
+ two_theta.append(
388
+ math.nan if sin_theta > 1.0 else 2.0 * math.degrees(math.asin(min(sin_theta, 1.0)))
389
+ )
390
+
391
+ return {
392
+ "hkl": hkl_out,
393
+ "d": d_out,
394
+ "two_theta": two_theta,
395
+ "multiplicity": mult_out,
396
+ "centering": cen,
397
+ "system": _infer_system(a, bb, cc, alpha, beta, gamma),
398
+ "lambda": lambda_,
399
+ "n_reflections": len(d_out),
400
+ }
@@ -0,0 +1,120 @@
1
+ r"""Diffusion calculators (DiraCulator ``buildDiffusionTab``).
2
+
3
+ Pure calc layer — closed-form scalars in, result dicts out. No fastapi /
4
+ pydantic imports. Ports the three inline MATLAB diffusion formulas verbatim:
5
+
6
+ .. math::
7
+
8
+ D = D_0\,e^{-E_a/(k_B T)} \qquad L = \sqrt{D\,t} \qquad
9
+ J = -D\,\frac{\partial C}{\partial x} \approx -D\,\frac{\Delta C}{\Delta x}
10
+
11
+ Units follow the MATLAB toolbox (the behavioural reference): the
12
+ pre-exponential factor ``D0`` and the diffusion coefficient ``D`` in cm²/s,
13
+ the activation energy ``Ea`` in eV, temperature ``T`` in K, time ``t`` in s,
14
+ concentration gradient ``ΔC`` in cm⁻³, distance ``Δx`` in cm, and the Fick
15
+ flux ``J`` in atoms/(cm²·s).
16
+
17
+ The Arrhenius Boltzmann constant in eV/K is derived from the CODATA
18
+ constants (``kB / e``), which equals the MATLAB hardcoded value
19
+ ``8.617333262e-5`` eV/K exactly.
20
+
21
+ Reference values (closed-form physics, not MATLAB-idiosyncratic):
22
+ - ``arrhenius(0.1, 1.0, 1000) -> D ≈ 9.12e-7`` cm²/s
23
+ - ``diffusion_length(1e-12, 3600) -> L = 6e-5`` cm = 0.6 µm
24
+ - ``fick_flux(1e-12, 1e18, 1e-5) -> J = -1e11`` atoms/(cm²·s)
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import math
30
+
31
+ from quantized.calc.constants import constants
32
+
33
+ __all__ = [
34
+ "arrhenius",
35
+ "diffusion_length",
36
+ "fick_flux",
37
+ "kb_ev",
38
+ ]
39
+
40
+
41
+ def kb_ev() -> float:
42
+ """Boltzmann constant in eV/K (``kB / e``).
43
+
44
+ Equals the MATLAB hardcoded ``8.617333262e-5`` eV/K to full precision.
45
+
46
+ >>> round(kb_ev() * 1e5, 6)
47
+ 8.617333
48
+ """
49
+ c = constants()
50
+ return c["kB"] / c["e"]
51
+
52
+
53
+ def arrhenius(d0: float, ea: float, t: float) -> dict[str, float]:
54
+ """Arrhenius diffusion coefficient ``D = D0·exp(-Ea/(kB·T))`` (cm²/s).
55
+
56
+ Args:
57
+ d0: pre-exponential factor D₀ (cm²/s), ≥ 0.
58
+ ea: activation energy E_a (eV), ≥ 0.
59
+ t: temperature T (K), > 0.
60
+
61
+ >>> round(arrhenius(0.1, 1.0, 1000.0)["D"], 13)
62
+ 9.124768e-07
63
+ """
64
+ if d0 < 0:
65
+ raise ValueError("D0 must be non-negative")
66
+ if ea < 0:
67
+ raise ValueError("Ea must be non-negative")
68
+ if t <= 0:
69
+ raise ValueError("T must be positive")
70
+ d = d0 * math.exp(-ea / (kb_ev() * t))
71
+ return {"D": d, "D0": d0, "Ea": ea, "T": t}
72
+
73
+
74
+ def diffusion_length(d: float, t: float) -> dict[str, float]:
75
+ """Characteristic diffusion length ``L = sqrt(D·t)`` (cm).
76
+
77
+ Also returns the length in µm (×1e4) and nm (×1e7), as the MATLAB card
78
+ displays. ``L`` is the RMS displacement scale in 1-D.
79
+
80
+ Args:
81
+ d: diffusion coefficient D (cm²/s), ≥ 0.
82
+ t: diffusion time t (s), ≥ 0.
83
+
84
+ >>> r = diffusion_length(1e-12, 3600.0)
85
+ >>> round(r["L"], 8)
86
+ 6e-05
87
+ >>> round(r["L_um"], 6)
88
+ 0.6
89
+ """
90
+ if d < 0:
91
+ raise ValueError("D must be non-negative")
92
+ if t < 0:
93
+ raise ValueError("t must be non-negative")
94
+ length = math.sqrt(d * t) # cm
95
+ return {
96
+ "L": length,
97
+ "L_um": length * 1e4,
98
+ "L_nm": length * 1e7,
99
+ "D": d,
100
+ "t": t,
101
+ }
102
+
103
+
104
+ def fick_flux(d: float, dc: float, dx: float) -> dict[str, float]:
105
+ """Fick's first law steady-state flux ``J = -D·ΔC/Δx`` (atoms/(cm²·s)).
106
+
107
+ Args:
108
+ d: diffusion coefficient D (cm²/s), ≥ 0.
109
+ dc: concentration difference ΔC (cm⁻³).
110
+ dx: distance over which ΔC occurs (cm), > 0.
111
+
112
+ >>> round(fick_flux(1e-12, 1e18, 1e-5)["J"] / 1e11, 6)
113
+ -1.0
114
+ """
115
+ if d < 0:
116
+ raise ValueError("D must be non-negative")
117
+ if dx <= 0:
118
+ raise ValueError("Δx must be > 0")
119
+ j = -d * dc / dx # atoms/(cm²·s)
120
+ return {"J": j, "J_abs": abs(j), "D": d, "dC": dc, "dx": dx}