Source code for hyperfine.superconductivity.london

"""Expressions for the London model of magnetic field screening.
"""

import numpy as np
from scipy import constants, integrate, special
from typing import Annotated, Sequence


[docs] def screening_profile_bulk( depth_nm: 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], ) -> Sequence[float]: """Meissner screening profile for a bulk superconductor. Meissner screening profile derived from London theory for a bulk superconductor, wherein the material's electromagnetic response is treated in the local limit. Args: depth_nm: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Thickness of the non-superconducting surface dead layer (nm). penetration_depth_nm: Magnetic penetration depth (nm). demagnetization_factor: Effective demagnetization factor. Returns: The Meissner screening profile at depth ``depth_nm`` (G). Example: .. plot:: import numpy as np import matplotlib.pyplot as plt from hyperfine.superconductivity import london z = np.linspace(0.0, 200.0, 100) args = (100.0, 10.0, 50.0, 0.05) plt.plot(z, london.screening_profile_bulk(z, *args), "-") plt.xlabel("$z$ (nm)") plt.ylabel("$B(z)$ (nm)") plt.show() """ # effective field at the sample's surface effective_field_G = applied_field_G / (1.0 - demagnetization_factor) # correct for the dead layer thickness z_nm = depth_nm - dead_layer_nm # evaluate the expression as piecewise components return np.piecewise( z_nm, [ z_nm <= 0.0, z_nm > 0.0, ], [ lambda x: effective_field_G * np.ones(len(x)), lambda x: effective_field_G * np.exp(-x / penetration_depth_nm), ], )
[docs] def screening_profile_film( depth_nm: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_nm: Annotated[float, 0:None], film_thickness_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1], ) -> Sequence[float]: """Meissner screening profile for a thin film superconductor. Meissner screening profile derived from London theory for a thin film superconductor, wherein the material's electromagnetic response is treated in the local limit. Args: depth_nm: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Thickness of the non-superconducting surface dead layer (nm). film_thickness_nm: Film thickness (nm). penetration_depth_nm: Magnetic penetration depth (nm). demagnetization_factor: Effective demagnetization factor. Returns: The Meissner screening profile at depth ``depth_nm`` (G). Example: .. plot:: import numpy as np import matplotlib.pyplot as plt from hyperfine.superconductivity import london z = np.linspace(0.0, 200.0, 100) args = (100.0, 10.0, 50.0, 190.0, 0.0) plt.plot(z, london.screening_profile_film(z, *args), "-") plt.xlabel("$z$ (nm)") plt.ylabel("$B(z)$ (nm)") plt.show() """ # effective field at the sample's surface effective_field_G = applied_field_G / (1.0 - demagnetization_factor) # correct for the dead layer thickness z_nm = depth_nm - dead_layer_nm t_nm = film_thickness_nm - dead_layer_nm # evaluate the expression as piecewise components return np.piecewise( z_nm, [ z_nm <= 0.0, (z_nm > 0.0) & (z_nm <= t_nm), z_nm > t_nm, ], [ lambda x: effective_field_G * np.ones(len(x)), lambda x: effective_field_G * np.cosh((0.5 * t_nm - x) / penetration_depth_nm) / np.cosh(0.5 * t_nm / penetration_depth_nm), lambda x: effective_field_G * np.ones(len(x)), ], )
[docs] class GLESolver: """Generalized London Equation (GLE) Solver. Numerically solve the GLE for a depth-dependent magnetic penetration depth. See: M. Checchin et al., Appl. Phys. Lett. 117, 032601 (2020). https://doi.org/10.1063/5.0013698 See also Eq. (1) in: M. S. Pamianchi at al., Phys. Rev. B 50, 13659 (1994). https://doi.org/10.1103/PhysRevB.50.13659 Attributes: _x_nodes: x-values used as the initial mesh by the solver. _y_guess: y-values used as the guess for the function/derivative by the solver. _lambda_s: Magnetic penetration depth at the surface (nm). _lambda_0: Magnetic penetration depth in the bulk (nm). _delta: Diffusion length of the impurity layer (nm). _sol: Object encapsulating the solver's solution. """
[docs] def __init__( self, x_nodes_min: float = 0.0, x_nodes_max: float = 1000.0, x_nodes_num: int = 1001, ) -> None: """Constructor for the GLE Solver. Args: x_nodes_min: Minimum of the x-values used as the initial mesh by the solver. x_nodes_max: Maximum of the x-values used as the initial mesh by the solver. x_nodes_num: Number of x-values used as the initial mesh by the solver. """ # create the x-nodes self._x_nodes = np.linspace(x_nodes_min, x_nodes_max, num=x_nodes_num) # assign some internal variables with nonsensical values; this is to # diminish any likelihood of corresponding to a user's "real" input, # which is important for the arg check done in self.solve self._lambda_s = -1.0 self._lambda_0 = -1.0 self._delta = -1.0 # create an empty initial guess for the solver # values will be updated dynamically when calling self.solve self._y_guess = np.zeros((2, self._x_nodes.size))
[docs] def _lambda( self, x: Sequence[float], lambda_s: Annotated[float, 0:None], lambda_0: Annotated[float, 0:None], delta: Annotated[float, 0:None], ) -> Sequence[float]: """Postulated depth-dependence of the magnetic penetration depth. See Eq. (1) in: M. Checchin et al., Appl. Phys. Lett. 117, 032601 (2020). https://doi.org/10.1063/5.0013698 Args: x: Depth below the surface (nm). lambda_s: Magnetic penetration depth at the surface (nm). lambda_0: Magnetic penetration depth in the bulk (nm). delta: Diffusion length of impurities causing depth-dependence (nm). Returns: The magnetic penetration depth at depth x (nm). """ return (lambda_s - lambda_0) * special.erfc(x / delta) + lambda_0
[docs] def _lambda_prime( self, x: Sequence[float], lambda_s: Annotated[float, 0:None], lambda_0: Annotated[float, 0:None], delta: Annotated[float, 0:None], ) -> Sequence[float]: """First derivative of the postulated depth-dependence of the magnetic penetration depth. See Eq. (1) in: M. Checchin et al., Appl. Phys. Lett. 117, 032601 (2020). https://doi.org/10.1063/5.0013698 Args: x: Depth below the surface (nm). lambda_s: Magnetic penetration depth at the surface (nm). lambda_0: Magnetic penetration depth in the bulk (nm). delta: Diffusion length of impurities causing depth-dependence (nm). Returns: The first derivative of the magnetic penetration depth at depth x. """ return (lambda_s - lambda_0) * ( -2.0 * np.exp(-((x / delta) ** 2)) / (np.sqrt(np.pi) * delta) )
[docs] def _gle_derivs( self, t: Sequence[float], y: Sequence[float], ) -> Sequence[float]: """Right-hand side of the system of equations to solve, re-written as 1st-order expressions. Indexes [0] refer to the function being solved for. Indexes [1] refer to the function's derivative. Args: t: x-values. y: y-values Returns: An array of the system of 1st-order equations to solve. """ # variable substitution b, nu = y # evaluate the penetration depth and its derivative args = (self._lambda_s, self._lambda_0, self._delta) l = self._lambda(t, *args) l_prime = self._lambda_prime(t, *args) return np.vstack( [ nu, -(2.0 / l) * l_prime * nu + (1.0 / l**2) * b, ] )
[docs] def _bc( self, ya: Sequence[float], yb: Sequence[float], ) -> Sequence[float]: """Boundary conditions for the solver. Indexes [0] refer to the function being solved for. Indexes [1] refer to the function's derivative. Args: ya: Array of lower bounds. yb: Array of upper bounds. Returns: An array of penalties for the boundary conditions. """ return np.array( [ ya[0] - 1, # B(x) / B_0 = 1 yb[1], # B'(np.inf) / B_0 = 0 ] )
[docs] def solve( self, lambda_s: Annotated[float, 0:None], lambda_0: Annotated[float, 0:None], delta: Annotated[float, 0:None], tolerance: float = np.sqrt(np.finfo(float).eps), # 1e3 * np.finfo(float).eps, max_x_nodes: int = np.iinfo(np.int32).max, ) -> None: """Solve the GLE numerically. Args: lambda_s: Magnetic penetration depth at the surface (nm). lambda_0: Magnetic penetration depth in the bulk (nm). delta: Diffusion length of the impurity layer (nm). tolerance: Convergence criteria for the solver. max_x_nodes: Maximum number of x nodes used by the solver. """ # collect the function/class arguments for comparison fcn_args = (lambda_s, lambda_0, delta) cls_vals = (self._lambda_s, self._lambda_0, self._delta) # check if a solution has already been generated for the same inputs if fcn_args != cls_vals: # assign the arguments to data members self._lambda_s = lambda_s self._lambda_0 = lambda_0 self._delta = delta # dynamically create the initial guesses for the solver self._y_guess = np.zeros((2, self._x_nodes.size)) self._y_guess[0] = np.exp(-self._x_nodes / self._lambda_0) self._y_guess[1] = (-1.0 / self._lambda_0) * np.exp( -self._x_nodes / self._lambda_0 ) # solve the system of equations using boundary conditions and save the result self._sol = integrate.solve_bvp( self._gle_derivs, self._bc, self._x_nodes, self._y_guess, p=None, S=None, fun_jac=None, bc_jac=None, tol=tolerance, max_nodes=max_x_nodes, verbose=0, # bc_tol=np.sqrt(np.finfo(float).eps), )
[docs] def screening_profile( self, z_nm: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_surface_nm: Annotated[float, 0:None], penetration_depth_bulk_nm: Annotated[float, 0:None], diffusion_length_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> Sequence[float]: """Calculate the Meissner screening profile. Args: z_nm: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer thickness (nm). penetration_depth_surface_nm: Magnetic penetration depth at the surface (nm). penetration_depth_bulk_nm: Magnetic penetration depth in the bulk (nm). diffusion_length_nm: Diffusion length of the impurity layer (nm). demagnetization_factor: Effective demagnetization factor. Returns: The Meissner screening profile at depth z (G). Example: .. plot:: import numpy as np import matplotlib.pyplot as plt from hyperfine.superconductivity import london gles = london.GLESolver() z = np.linspace(0.0, 200.0, 100) args = (100.0, 10.0, 100.0, 30.0, 50.0, 0.05) plt.plot(z, gles.screening_profile(z, *args), "-") plt.xlabel("$z$ (nm)") plt.ylabel("$B(z)$ (nm)") plt.show() """ # solve the GLE self.solve( penetration_depth_surface_nm, penetration_depth_bulk_nm, diffusion_length_nm, ) # calculate the geometrically enhanced field value effective_field_G = applied_field_G / (1.0 - demagnetization_factor) # correct the depth for the dead layer z_corr_nm = z_nm - dead_layer_nm # return the current density return np.piecewise( z_corr_nm, [ z_corr_nm < 0.0, z_corr_nm >= 0.0, ], [ lambda x: np.full(x.shape, effective_field_G), lambda x: effective_field_G * self._sol.sol(x)[0], ], )
[docs] def current_density( self, z_nm: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_surface_nm: Annotated[float, 0:None], penetration_depth_bulk_nm: Annotated[float, 0:None], diffusion_length_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> Sequence[float]: """Calculate the current density profile. Args: z_nm: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer thickness (nm). penetration_depth_surface_nm: Magnetic penetration depth at the surface (nm). penetration_depth_bulk_nm: Magnetic penetration depth in the bulk (nm). diffusion_length_nm: Diffusion length of the impurity layer (nm). demagnetization_factor: Effective demagnetization factor. Returns: The current density profile at depth z (A m^-2). Example: .. plot:: import numpy as np import matplotlib.pyplot as plt from hyperfine.superconductivity import london gles = london.GLESolver() z = np.linspace(0.0, 200.0, 100) args = (100.0, 10.0, 100.0, 30.0, 50.0, 0.05) plt.plot(z, gles.current_density(z, *args), "-") plt.xlabel("$z$ (nm)") plt.ylabel("$J(z)$ (A m$^{-2}$)") plt.show() """ # solve the GLE self.solve( penetration_depth_surface_nm, penetration_depth_bulk_nm, diffusion_length_nm, ) # calculate the geometrically enhanced field value effective_field_G = applied_field_G / (1.0 - demagnetization_factor) # calculate the prefactor for the conversion G_per_T = 1e4 # nm_per_m = 1e9 m_per_nm = 1e-9 mu_0 = constants.value("vacuum mag. permeability") * G_per_T j_0 = -1.0 * effective_field_G / mu_0 # correct the depth for the dead layer z_corr_nm = z_nm - dead_layer_nm # return the screening profile return np.piecewise( z_corr_nm, [ z_corr_nm < 0.0, z_corr_nm >= 0.0, ], [ lambda x: np.full(x.shape, 0.0 * j_0), lambda x: j_0 * self._sol.sol(x)[1] / m_per_nm, ], )
[docs] def __call__( self, z_nm: Sequence[float], applied_field_G: Annotated[float, 0:None], dead_layer_nm: Annotated[float, 0:None], penetration_depth_surface_nm: Annotated[float, 0:None], penetration_depth_bulk_nm: Annotated[float, 0:None], diffusion_length_nm: Annotated[float, 0:None], demagnetization_factor: Annotated[float, 0:1] = 0.0, ) -> Sequence[float]: """Calculate the Meissner screening profile (alias for self.screening_profile). Args: z_nm: Depth below the surface (nm). applied_field_G: Applied magnetic field (G). dead_layer_nm: Non-superconducting dead layer thickness (nm). penetration_depth_surface_nm: Magnetic penetration depth at the surface (nm). penetration_depth_bulk_nm: Magnetic penetration depth in the bulk (nm). diffusion_length_nm: Diffusion length of the impurity layer (nm). demagnetization_factor: Effective demagnetization factor. Returns: The Meissner screening profile at depth z (G). Example: .. plot:: import numpy as np import matplotlib.pyplot as plt from hyperfine.superconductivity import london gles = london.GLESolver() z = np.linspace(0.0, 200.0, 100) args = (100.0, 10.0, 100.0, 30.0, 50.0, 0.05) plt.plot(z, gles(z, *args), "-") plt.xlabel("$z$ (nm)") plt.ylabel("$B(z)$ (nm)") plt.show() """ return self.screening_profile( z_nm, applied_field_G, dead_layer_nm, penetration_depth_surface_nm, penetration_depth_bulk_nm, diffusion_length_nm, demagnetization_factor, )