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