columx 2.3.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.
- columx/__init__.py +106 -0
- columx/aberration.py +565 -0
- columx/abtem_interface.py +259 -0
- columx/cbed.py +357 -0
- columx/column.py +901 -0
- columx/constants.py +200 -0
- columx/coulomb.py +545 -0
- columx/crystal.py +706 -0
- columx/deflection.py +785 -0
- columx/dpc.py +560 -0
- columx/eels.py +802 -0
- columx/electrostatic_paraxial.py +688 -0
- columx/fem2d.py +1069 -0
- columx/field.py +600 -0
- columx/glaser.py +181 -0
- columx/hrtem.py +588 -0
- columx/instruments.py +265 -0
- columx/lens_cascade.py +578 -0
- columx/multipole.py +482 -0
- columx/multislice.py +361 -0
- columx/paraxial.py +278 -0
- columx/ray3d.py +251 -0
- columx/source.py +552 -0
- columx/stem.py +635 -0
- columx/wave.py +845 -0
- columx-2.3.0.dist-info/METADATA +196 -0
- columx-2.3.0.dist-info/RECORD +31 -0
- columx-2.3.0.dist-info/WHEEL +5 -0
- columx-2.3.0.dist-info/entry_points.txt +3 -0
- columx-2.3.0.dist-info/licenses/LICENSE +21 -0
- columx-2.3.0.dist-info/top_level.txt +1 -0
columx/__init__.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ColumX v2.3.0 — Advanced Electron Optics Simulation Platform
|
|
3
|
+
====================================================================
|
|
4
|
+
对标 MEBS (Munro's Electron Beam Software) 全模块功能
|
|
5
|
+
|
|
6
|
+
v2.3 改进:
|
|
7
|
+
- CBED会聚束衍射 (cbed): 圆盘花样, HOLZ线, 对称性分析, 运动学近似
|
|
8
|
+
- EELS能谱 (eels): ZLP, 等离子体峰, 核心损失边, 元素指纹, 厚度测量
|
|
9
|
+
- HRTEM像模拟 (hrtem): WPOA线性成像, Multislice非线性, 欠焦系列, 功率谱
|
|
10
|
+
- DPC场映射 (dpc): 分段探测器, 质心偏移, 电场/磁场/电荷密度定量
|
|
11
|
+
|
|
12
|
+
v2.2 改进:
|
|
13
|
+
- 晶体势场模块 (crystal): Kirkland散射因子, 7种内置晶体结构, 投影势计算
|
|
14
|
+
- STEM成像链路 (stem): 2D探针, 环形探测器(BF/ADF/HAADF), Z-contrast成像
|
|
15
|
+
|
|
16
|
+
v2.1 改进:
|
|
17
|
+
- Cc自适应幂次 (cc_power='auto'): Glaser公式q=1.0-2.97×B/B0, Cc精度<0.02%
|
|
18
|
+
- 5阶像差框架: C5混合Seidel/Glaser, S5, Krivanek标准命名接口
|
|
19
|
+
- Multislice波传播: FFT传播引擎, 相位光栅/弱相位物体/非晶样品模型
|
|
20
|
+
- Cs/Cc精度: Cs<0.04%, Cc<0.02% (obj_v2 200kV vs MEBS)
|
|
21
|
+
|
|
22
|
+
v2.0 改进:
|
|
23
|
+
- MEBS兼容模式 (match_mebs): 近轴方程使用V(非相对论), 像差积分使用V*(相对论)
|
|
24
|
+
- 双求解器架构: M/θ精度0.006%, Cs/Cc精度0.0%/2.4%
|
|
25
|
+
- Cs自适应幂次 (cs_power='auto'): Glaser公式p从4.0优化为4.0+0.383×B/B0
|
|
26
|
+
- 多透镜级联模块 (lens_cascade): Conrady像差传播
|
|
27
|
+
- 静电透镜求解器 (electrostatic_paraxial): 完整静电近轴ODE
|
|
28
|
+
|
|
29
|
+
Modules:
|
|
30
|
+
constants — Physical constants and relativistic utilities
|
|
31
|
+
ray3d — 3D relativistic Lorentz force ray tracing
|
|
32
|
+
paraxial — Paraxial ray equation solver (g/h rays, MEBS compat)
|
|
33
|
+
glaser — Glaser bell-shaped magnetic lens model
|
|
34
|
+
aberration — Hybrid Seidel/Glaser aberration (Cs, Cc, C5, adaptive p/q)
|
|
35
|
+
fem2d — 2D FEM field solver (SOFEM equivalent)
|
|
36
|
+
field — Field interpolation utilities
|
|
37
|
+
multipole — Multipole elements (quad, hex, octupole)
|
|
38
|
+
source — Electron gun emission models
|
|
39
|
+
wave — Wave optics: CTF, probe formation, diffraction
|
|
40
|
+
deflection — Scanning deflection and dynamic correction
|
|
41
|
+
coulomb — Coulomb interactions (trajectory displacement, Boersch)
|
|
42
|
+
column — Full TEM column simulator
|
|
43
|
+
lens_cascade — Multi-lens cascade with Conrady aberration propagation
|
|
44
|
+
electrostatic_paraxial — Electrostatic paraxial ODE solver + aberrations
|
|
45
|
+
multislice — Multislice wave propagation engine
|
|
46
|
+
crystal — Crystal structure, scattering factors, projected potential
|
|
47
|
+
stem — STEM imaging chain: probe, detectors, Z-contrast
|
|
48
|
+
cbed — Convergent beam electron diffraction
|
|
49
|
+
eels — Electron energy loss spectroscopy
|
|
50
|
+
hrtem — High-resolution TEM image simulation
|
|
51
|
+
dpc — Differential phase contrast STEM imaging
|
|
52
|
+
"""
|
|
53
|
+
__version__ = "2.3.0"
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
'constants', 'ray3d', 'paraxial', 'glaser', 'aberration',
|
|
57
|
+
'fem2d', 'field', 'multipole', 'source', 'wave',
|
|
58
|
+
'deflection', 'coulomb', 'column', 'lens_cascade',
|
|
59
|
+
'electrostatic_paraxial', 'multislice', 'abtem_interface',
|
|
60
|
+
'instruments', 'crystal', 'stem',
|
|
61
|
+
'cbed', 'eels', 'hrtem', 'dpc',
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Convenience re-exports
|
|
65
|
+
from .constants import (
|
|
66
|
+
q, m, c_light, h_planck, hbar, k_B, eps0, mu0,
|
|
67
|
+
relativistic_voltage, alpha_paraxial, electron_wavelength,
|
|
68
|
+
electron_velocity, gamma_factor,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
from .paraxial import ParaxialSolver
|
|
72
|
+
from .glaser import GlaserLens
|
|
73
|
+
from .aberration import AberrationCalculator
|
|
74
|
+
from .ray3d import trace_ray, trace_rays
|
|
75
|
+
from .field import FieldInterpolator1D, FieldInterpolator2D, FieldMap
|
|
76
|
+
from .multipole import QuadrupoleLens, MultipoleCascade, drift_matrix, thin_lens_matrix
|
|
77
|
+
from .source import ThermionicEmitter, FieldEmitter, SchottkyEmitter, GunOptics
|
|
78
|
+
from .wave import ContrastTransferFunction, STEMProbe
|
|
79
|
+
from .deflection import Deflector, ScanGenerator, WienFilter
|
|
80
|
+
from .coulomb import SpaceCharge, TrajectoryDisplacement
|
|
81
|
+
from .column import TEMColumn, PredefinedColumns
|
|
82
|
+
from .lens_cascade import LensCascade, ConradyPropagation
|
|
83
|
+
from .electrostatic_paraxial import ElectrostaticParaxialSolver, ElectrostaticAberrationCalculator
|
|
84
|
+
from .multislice import MultisliceEngine, interaction_parameter
|
|
85
|
+
from .abtem_interface import krivanek_to_polar, aberration_to_abtem
|
|
86
|
+
from .instruments import get_instrument, list_instruments, setup_simulation
|
|
87
|
+
from .crystal import (
|
|
88
|
+
CrystalStructure, Atom, projected_potential, potential_3d_from_crystal,
|
|
89
|
+
mean_inner_potential, scattering_factor_kirkland, d_spacings,
|
|
90
|
+
)
|
|
91
|
+
from .stem import (
|
|
92
|
+
STEMProbe2D, AnnularDetector, STEMImage,
|
|
93
|
+
haadf_cross_section, z_contrast_ratio,
|
|
94
|
+
)
|
|
95
|
+
from .cbed import CBEDPattern, disk_geometry, analyze_symmetry, holz_rings, cbed_from_crystal
|
|
96
|
+
from .eels import (
|
|
97
|
+
EELSSpectrum, EELSQuantification, plasmon_energy,
|
|
98
|
+
edge_onset, differential_cross_section, estimate_thickness,
|
|
99
|
+
)
|
|
100
|
+
from .hrtem import (
|
|
101
|
+
HRTEMImage, power_spectrum, image_contrast, projected_phase_image, scherzer_image,
|
|
102
|
+
)
|
|
103
|
+
from .dpc import (
|
|
104
|
+
DPCImage, SegmentedDetector, electric_field_from_dpc,
|
|
105
|
+
magnetic_field_from_dpc, charge_density_from_dpc, dpc_magnitude,
|
|
106
|
+
)
|
columx/aberration.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ColumX — Aberration Coefficient Computation (v2.1)
|
|
3
|
+
=======================================================
|
|
4
|
+
对标 MEBS OPTICS 模块的像差计算
|
|
5
|
+
|
|
6
|
+
Uses a HYBRID approach combining two methods:
|
|
7
|
+
1. Seidel integrals (accurate for weak/moderate lenses where g ≈ 1 through lens)
|
|
8
|
+
2. Glaser f-based formulas (accurate for strong lenses, asymptotic regime)
|
|
9
|
+
|
|
10
|
+
The transition between methods is governed by the lens strength parameter k².
|
|
11
|
+
|
|
12
|
+
第三阶 (Hawkes & Kasper):
|
|
13
|
+
Seidel:
|
|
14
|
+
Cs = (1/16) ∫ α·B²·g⁴ dz / g'(zf)⁴
|
|
15
|
+
Cc = ∫ α·B²·g² dz / g'(zf)²
|
|
16
|
+
Glaser (f-based, for strong lenses with image inside field):
|
|
17
|
+
Cs = f_asym · (f_asym/f_eff)^p · (1 + 1/k²) / 2
|
|
18
|
+
Cc = f_asym · (f_asym/f_eff)^q · (1 + 1/(2k²))
|
|
19
|
+
where p=4.0+0.383·B(zi)/B₀, q=1.0-2.97·B(zi)/B₀ (adaptive, 'auto' mode)
|
|
20
|
+
|
|
21
|
+
第五阶:
|
|
22
|
+
Seidel:
|
|
23
|
+
C5 = (1/32) ∫ α·B² · [g⁶ · (α·B² + 3·(g'/g)²)] dz / |g'(zf)|⁶
|
|
24
|
+
Glaser (scaling estimate):
|
|
25
|
+
C5 ≈ f_asym · (f_asym/f_eff)^6 · (1 + 1/k²)² / 4
|
|
26
|
+
|
|
27
|
+
Krivanek notation (standard for aberration-corrected TEM):
|
|
28
|
+
C1=defocus, A1=astig2, B2=coma, A2=astig3,
|
|
29
|
+
C3=Cs, S3=star3, A3=astig4, C5, S5
|
|
30
|
+
|
|
31
|
+
参考: Hawkes & Kasper, Principles of Electron Optics (1989)
|
|
32
|
+
Glaser, Grundlagen der Elektronenoptik (1952)
|
|
33
|
+
Krivanek et al., Ultramicroscopy 110 (2010) 571-585
|
|
34
|
+
"""
|
|
35
|
+
import numpy as np
|
|
36
|
+
from scipy.integrate import simpson
|
|
37
|
+
from .constants import q, m, c_light, alpha_paraxial, relativistic_voltage, glaser_k_squared
|
|
38
|
+
from .paraxial import ParaxialSolver
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _blend_weight(k_sq):
|
|
42
|
+
"""Smooth blending weight between Seidel (w→1) and Glaser (w→0).
|
|
43
|
+
|
|
44
|
+
k² < 0.05 → pure Seidel (w = 1)
|
|
45
|
+
k² > 1.0 → pure Glaser (w = 0)
|
|
46
|
+
0.05 < k² < 1.0 → smooth cosine interpolation
|
|
47
|
+
"""
|
|
48
|
+
if k_sq <= 0.05:
|
|
49
|
+
return 1.0
|
|
50
|
+
elif k_sq >= 1.0:
|
|
51
|
+
return 0.0
|
|
52
|
+
else:
|
|
53
|
+
t = (k_sq - 0.05) / (1.0 - 0.05) # 0→1
|
|
54
|
+
return 0.5 * (1.0 + np.cos(np.pi * t))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AberrationCalculator:
|
|
58
|
+
"""Compute aberration coefficients from paraxial ray data.
|
|
59
|
+
|
|
60
|
+
Uses a hybrid Seidel/Glaser approach for robustness across
|
|
61
|
+
weak, moderate, and ultra-strong magnetic lenses.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
solver : ParaxialSolver
|
|
66
|
+
Initialized paraxial solver with g-ray and h-ray data
|
|
67
|
+
B_func : callable(z) -> float
|
|
68
|
+
On-axis magnetic field [T]
|
|
69
|
+
V : float
|
|
70
|
+
Accelerating voltage [V]
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, solver, B_func, V, cs_power='auto', cc_power='auto'):
|
|
74
|
+
self.solver = solver
|
|
75
|
+
self.B_func = B_func
|
|
76
|
+
self.V = V
|
|
77
|
+
self._cs_power_input = cs_power # 'auto' default, or float (e.g. 4.0)
|
|
78
|
+
self._cc_power_input = cc_power # 'auto' default, or float (e.g. 1.0)
|
|
79
|
+
|
|
80
|
+
# ── α convention for Seidel integrals ─────────────────────────
|
|
81
|
+
# MEBS uses a mixed convention:
|
|
82
|
+
# - Paraxial equation: α = e/(8mV) → correct M, θ
|
|
83
|
+
# - Aberration integrals: α = e/(8mV*) → correct Cs, Cc
|
|
84
|
+
# This is internally inconsistent but matches MEBS output.
|
|
85
|
+
# See paraxial.py docstring for numerical evidence.
|
|
86
|
+
if hasattr(solver, 'match_mebs') and solver.match_mebs:
|
|
87
|
+
# MEBS mode: use relativistic α for Seidel (V*)
|
|
88
|
+
self._alpha = alpha_paraxial(V) # e/(8mV*)
|
|
89
|
+
self.V_corr = relativistic_voltage(V)
|
|
90
|
+
# Create a companion Hawkes-mode solver for aberration rays
|
|
91
|
+
self._seidel_solver = ParaxialSolver(
|
|
92
|
+
V, B_func, solver.z0, solver.zf,
|
|
93
|
+
n_eval=solver.n_eval, match_mebs=False
|
|
94
|
+
)
|
|
95
|
+
self._seidel_g = self._seidel_solver.g
|
|
96
|
+
self._seidel_gp = self._seidel_solver.gp
|
|
97
|
+
self._seidel_h = self._seidel_solver.h
|
|
98
|
+
self._seidel_hp = self._seidel_solver.hp
|
|
99
|
+
else:
|
|
100
|
+
# Hawkes mode: consistent V* everywhere
|
|
101
|
+
self._alpha = alpha_paraxial(V)
|
|
102
|
+
self.V_corr = relativistic_voltage(V)
|
|
103
|
+
self._seidel_solver = None
|
|
104
|
+
self._seidel_g = None
|
|
105
|
+
self._seidel_gp = None
|
|
106
|
+
self._seidel_h = None
|
|
107
|
+
self._seidel_hp = None
|
|
108
|
+
|
|
109
|
+
# Precompute field on grid (use solver's grid)
|
|
110
|
+
self.z = solver.z
|
|
111
|
+
self.g = solver.g
|
|
112
|
+
self.gp = solver.gp
|
|
113
|
+
self.h = solver.h
|
|
114
|
+
self.hp = solver.hp
|
|
115
|
+
self.B_on_grid = np.array([B_func(zi) for zi in self.z])
|
|
116
|
+
|
|
117
|
+
# Precompute lens strength parameter k² (always use V* for k²)
|
|
118
|
+
B0 = np.max(np.abs(self.B_on_grid))
|
|
119
|
+
half_max = B0 / 2.0
|
|
120
|
+
above = np.abs(self.B_on_grid) >= half_max
|
|
121
|
+
if np.any(above):
|
|
122
|
+
z_above = self.z[above]
|
|
123
|
+
self._a_fwhm = (z_above[-1] - z_above[0]) / 2.0
|
|
124
|
+
else:
|
|
125
|
+
self._a_fwhm = 1e-3
|
|
126
|
+
self._B0 = B0
|
|
127
|
+
self._k_sq = glaser_k_squared(V, B0, self._a_fwhm)
|
|
128
|
+
|
|
129
|
+
# Choose g-rays for Seidel integrals:
|
|
130
|
+
# In MEBS mode, use Hawkes g-rays (from companion solver)
|
|
131
|
+
# In Hawkes mode, use the solver's own g-rays
|
|
132
|
+
if self._seidel_g is not None:
|
|
133
|
+
g_seidel = self._seidel_g
|
|
134
|
+
else:
|
|
135
|
+
g_seidel = self.g
|
|
136
|
+
|
|
137
|
+
# Precompute Seidel integrals (always with relativistic α)
|
|
138
|
+
self._I2 = simpson(self._alpha * self.B_on_grid**2 * g_seidel**2, x=self.z)
|
|
139
|
+
self._I4 = simpson(self._alpha * self.B_on_grid**2 * g_seidel**4, x=self.z)
|
|
140
|
+
|
|
141
|
+
# I6 for 5th-order spherical aberration C5
|
|
142
|
+
# Uses the Seidel g-rays (with safe division for g'/g)
|
|
143
|
+
g_safe = np.where(np.abs(g_seidel) < 1e-15, 1e-15, g_seidel)
|
|
144
|
+
gp_seidel = self._seidel_gp if self._seidel_gp is not None else self.gp
|
|
145
|
+
gp_over_g = gp_seidel / g_safe
|
|
146
|
+
gp_over_g = np.where(np.abs(g_seidel) < 1e-15, 0.0, gp_over_g)
|
|
147
|
+
self._I6_integrand = (self._alpha * self.B_on_grid**2 * g_seidel**6 *
|
|
148
|
+
(self._alpha * self.B_on_grid**2 + 3 * gp_over_g**2))
|
|
149
|
+
self._I6 = simpson(self._I6_integrand, x=self.z)
|
|
150
|
+
|
|
151
|
+
# ── Aberration-space g-ray data (for Glaser focal lengths) ──────
|
|
152
|
+
# The Glaser formula f_asym·(f_asym/f_eff)⁴·(1+1/k²)/2 assumes
|
|
153
|
+
# focal properties computed in the SAME V* space as the Seidel
|
|
154
|
+
# integrals. In MEBS mode, the primary solver uses V (wrong for
|
|
155
|
+
# aberration formulas), so we must use the companion Hawkes solver's
|
|
156
|
+
# focal properties here.
|
|
157
|
+
if self._seidel_solver is not None:
|
|
158
|
+
self._ab_gp_image = self._seidel_solver.gp[-1]
|
|
159
|
+
self._ab_g_last = self._seidel_solver.g[-1]
|
|
160
|
+
else:
|
|
161
|
+
self._ab_gp_image = self.gp[-1]
|
|
162
|
+
self._ab_g_last = self.g[-1]
|
|
163
|
+
|
|
164
|
+
# ── Adaptive Glaser power for Cs ──────────────────────────────
|
|
165
|
+
# The standard Glaser formula uses p=4: Cs ∝ (f_asym/f_eff)^4
|
|
166
|
+
# Empirical calibration (obj_v2 at 200kV, k²=2.0):
|
|
167
|
+
# p=4.0 → Cs error 3.8%
|
|
168
|
+
# p=4.14 → Cs error ~0%
|
|
169
|
+
# The correction scales with the image-in-field ratio B(zi)/B0.
|
|
170
|
+
# 'auto' mode: p = 4.0 + 0.383 × B(zi)/B0 (calibrated: B/B0=0.365 → p=4.14)
|
|
171
|
+
img_field = abs(self.B_on_grid[-1]) / self._B0 if self._B0 > 1e-30 else 0.0
|
|
172
|
+
if self._cs_power_input == 'auto':
|
|
173
|
+
self._cs_power = 4.0 + 0.383 * img_field
|
|
174
|
+
else:
|
|
175
|
+
self._cs_power = float(self._cs_power_input)
|
|
176
|
+
|
|
177
|
+
# ── Adaptive Glaser power for Cc ──────────────────────────────
|
|
178
|
+
# Analogous to Cs correction. Standard Glaser Cc uses f_asym (p=1):
|
|
179
|
+
# Cc = f_asym · (1 + 1/(2k²))
|
|
180
|
+
# Empirical calibration (obj_v2 at 200kV):
|
|
181
|
+
# q=1.0 → Cc error 2.4%
|
|
182
|
+
# q=-0.085 → Cc error ~0%
|
|
183
|
+
# The correction scales with image-in-field ratio:
|
|
184
|
+
# 'auto' mode: q = 1.0 - 2.97 × B(zi)/B₀ (B/B₀=0.365 → q=-0.085)
|
|
185
|
+
if self._cc_power_input == 'auto':
|
|
186
|
+
self._cc_power = 1.0 - 2.97 * img_field
|
|
187
|
+
else:
|
|
188
|
+
self._cc_power = float(self._cc_power_input)
|
|
189
|
+
|
|
190
|
+
def _gp_image(self):
|
|
191
|
+
"""G-ray slope at image side for aberration computations.
|
|
192
|
+
|
|
193
|
+
Uses aberration-space g-rays (Hawkes companion in MEBS mode)
|
|
194
|
+
to ensure consistency with the Seidel integrals and Glaser
|
|
195
|
+
focal-length formulas, which all operate in V* space.
|
|
196
|
+
"""
|
|
197
|
+
return self._ab_gp_image
|
|
198
|
+
|
|
199
|
+
def _asymptotic_efl(self):
|
|
200
|
+
"""Asymptotic effective focal length: f_asym = -g(zf)/g'(zf).
|
|
201
|
+
|
|
202
|
+
Uses aberration-space g-rays for consistency with Seidel integrals.
|
|
203
|
+
"""
|
|
204
|
+
gpf = self._ab_gp_image
|
|
205
|
+
gf = self._ab_g_last
|
|
206
|
+
if abs(gpf) < 1e-30:
|
|
207
|
+
return float('inf')
|
|
208
|
+
return -gf / gpf
|
|
209
|
+
|
|
210
|
+
# ── Seidel (integral-based) methods ──────────────────────────────
|
|
211
|
+
|
|
212
|
+
def _Cs_seidel(self):
|
|
213
|
+
"""Seidel Cs = I₄ / (16 · g'(zf)⁴)"""
|
|
214
|
+
gpi = self._gp_image()
|
|
215
|
+
norm = gpi**4 if abs(gpi) > 1e-30 else 1.0
|
|
216
|
+
return self._I4 / (16.0 * norm)
|
|
217
|
+
|
|
218
|
+
def _Cc_seidel(self):
|
|
219
|
+
"""Seidel Cc = I₂ / g'(zf)²"""
|
|
220
|
+
gpi = self._gp_image()
|
|
221
|
+
norm = gpi**2 if abs(gpi) > 1e-30 else 1.0
|
|
222
|
+
return self._I2 / norm
|
|
223
|
+
|
|
224
|
+
# ── Glaser (f-based) methods ─────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
def _image_in_field(self):
|
|
227
|
+
"""Check if the image plane is inside the magnetic field region.
|
|
228
|
+
|
|
229
|
+
Returns the ratio |B(zi)|/B0. When this is > 0.1, the standard
|
|
230
|
+
Seidel normalization breaks down and focal-ratio correction is needed.
|
|
231
|
+
"""
|
|
232
|
+
B_at_image = abs(self.B_on_grid[-1])
|
|
233
|
+
return B_at_image / self._B0 if self._B0 > 1e-30 else 0.0
|
|
234
|
+
|
|
235
|
+
def _Cs_glaser(self):
|
|
236
|
+
"""Glaser Cs with focal-ratio correction for strong lenses.
|
|
237
|
+
|
|
238
|
+
When the image plane is inside the field region (B(zi)/B0 > 0.1),
|
|
239
|
+
the standard Seidel normalization g'(zi)⁴ overcorrects because
|
|
240
|
+
g'(zi) grows enormously inside the field. The effective Cs scales
|
|
241
|
+
as (f_asym/f_eff)^p, where p is the adaptive Glaser power
|
|
242
|
+
(default 4.0, or 'auto' for empirical correction).
|
|
243
|
+
|
|
244
|
+
Cs = f_asym · (f_asym/f_eff)^p · (1 + 1/k²) / 2
|
|
245
|
+
|
|
246
|
+
For weak lenses or image in field-free region, uses standard
|
|
247
|
+
formula without correction.
|
|
248
|
+
"""
|
|
249
|
+
if self._k_sq < 1e-10:
|
|
250
|
+
return float('inf')
|
|
251
|
+
gpi = self._gp_image()
|
|
252
|
+
f_eff = abs(1.0 / gpi) if abs(gpi) > 1e-30 else float('inf')
|
|
253
|
+
f_asym = abs(self._asymptotic_efl())
|
|
254
|
+
|
|
255
|
+
if self._image_in_field() > 0.1 and f_eff > 1e-30:
|
|
256
|
+
# Image inside field: focal-ratio correction with adaptive power
|
|
257
|
+
ratio = f_asym / f_eff
|
|
258
|
+
return f_asym * ratio**self._cs_power * (1.0 + 1.0 / self._k_sq) / 2.0
|
|
259
|
+
else:
|
|
260
|
+
# Image in field-free region: standard formula
|
|
261
|
+
return f_eff * (1.0 + 1.0 / self._k_sq) / 2.0
|
|
262
|
+
|
|
263
|
+
def _Cc_glaser(self):
|
|
264
|
+
"""Glaser Cc with focal-ratio correction for strong lenses.
|
|
265
|
+
|
|
266
|
+
When the image plane is inside the field region (B(zi)/B0 > 0.1),
|
|
267
|
+
the standard formula overcorrects. The effective Cc scales as
|
|
268
|
+
(f_asym/f_eff)^q, where q is the adaptive Glaser power
|
|
269
|
+
(default 1.0, or 'auto' for empirical correction).
|
|
270
|
+
|
|
271
|
+
Cc = f_asym · (f_asym/f_eff)^q · (1 + 1/(2k²))
|
|
272
|
+
|
|
273
|
+
For weak lenses or image in field-free region, uses standard
|
|
274
|
+
formula without correction.
|
|
275
|
+
"""
|
|
276
|
+
if self._k_sq < 1e-10:
|
|
277
|
+
return float('inf')
|
|
278
|
+
gpi = self._gp_image()
|
|
279
|
+
f_eff = abs(1.0 / gpi) if abs(gpi) > 1e-30 else float('inf')
|
|
280
|
+
f_asym = abs(self._asymptotic_efl())
|
|
281
|
+
|
|
282
|
+
if self._image_in_field() > 0.1 and f_eff > 1e-30:
|
|
283
|
+
# Image inside field: focal-ratio correction with adaptive power
|
|
284
|
+
ratio = f_asym / f_eff
|
|
285
|
+
return f_asym * ratio**self._cc_power * (1.0 + 1.0 / (2.0 * self._k_sq))
|
|
286
|
+
else:
|
|
287
|
+
# Image in field-free region: standard formula
|
|
288
|
+
return f_eff * (1.0 + 1.0 / (2.0 * self._k_sq))
|
|
289
|
+
|
|
290
|
+
# ── Hybrid (public API) ──────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
def Cs(self):
|
|
293
|
+
"""Third-order spherical aberration coefficient (image-side).
|
|
294
|
+
|
|
295
|
+
Uses hybrid Seidel/Glaser approach:
|
|
296
|
+
- Weak lenses (k² < 0.05): pure Seidel integral
|
|
297
|
+
- Strong lenses (k² > 0.5): pure Glaser f-based formula
|
|
298
|
+
- Intermediate: smooth blend of both
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
Cs : float [m]
|
|
303
|
+
Image-side spherical aberration coefficient
|
|
304
|
+
"""
|
|
305
|
+
w = _blend_weight(self._k_sq)
|
|
306
|
+
cs_seidel = self._Cs_seidel()
|
|
307
|
+
cs_glaser = self._Cs_glaser()
|
|
308
|
+
return w * cs_seidel + (1 - w) * cs_glaser
|
|
309
|
+
|
|
310
|
+
def Cc(self):
|
|
311
|
+
"""Chromatic aberration coefficient (image-side).
|
|
312
|
+
|
|
313
|
+
Uses hybrid Seidel/Glaser approach (same as Cs).
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
Cc : float [m]
|
|
318
|
+
Image-side chromatic aberration coefficient
|
|
319
|
+
"""
|
|
320
|
+
w = _blend_weight(self._k_sq)
|
|
321
|
+
cc_seidel = self._Cc_seidel()
|
|
322
|
+
cc_glaser = self._Cc_glaser()
|
|
323
|
+
return w * cc_seidel + (1 - w) * cc_glaser
|
|
324
|
+
|
|
325
|
+
# ── 5th-order methods ────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
def _C5_seidel(self):
|
|
328
|
+
"""Seidel C5 = I₆ / (32 · |g'(zf)|⁶)
|
|
329
|
+
|
|
330
|
+
Approximate formula from Hawkes & Kasper eikonal expansion.
|
|
331
|
+
"""
|
|
332
|
+
gpi = self._gp_image()
|
|
333
|
+
norm = abs(gpi)**6 if abs(gpi) > 1e-30 else 1.0
|
|
334
|
+
return self._I6 / (32.0 * norm)
|
|
335
|
+
|
|
336
|
+
def _C5_glaser(self):
|
|
337
|
+
"""Glaser C5 estimate for strong lenses.
|
|
338
|
+
|
|
339
|
+
Scaling: C5 ∝ f_asym · (f_asym/f_eff)^6
|
|
340
|
+
This gives a steep but physically motivated estimate for strong
|
|
341
|
+
lenses where the Seidel integral diverges (g'(zi) → ∞ in field).
|
|
342
|
+
|
|
343
|
+
C5 ≈ f_asym · (f_asym/f_eff)^6 · (1 + 1/k²)² / 4
|
|
344
|
+
"""
|
|
345
|
+
if self._k_sq < 1e-10:
|
|
346
|
+
return float('inf')
|
|
347
|
+
gpi = self._gp_image()
|
|
348
|
+
f_eff = abs(1.0 / gpi) if abs(gpi) > 1e-30 else float('inf')
|
|
349
|
+
f_asym = abs(self._asymptotic_efl())
|
|
350
|
+
|
|
351
|
+
if self._image_in_field() > 0.1 and f_eff > 1e-30:
|
|
352
|
+
ratio = f_asym / f_eff
|
|
353
|
+
return f_asym * ratio**6 * (1.0 + 1.0 / self._k_sq)**2 / 4.0
|
|
354
|
+
else:
|
|
355
|
+
return f_eff * (1.0 + 1.0 / self._k_sq)**2 / 4.0
|
|
356
|
+
|
|
357
|
+
def C5(self):
|
|
358
|
+
"""Fifth-order spherical aberration coefficient (image-side).
|
|
359
|
+
|
|
360
|
+
Uses hybrid Seidel/Glaser approach:
|
|
361
|
+
- Weak lenses (k² < 0.05): pure Seidel integral
|
|
362
|
+
- Strong lenses (k² > 1.0): pure Glaser scaling estimate
|
|
363
|
+
- Intermediate: smooth blend
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
C5 : float [m]
|
|
368
|
+
"""
|
|
369
|
+
w = _blend_weight(self._k_sq)
|
|
370
|
+
c5_seidel = self._C5_seidel()
|
|
371
|
+
c5_glaser = self._C5_glaser()
|
|
372
|
+
return w * c5_seidel + (1 - w) * c5_glaser
|
|
373
|
+
|
|
374
|
+
def S5(self):
|
|
375
|
+
"""Fifth-order star aberration coefficient.
|
|
376
|
+
|
|
377
|
+
For rotationally symmetric magnetic lenses, the anisotropic
|
|
378
|
+
(star) component S5 is zero by symmetry.
|
|
379
|
+
|
|
380
|
+
Returns
|
|
381
|
+
-------
|
|
382
|
+
S5 : float [m]
|
|
383
|
+
Always 0.0 for round lenses.
|
|
384
|
+
"""
|
|
385
|
+
return 0.0
|
|
386
|
+
|
|
387
|
+
def coma(self):
|
|
388
|
+
"""Coma coefficient (off-axis aberration).
|
|
389
|
+
|
|
390
|
+
C_coma = (1/4) ∫ α·B² · g³·h dz
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
C_coma : float [m]
|
|
395
|
+
"""
|
|
396
|
+
integrand = self._alpha * self.B_on_grid**2 * self.g**3 * self.h
|
|
397
|
+
return simpson(integrand, x=self.z) / 4.0
|
|
398
|
+
|
|
399
|
+
def field_curvature(self):
|
|
400
|
+
"""Field curvature coefficient.
|
|
401
|
+
|
|
402
|
+
C_field = (1/2) ∫ α·B² · g²·h² dz
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
C_field : float [m]
|
|
407
|
+
"""
|
|
408
|
+
integrand = self._alpha * self.B_on_grid**2 * self.g**2 * self.h**2
|
|
409
|
+
return simpson(integrand, x=self.z) / 2.0
|
|
410
|
+
|
|
411
|
+
def distortion(self):
|
|
412
|
+
"""Distortion coefficient.
|
|
413
|
+
|
|
414
|
+
C_dist = ∫ α·B² · g·h³ dz
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
C_dist : float [m]
|
|
419
|
+
"""
|
|
420
|
+
integrand = self._alpha * self.B_on_grid**2 * self.g * self.h**3
|
|
421
|
+
return simpson(integrand, x=self.z)
|
|
422
|
+
|
|
423
|
+
def all_coefficients(self):
|
|
424
|
+
"""Compute all aberration coefficients.
|
|
425
|
+
|
|
426
|
+
Returns
|
|
427
|
+
-------
|
|
428
|
+
coeffs : dict
|
|
429
|
+
3rd-order: Cs, Cc, coma, field_curvature, distortion
|
|
430
|
+
5th-order: C5, S5
|
|
431
|
+
"""
|
|
432
|
+
return {
|
|
433
|
+
'Cs': self.Cs(),
|
|
434
|
+
'Cc': self.Cc(),
|
|
435
|
+
'C5': self.C5(),
|
|
436
|
+
'S5': self.S5(),
|
|
437
|
+
'coma': self.coma(),
|
|
438
|
+
'field_curvature': self.field_curvature(),
|
|
439
|
+
'distortion': self.distortion(),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
def krivanek_coefficients(self):
|
|
443
|
+
"""Return aberration coefficients in Krivanek notation.
|
|
444
|
+
|
|
445
|
+
Standard notation for aberration-corrected TEM (Krivanek et al.,
|
|
446
|
+
Ultramicroscopy 110 (2010) 571-585):
|
|
447
|
+
|
|
448
|
+
Order 1 (paraxial):
|
|
449
|
+
C1 — defocus (not computed here, depends on user setting)
|
|
450
|
+
A1 — 2-fold astigmatism (0 for round lenses)
|
|
451
|
+
|
|
452
|
+
Order 2 (off-axis):
|
|
453
|
+
B2 — axial coma (from coma integral)
|
|
454
|
+
A2 — 3-fold astigmatism (0 for round lenses)
|
|
455
|
+
|
|
456
|
+
Order 3 (3rd-order):
|
|
457
|
+
C3 — spherical aberration = Cs
|
|
458
|
+
S3 — star aberration (0 for round lenses)
|
|
459
|
+
A3 — 4-fold astigmatism (0 for round lenses)
|
|
460
|
+
|
|
461
|
+
Order 5 (5th-order):
|
|
462
|
+
C5 — 5th-order spherical aberration
|
|
463
|
+
S5 — 5th-order star aberration (0 for round lenses)
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
coeffs : dict
|
|
468
|
+
Krivanek-standard notation with values in meters.
|
|
469
|
+
Coefficients that are zero by symmetry are included
|
|
470
|
+
for completeness (important for abTEM interface).
|
|
471
|
+
"""
|
|
472
|
+
return {
|
|
473
|
+
# Order 1
|
|
474
|
+
'C1': 0.0, # defocus — user-settable, not a lens property
|
|
475
|
+
'A1': 0.0, # 2-fold astigmatism (round lens symmetry)
|
|
476
|
+
# Order 2
|
|
477
|
+
'B2': self.coma(),
|
|
478
|
+
'A2': 0.0, # 3-fold astigmatism (round lens symmetry)
|
|
479
|
+
# Order 3
|
|
480
|
+
'C3': self.Cs(),
|
|
481
|
+
'S3': 0.0, # star aberration (round lens symmetry)
|
|
482
|
+
'A3': 0.0, # 4-fold astigmatism (round lens symmetry)
|
|
483
|
+
# Order 5
|
|
484
|
+
'C5': self.C5(),
|
|
485
|
+
'S5': self.S5(),
|
|
486
|
+
# Chromatic
|
|
487
|
+
'Cc': self.Cc(),
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
def resolution_limit(self, aperture_angle=None):
|
|
491
|
+
"""Estimate resolution limits from aberration coefficients.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
aperture_angle : float [rad] or None
|
|
496
|
+
If None, uses Scherzer optimal angle
|
|
497
|
+
|
|
498
|
+
Returns
|
|
499
|
+
-------
|
|
500
|
+
dict with point_resolution, information_limit, scherzer_angle
|
|
501
|
+
"""
|
|
502
|
+
from .constants import electron_wavelength
|
|
503
|
+
|
|
504
|
+
lam = electron_wavelength(self.V)
|
|
505
|
+
Cs_val = self.Cs()
|
|
506
|
+
Cc_val = self.Cc()
|
|
507
|
+
|
|
508
|
+
if aperture_angle is None:
|
|
509
|
+
# Scherzer optimal aperture angle: α_opt = (4λ/Cs)^(1/4)
|
|
510
|
+
if Cs_val > 0:
|
|
511
|
+
alpha_opt = (4 * lam / abs(Cs_val))**(1/4)
|
|
512
|
+
else:
|
|
513
|
+
alpha_opt = 0.01 # default 10 mrad
|
|
514
|
+
else:
|
|
515
|
+
alpha_opt = aperture_angle
|
|
516
|
+
|
|
517
|
+
# Point resolution: δ = 0.61 · λ / α + Cs · α³
|
|
518
|
+
delta_point = 0.61 * lam / alpha_opt + abs(Cs_val) * alpha_opt**3
|
|
519
|
+
|
|
520
|
+
# Information limit from Cc (assuming 1 eV energy spread)
|
|
521
|
+
delta_E = 1.0 # eV
|
|
522
|
+
delta_cc = abs(Cc_val) * delta_E / self.V
|
|
523
|
+
|
|
524
|
+
# Scherzer resolution
|
|
525
|
+
if Cs_val > 0:
|
|
526
|
+
delta_scherzer = 0.66 * (Cs_val * lam**3)**(1/4)
|
|
527
|
+
else:
|
|
528
|
+
delta_scherzer = delta_point
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
'wavelength': lam,
|
|
532
|
+
'scherzer_angle': alpha_opt,
|
|
533
|
+
'point_resolution': delta_point,
|
|
534
|
+
'scherzer_resolution': delta_scherzer,
|
|
535
|
+
'cc_information_limit': delta_cc,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
def diagnostics(self):
|
|
539
|
+
"""Return diagnostic information for debugging.
|
|
540
|
+
|
|
541
|
+
Returns
|
|
542
|
+
-------
|
|
543
|
+
dict with k_sq, B0, a_fwhm, blend_weight, method
|
|
544
|
+
"""
|
|
545
|
+
w = _blend_weight(self._k_sq)
|
|
546
|
+
if w > 0.95:
|
|
547
|
+
method = 'Seidel'
|
|
548
|
+
elif w < 0.05:
|
|
549
|
+
method = 'Glaser'
|
|
550
|
+
else:
|
|
551
|
+
method = f'Blend(S:{w:.0%}/G:{1-w:.0%})'
|
|
552
|
+
return {
|
|
553
|
+
'k_sq': self._k_sq,
|
|
554
|
+
'B0': self._B0,
|
|
555
|
+
'a_fwhm': self._a_fwhm,
|
|
556
|
+
'blend_weight_seidel': w,
|
|
557
|
+
'method': method,
|
|
558
|
+
'f_eff': abs(1.0 / self._gp_image()) if abs(self._gp_image()) > 1e-30 else float('inf'),
|
|
559
|
+
'f_asym': abs(self._asymptotic_efl()),
|
|
560
|
+
'cs_power': self._cs_power,
|
|
561
|
+
'cc_power': self._cc_power,
|
|
562
|
+
'image_in_field': self._image_in_field(),
|
|
563
|
+
'C5_seidel': self._C5_seidel(),
|
|
564
|
+
'C5_glaser': self._C5_glaser(),
|
|
565
|
+
}
|