Source code for quscope.utils.kirkland

"""
Kirkland Atomic Potential Calculations

This module implements the Kirkland atomic potential parameterization
for electron scattering calculations in TEM simulations.

Reference:
E. J. Kirkland, "Advanced Computing in Electron Microscopy", 2nd Edition
Appendix C - Atomic Potentials and Scattering Factors
"""

import json
import os

import numpy as np
from scipy.special import kn

KIRKLAND_SCATTERING_FACTOR = 14.4  # eV⋅Å


[docs] class KirklandPotential: """Calculate atomic potentials using Kirkland parametrization.""" def __init__(self, params_file=None): if params_file is None: # Packaged copy (ships in wheels); pass a path to override. params_file = os.path.join(os.path.dirname(__file__), "kirkland.json") self.params_file = params_file self.parameters = self.load_parameters() or {}
[docs] def load_parameters(self): try: with open(self.params_file, "r") as f: return json.load(f) except FileNotFoundError: print( f"Warning: Kirkland parameter file '{self.params_file}' not found. Using empty parameter set." ) return None except json.JSONDecodeError as e: print( f"Warning: Failed to parse Kirkland parameter file '{self.params_file}': {e}" ) return None
[docs] def kirkland_potential_2d(self, x_grid, y_grid, atom_x, atom_y, Z, element=None): element = element or self.get_element_symbol(Z) if element not in self.parameters: raise ValueError( f"Element with Z={Z} ({element}) not found in Kirkland parameters" ) params = self.parameters[element] a = np.array(params[0], dtype=float) b = np.array(params[1], dtype=float) c = np.array(params[2], dtype=float) d = np.array(params[3], dtype=float) r2 = (x_grid - atom_x) ** 2 + (y_grid - atom_y) ** 2 r = np.sqrt(r2 + 1e-16) V = np.zeros_like(r, dtype=float) for i in range(3): if b[i] > 0: arg = 2 * np.pi * r * np.sqrt(b[i]) mask_small = arg < 50 mask_large = ~mask_small if np.any(mask_small): V[mask_small] += 4 * np.pi**2 * a[i] * kn(0, arg[mask_small]) if np.any(mask_large): x = arg[mask_large] V[mask_large] += ( 4 * np.pi**2 * a[i] * np.sqrt(np.pi / (2 * x)) * np.exp(-x) ) for i in range(3): if d[i] > 0: V += ( 2 * np.pi ** (3 / 2) * c[i] / d[i] ** (3 / 2) * np.exp(-np.pi**2 * r2 / d[i]) ) center_mask = r < 1e-8 if np.any(center_mask): V_center = 0.0 for i in range(3): if b[i] > 0: small_arg = 2 * np.pi * 1e-8 * np.sqrt(b[i]) V_center += ( 4 * np.pi**2 * a[i] * (-np.log(small_arg / 2) - 0.5772156649) ) for i in range(3): if d[i] > 0: V_center += 2 * np.pi ** (3 / 2) * c[i] / d[i] ** (3 / 2) V[center_mask] = V_center V *= KIRKLAND_SCATTERING_FACTOR return V
[docs] def get_element_symbol(self, Z): elements = { 1: "H", 2: "He", 3: "Li", 4: "Be", 5: "B", 6: "C", 7: "N", 8: "O", 9: "F", 10: "Ne", 11: "Na", 12: "Mg", 13: "Al", 14: "Si", 15: "P", 16: "S", 17: "Cl", 18: "Ar", 19: "K", 20: "Ca", 21: "Sc", 22: "Ti", 23: "V", 24: "Cr", 25: "Mn", 26: "Fe", 27: "Co", 28: "Ni", 29: "Cu", 30: "Zn", 31: "Ga", 32: "Ge", 33: "As", 34: "Se", 35: "Br", 36: "Kr", 37: "Rb", 38: "Sr", 39: "Y", 40: "Zr", 41: "Nb", 42: "Mo", 43: "Tc", 44: "Ru", 45: "Rh", 46: "Pd", 47: "Ag", 48: "Cd", 49: "In", 50: "Sn", 51: "Sb", 52: "Te", 53: "I", 54: "Xe", 55: "Cs", 56: "Ba", 57: "La", 58: "Ce", 59: "Pr", 60: "Nd", 61: "Pm", 62: "Sm", 63: "Eu", 64: "Gd", 65: "Tb", 66: "Dy", 67: "Ho", 68: "Er", 69: "Tm", 70: "Yb", 71: "Lu", 72: "Hf", 73: "Ta", 74: "W", 75: "Re", 76: "Os", 77: "Ir", 78: "Pt", 79: "Au", 80: "Hg", 81: "Tl", 82: "Pb", 83: "Bi", 84: "Po", 85: "At", 86: "Rn", 87: "Fr", 88: "Ra", 89: "Ac", 90: "Th", 91: "Pa", 92: "U", 93: "Np", 94: "Pu", 95: "Am", 96: "Cm", 97: "Bk", 98: "Cf", 99: "Es", 100: "Fm", } return elements.get(Z, f"Z{Z}")