Source code for hyperfine.musr.meissner

"""Facilities for analyzing Meissner screening profiles determined from LEM measurements.
"""

from typing import Annotated, Sequence, override
import numpy as np
import pandas as pd
from scipy import constants, integrate, interpolate, special
from .. import distributions
from ..superconductivity import london, pippard


[docs] class DepthAveragingCalculator: """Calculator for convolving a Meissner screening profile with a muon stopping distribution. Class for handling the details required for calculating the mean magnetic field at a given muon implantation energy by convolving a Meissner screening profile with the corresponding stopping distribution. This instance assumes local electrodynamics for the screening profile. """
[docs] def __init__( self, file_name: str, interpolation: str = "linear", ) -> None: """Constructor. Args: file_name: Name of the CSV containing the stopping profile coefficients. interpolation: Type of interpolation scheme used for the stopping profile coefficients. """ # read the implantation distribution parameterss self.df = pd.read_csv(file_name, delimiter=",") # interpolation functions for the stopping distribution parameters self.z_max_1 = interpolate.interp1d( self.df["Energy (keV)"], self.df["z_max_1"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, ) self.alpha_1 = interpolate.interp1d( self.df["Energy (keV)"], self.df["alpha_1"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, ) self.beta_1 = interpolate.interp1d( self.df["Energy (keV)"], self.df["beta_1"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, ) self.fraction_1 = interpolate.interp1d( self.df["Energy (keV)"], self.df["fraction_1"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, ) self.z_max_2 = interpolate.interp1d( self.df["Energy (keV)"], self.df["z_max_2"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, ) self.alpha_2 = interpolate.interp1d( self.df["Energy (keV)"], self.df["alpha_2"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, ) self.beta_2 = interpolate.interp1d( self.df["Energy (keV)"], self.df["beta_2"], kind=interpolation, copy=True, bounds_error=True, assume_sorted=False, )
[docs] def stopping_distribution( self, depth_nm: Sequence[float], energy_keV: Annotated[float, 0:None], ) -> Sequence[float]: """Probability density function for the muon stopping distribution. The distribution is assumed to follow a weighted sum of two modified beta distributions. Args: depth_nm: Depth below the surface (nm). energy_keV: Muon implantation energy (keV). Returns: The probability density at depth_nm. """ return distributions.modified_beta_2_pdf( depth_nm, self.alpha_1(energy_keV), self.beta_1(energy_keV), self.z_max_1(energy_keV), self.fraction_1(energy_keV), self.alpha_2(energy_keV), self.beta_2(energy_keV), self.z_max_2(energy_keV), )
[docs] def calculate_mean_depth( self, energy_keV: Annotated[float, 0:None], ) -> float: """Calculate the mean muon stopping depth for a given implantation energy. The stopping distribution is assumed to follow a weighted sum of two modified beta distributions. Args: energy_keV: Muon implantation energy (keV). Returns: The mean stopping depth (nm). """ return distributions.modified_beta_2_mean( self.alpha_1(energy_keV), self.beta_1(energy_keV), self.z_max_1(energy_keV), self.fraction_1(energy_keV), self.alpha_2(energy_keV), self.beta_2(energy_keV), self.z_max_2(energy_keV), )
[docs] def _london( self, z: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> Sequence[float]: """London model for the Meissner screening profile. Args: z: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Thickness of the non-superconducting dead layer (nm). penetration_depth_nm: Effective magnetic penetration depth (nm). demagnetization_factor: Effective demagnetization factor. Returns: The magnetic field value at depth z below the surface (G). """ # determine the geometrically enhanced field value effective_field_G = applied_field_G / (1.0 - demagnetization_factor) return np.piecewise( z, [ z <= dead_layer_nm, z > dead_layer_nm, ], [ lambda x: effective_field_G + x * 0.0, lambda x: effective_field_G * np.exp(-(x - dead_layer_nm) / penetration_depth_nm), ], )
[docs] def london_ms( self, z: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> Sequence[float]: """London model for the Meissner screening profile. Args: z: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Thickness of the non-superconducting dead layer (nm). penetration_depth_nm: Effective magnetic penetration depth (nm). demagnetization_factor: Effective demagnetization factor. Returns: The magnetic field value at depth z below the surface (G). """ return self._london( z, applied_field_G, dead_layer_nm, penetration_depth_nm, demagnetization_factor, )
[docs] def calculate_mean_field( self, energy_keV: float, applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1], ) -> float: """Helper function for calculating the mean magnetic field below the surface. Args: energy_keV: Muon implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Thickness of the non-superconducting dead layer (nm). penetration_depth_nm: Effective magnetic penetration depth (nm). demagnetization_factor: Effective demagnetization factor. Returns: The mean magnetic field below the surface. """ # product of the london model w/ the implantion distribution def integrand(z: float) -> float: return self.london_ms( z, applied_field_G, dead_layer_nm, penetration_depth_nm, demagnetization_factor, ) * self.stopping_distribution(z, energy_keV) # do the numeric integration using adaptive Gaussian quadrature # https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.quad.html result, _ = integrate.quad( integrand, 0.0, # lower integration limit max( # upper integration limit np.max(self.z_max_1(energy_keV)), np.max(self.z_max_2(energy_keV)) ), epsabs=np.sqrt(np.finfo(float).eps), # absolute error tolerance epsrel=np.sqrt(np.finfo(float).eps), # relative error tolerance limit=np.iinfo(np.int32).max, # maximum number of subintervals points=[ # potential singularities/discontinuities in the integrand 0.0, dead_layer_nm, self.z_max_1(energy_keV), self.z_max_2(energy_keV), ], ) return result
[docs] def __call__( self, energy_keV: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:None], ) -> Sequence[float]: """Functor for calculating the mean magnetic field below the surface. Can accept arrays of energies as input! Args: energy_keV: Muon implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Thickness of the non-superconducting dead layer (nm). penetration_depth_nm: Effective magnetic penetration depth (nm). demagnetization_factor: Effective demagnetization factor. Returns: The mean magnetic field below the surface. """ # energy_keV = np.asarray(energy_keV) results = np.empty(energy_keV.size) for i, e_keV in enumerate(energy_keV): results[i] = self.calculate_mean_field( e_keV, applied_field_G, dead_layer_nm, penetration_depth_nm, demagnetization_factor, ) return results
[docs] class DepthAveragingCalculatorNL(DepthAveragingCalculator): """Calculator for convolving a Meissner screening profile with a muon stopping distribution. Class for handling the details required for calculating the mean magnetic field at a given muon implantation energy by convolving a Meissner screening profile with the corresponding stopping distribution. This instance assumes nonlocal electrodynamics for the screening profile. """
[docs] def __init__( self, file_name: str, interpolation: str = "linear", ) -> None: """Constructor. Args: file_name: Name of the CSV containing the stopping profile coefficients. interpolation: Type of interpolation scheme used for the stopping profile coefficients. """ # initialize from the base class super().__init__(file_name, interpolation)
[docs] def pippard_ms( self, depth_nm: float, applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], london_penetration_depth_nm: Annotated[float, 0:None], bcs_coherence_length_nm: Annotated[float, 0:None], mean_free_path_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1], temperature_K: Annotated[float, 0:None], critical_temperature_K: Annotated[float, 0:None], gap_0K_eV: Annotated[float, 0:None], ) -> float: """Pippard's nonlocal screening model. Assumes specular reflection of electrons at the surface. Args: depth_nm: Depth (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). london_penetration_depth_nm: London penetration depth (nm). bcs_coherence_length_nm: BCS coherence length (nm). mean_free_path_nm: Electron mean-free-path (nm). demagnetization_factor: Effective demagnetization factor. temperature_K: Absolute temperature (K). critical_temperature_K: Superconducting transition temperature (K). gap_0K_eV: Superconducting gap energy at 0 K (eV). Returns: The field screening profile at a given depth (G). """ return ( # nonlocal field screening model pippard.specular_profile_dl( depth_nm, temperature_K, critical_temperature_K, gap_0K_eV, london_penetration_depth_nm, mean_free_path_nm, bcs_coherence_length_nm, 1.0, dead_layer_nm, ) # effective applied magnetic field * applied_field_G / (1.0 - demagnetization_factor) )
[docs] def mean_field_integrand( self, z: float, energy_keV: float, applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], london_penetration_depth_nm: Annotated[float, 0:None], bcs_coherence_length_nm: Annotated[float, 0:None], mean_free_path_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1], temperature_K: Annotated[float, 0:None], critical_temperature_K: Annotated[float, 0:None], gap_0K_eV: Annotated[float, 0:None], ) -> float: """Integrand for calculating the mean magnetic field at a given implantation energy. Args: z: Depth (nm). energy_keV: Implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). london_penetration_depth_nm: London penetration depth (nm). bcs_coherence_length_nm: BCS coherence length (nm). mean_free_path_nm: Electron mean-free-path (nm). demagnetization_factor: Effective demagnetization factor. temperature_K: Absolute temperature (K). critical_temperature_K: Superconducting transition temperature (K). gap_0K_eV: Superconducting gap energy at 0 K (eV). Returns: Integrand for the average magnetic field at a given implantation energy (G). """ return self.pippard_ms( z, applied_field_G, dead_layer_nm, london_penetration_depth_nm, bcs_coherence_length_nm, mean_free_path_nm, demagnetization_factor, temperature_K, critical_temperature_K, gap_0K_eV, ) * self.stopping_distribution(z, energy_keV)
[docs] @override def calculate_mean_field( self, energy_keV: float, applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], london_penetration_depth_nm: Annotated[float, 0:None], bcs_coherence_length_nm: Annotated[float, 0:None], mean_free_path_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1], temperature_K: Annotated[float, 0:None], critical_temperature_K: Annotated[float, 0:None], gap_0K_eV: Annotated[float, 0:None], ) -> float: """Calculate average magnetic field at a given implantation energy. Args: energy_keV: Implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). london_penetration_depth_nm: London penetration depth (nm). bcs_coherence_length_nm: BCS coherence length (nm). mean_free_path_nm: Electron mean-free-path (nm). demagnetization_factor: Effective demagnetization factor. temperature_K: Absolute temperature (K). critical_temperature_K: Superconducting transition temperature (K). gap_0K_eV: Superconducting gap energy at 0 K (eV). Returns: The average magnetic field at a given implantation energy (G). """ # do the numeric integration using adaptive Gaussian quadrature # https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.quad.html result, _ = integrate.quad( self.mean_field_integrand, 0.0, # lower integration limit max( # upper integration limit np.max(self.z_max_1(energy_keV)), np.max(self.z_max_2(energy_keV)) ), args=( energy_keV, applied_field_G, dead_layer_nm, london_penetration_depth_nm, bcs_coherence_length_nm, mean_free_path_nm, demagnetization_factor, temperature_K, critical_temperature_K, gap_0K_eV, ), epsabs=np.sqrt(np.finfo(float).eps), # absolute error tolerance epsrel=np.sqrt(np.finfo(float).eps), # relative error tolerance limit=np.iinfo(np.int32).max, # maximum number of subintervals points=[ # potential singularities/discontinuities in the integrand 0.0, dead_layer_nm, self.z_max_1(energy_keV), self.z_max_2(energy_keV), ], ) return result
[docs] @override def __call__( self, energy_keV: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], london_penetration_depth_nm: Annotated[float, 0:None], bcs_coherence_length_nm: Annotated[float, 0:None], mean_free_path_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1], temperature_K: Annotated[float, 0:None], critical_temperature_K: Annotated[float, 0:None], gap_0K_eV: Annotated[float, 0:None], ) -> Sequence[float]: """Calculate average magnetic field at a given implantation energy. Functor version! Args: energy_keV: Implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). london_penetration_depth_nm: London penetration depth (nm). bcs_coherence_length_nm: BCS coherence length (nm). mean_free_path_nm: Electron mean-free-path (nm). demagnetization_factor: Effective demagnetization factor. temperature_K: Absolute temperature (K). critical_temperature_K: Superconducting transition temperature (K). gap_0K_eV: Superconducting gap energy at 0 K (eV). Returns: The average magnetic field at a given implantation energy. """ # make everything numpy arrays energy_keV = np.asarray(energy_keV) results = np.empty(energy_keV.size) for i, e_keV in enumerate(energy_keV): results[i] = self.calculate_mean_field( e_keV, applied_field_G, dead_layer_nm, london_penetration_depth_nm, bcs_coherence_length_nm, mean_free_path_nm, demagnetization_factor, temperature_K, critical_temperature_K, gap_0K_eV, ) return results
[docs] class DepthAveragingCalculatorGLE(DepthAveragingCalculator): """Calculator for convolving a Meissner screening profile with a muon stopping distribution. Class for handling the details required for calculating the mean magnetic field at a given muon implantation energy by convolving a Meissner screening profile with the corresponding stopping distribution. This instance assumes local electrodynamics following the generalized London equation (GLE). """
[docs] def __init__( self, file_name: str, interpolation: str = "linear", ) -> None: """Constructor. Args: file_name: Name of the CSV containing the stopping profile coefficients. interpolation: Type of interpolation scheme used for the stopping profile coefficients. """ # generalized London equation (GLE) solver self.gle = london.GLESolver() # initialize from the base class super().__init__(file_name, interpolation)
[docs] def mean_field_integrand( self, z: float, energy_keV: float, applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], surface_penetration_depth_nm: Annotated[float, 0:None], bulk_penetration_depth_nm: Annotated[float, 0:None], diffusion_length_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> float: """Integrand for calculating the mean magnetic field at a given implantation energy. Args: z: Depth (nm). energy_keV: Implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). surface_penetration_depth_nm: Magnetic penetration depth at the surface (nm). bulk_penetration_depth_nm: Magnetic penetration depth in the bulk (nm). diffusion_length_nm: Length of inhomogenous defect region (nm). demagnetization_factor: Effective demagnetization factor. Returns: Integrand for the average magnetic field at a given implantation energy (G). """ return self.gle.screening_profile( z, applied_field_G, dead_layer_nm, surface_penetration_depth_nm, bulk_penetration_depth_nm, diffusion_length_nm, demagnetization_factor, ) * self.stopping_distribution(z, energy_keV)
[docs] @override def calculate_mean_field( self, energy_keV: float, applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], surface_penetration_depth_nm: Annotated[float, 0:None], bulk_penetration_depth_nm: Annotated[float, 0:None], diffusion_length_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> float: """Calculate average magnetic field at a given implantation energy. Args: energy_keV: Implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). surface_penetration_depth_nm: Magnetic penetration depth at the surface (nm). bulk_penetration_depth_nm: Magnetic penetration depth in the bulk (nm). diffusion_length_nm: Length of inhomogenous defect region (nm). demagnetization_factor: Effective demagnetization factor. Returns: The average magnetic field at a given implantation energy (G). """ # do the numeric integration using adaptive Gaussian quadrature # https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.quad.html result, _ = integrate.quad( self.mean_field_integrand, 0.0, # lower integration limit max( # upper integration limit np.max(self.z_max_1(energy_keV)), np.max(self.z_max_2(energy_keV)) ), args=( energy_keV, applied_field_G, dead_layer_nm, surface_penetration_depth_nm, bulk_penetration_depth_nm, diffusion_length_nm, demagnetization_factor, ), epsabs=np.sqrt(np.finfo(float).eps), # absolute error tolerance epsrel=np.sqrt(np.finfo(float).eps), # relative error tolerance limit=np.iinfo(np.int32).max, # maximum number of subintervals points=[ # potential singularities/discontinuities in the integrand 0.0, dead_layer_nm, self.z_max_1(energy_keV), self.z_max_2(energy_keV), ], ) return result
[docs] @override def __call__( self, energy_keV: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], surface_penetration_depth_nm: Annotated[float, 0:None], bulk_penetration_depth_nm: Annotated[float, 0:None], diffusion_length_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> float: """Calculate average magnetic field at a given implantation energy. Functor version! Args: energy_keV: Implantation energy (keV). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer (nm). surface_penetration_depth_nm: Magnetic penetration depth at the surface (nm). bulk_penetration_depth_nm: Magnetic penetration depth in the bulk (nm). diffusion_length_nm: Length of inhomogenous defect region (nm). demagnetization_factor: Effective demagnetization factor. Returns: The average magnetic field at a given implantation energy (G). """ # make everything numpy arrays energy_keV = np.asarray(energy_keV) results = np.empty(energy_keV.size) for i, e_keV in enumerate(energy_keV): results[i] = self.calculate_mean_field( e_keV, applied_field_G, dead_layer_nm, surface_penetration_depth_nm, bulk_penetration_depth_nm, diffusion_length_nm, demagnetization_factor, ) return results