2024-06-04 09:27:34 +00:00
|
|
|
import logging
|
|
|
|
import numpy as np
|
2023-08-22 14:06:38 +00:00
|
|
|
|
2024-06-04 09:27:34 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2023-08-22 14:06:38 +00:00
|
|
|
|
|
|
|
class Sample:
|
|
|
|
"""
|
|
|
|
A class to represent a sample for NQR (Nuclear Quadrupole Resonance) Bloch Simulation.
|
|
|
|
"""
|
|
|
|
|
|
|
|
avogadro = 6.022e23
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2024-06-04 09:27:34 +00:00
|
|
|
name : str,
|
|
|
|
density : float,
|
|
|
|
molar_mass : float,
|
|
|
|
resonant_frequency : float,
|
|
|
|
gamma : float,
|
|
|
|
nuclear_spin : float,
|
|
|
|
spin_transition : int,
|
|
|
|
powder_factor : float,
|
|
|
|
filling_factor : float,
|
|
|
|
T1 : float,
|
|
|
|
T2 : float,
|
|
|
|
T2_star : float,
|
2023-08-22 14:06:38 +00:00
|
|
|
atom_density=None,
|
|
|
|
sample_volume=None,
|
2023-08-23 12:51:57 +00:00
|
|
|
sample_length=None,
|
2023-08-23 14:47:41 +00:00
|
|
|
sample_diameter=None,
|
2023-08-22 14:06:38 +00:00
|
|
|
):
|
|
|
|
"""
|
|
|
|
Constructs all the necessary attributes for the sample object.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
name : str
|
|
|
|
The name of the sample.
|
|
|
|
density : float
|
|
|
|
The density of the sample (g/m^3 or kg/m^3).
|
|
|
|
molar_mass : float
|
|
|
|
The molar mass of the sample (g/mol or kg/mol).
|
|
|
|
resonant_frequency : float
|
2024-06-02 18:31:51 +00:00
|
|
|
The resonant frequency of the sample in MHz.
|
2023-08-22 14:06:38 +00:00
|
|
|
gamma : float
|
2024-06-02 18:31:51 +00:00
|
|
|
The gamma value of the sample in MHz/T.
|
2023-08-22 14:06:38 +00:00
|
|
|
nuclear_spin : float
|
|
|
|
The nuclear spin quantum number of the sample.
|
2024-06-04 09:27:34 +00:00
|
|
|
spin_transition: int
|
|
|
|
The spin transition of the sample.
|
|
|
|
0 is -1/2 -> 1/2
|
|
|
|
1 is 1/2 -> 3/2
|
|
|
|
2 is 3/2 -> 5/2
|
|
|
|
3 is 5/2 -> 7/2
|
|
|
|
4 is 7/2 -> 9/2
|
2023-08-22 14:06:38 +00:00
|
|
|
powder_factor : float
|
|
|
|
The powder factor of the sample.
|
|
|
|
filling_factor : float
|
|
|
|
The filling factor of the sample.
|
|
|
|
T1 : float
|
2024-06-02 18:31:51 +00:00
|
|
|
The spin-lattice relaxation time of the sample in microseconds.
|
2023-08-22 14:06:38 +00:00
|
|
|
T2 : float
|
2024-06-02 18:31:51 +00:00
|
|
|
The spin-spin relaxation time of the sample in microseconds.
|
2023-08-22 14:06:38 +00:00
|
|
|
T2_star : float
|
2024-06-02 18:31:51 +00:00
|
|
|
The effective spin-spin relaxation time of the sample in microseconds.
|
2023-08-22 14:06:38 +00:00
|
|
|
atom_density : float, optional
|
|
|
|
The atom density of the sample (atoms per cm^3). By default None.
|
|
|
|
sample_volume : float, optional
|
|
|
|
The volume of the sample (m^3). By default None.
|
2023-08-23 12:51:57 +00:00
|
|
|
sample_length : float, optional
|
2024-06-02 18:31:51 +00:00
|
|
|
The length of the sample (mm). By default None.
|
2023-08-23 12:51:57 +00:00
|
|
|
sample_diameter : float, optional
|
2024-06-02 18:31:51 +00:00
|
|
|
The diameter of the sample m(m). By default None.
|
2023-08-22 14:06:38 +00:00
|
|
|
"""
|
|
|
|
self.name = name
|
|
|
|
self.density = density
|
|
|
|
self.molar_mass = molar_mass
|
2024-06-02 18:31:51 +00:00
|
|
|
self.resonant_frequency = resonant_frequency * 1e6
|
|
|
|
self.gamma = gamma * 1e6
|
2023-08-22 14:06:38 +00:00
|
|
|
self.nuclear_spin = nuclear_spin
|
2024-06-04 09:27:34 +00:00
|
|
|
self.spin_transition = spin_transition
|
|
|
|
self.spin_factor = self.calculate_spin_transition_factor(nuclear_spin, self.spin_transition)
|
2023-08-22 14:06:38 +00:00
|
|
|
self.powder_factor = powder_factor
|
|
|
|
self.filling_factor = filling_factor
|
2024-06-02 18:31:51 +00:00
|
|
|
self.T1 = T1 * 1e-6
|
|
|
|
self.T2 = T2 * 1e-6
|
|
|
|
self.T2_star = T2_star * 1e-6
|
2023-08-22 14:06:38 +00:00
|
|
|
self.atom_density = atom_density
|
|
|
|
self.sample_volume = sample_volume
|
2023-08-23 12:51:57 +00:00
|
|
|
self.sample_length = sample_length
|
|
|
|
self.sample_diameter = sample_diameter
|
2023-08-22 14:06:38 +00:00
|
|
|
self.calculate_atoms()
|
|
|
|
|
|
|
|
def calculate_atoms(self):
|
|
|
|
"""
|
2023-08-23 12:51:57 +00:00
|
|
|
Calculate the number of atoms in the sample per volume unit. This only works if the sample volume and atom density are provided.
|
|
|
|
Also the sample should be cylindrical.
|
2023-08-22 14:06:38 +00:00
|
|
|
|
|
|
|
If atom density and sample volume are provided, use these to calculate the number of atoms.
|
|
|
|
If not, use Avogadro's number, density, and molar mass to calculate the number of atoms.
|
|
|
|
"""
|
|
|
|
if self.atom_density and self.sample_volume:
|
|
|
|
self.atoms = (
|
|
|
|
self.atom_density
|
|
|
|
* self.sample_volume
|
|
|
|
/ 1e-6
|
2023-08-23 12:51:57 +00:00
|
|
|
/ (self.sample_volume * self.sample_length / self.sample_diameter)
|
2023-08-22 14:06:38 +00:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.atoms = self.avogadro * self.density / self.molar_mass
|
2024-06-04 09:27:34 +00:00
|
|
|
|
|
|
|
def pauli_spin_matrices(self, spin):
|
|
|
|
"""
|
|
|
|
Generate the spin matrices for a given spin value.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
spin (float): The spin value, which can be a half-integer or integer.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
tuple: A tuple containing the following elements:
|
|
|
|
Jx (np.ndarray): The x-component of the spin matrix.
|
|
|
|
Jy (np.ndarray): The y-component of the spin matrix.
|
|
|
|
Jz (np.ndarray): The z-component of the spin matrix.
|
|
|
|
J_minus (np.ndarray): The lowering operator matrix.
|
|
|
|
J_plus (np.ndarray): The raising operator matrix.
|
|
|
|
m (np.ndarray): The array of magnetic quantum numbers.
|
|
|
|
"""
|
|
|
|
|
|
|
|
m = np.arange(spin, -spin-1, -1)
|
|
|
|
paulirowlength = int(spin * 2 + 1)
|
|
|
|
|
|
|
|
pauli_z = np.diag(m)
|
|
|
|
pauli_plus = np.zeros((paulirowlength, paulirowlength))
|
|
|
|
pauli_minus = np.zeros((paulirowlength, paulirowlength))
|
|
|
|
|
|
|
|
for row_index in range(paulirowlength - 1):
|
|
|
|
col_index = row_index + 1
|
|
|
|
pauli_plus[row_index, col_index] = np.sqrt(spin * (spin + 1) - m[col_index] * (m[col_index] + 1))
|
|
|
|
|
|
|
|
for row_index in range(1, paulirowlength):
|
|
|
|
col_index = row_index - 1
|
|
|
|
pauli_minus[row_index, col_index] = np.sqrt(spin * (spin + 1) - m[col_index] * (m[col_index] - 1))
|
|
|
|
|
|
|
|
Jx = 0.5 * (pauli_plus + pauli_minus)
|
|
|
|
Jy = -0.5j * (pauli_plus - pauli_minus)
|
|
|
|
Jz = pauli_z
|
|
|
|
|
|
|
|
return Jx, Jy, Jz, pauli_minus, pauli_plus, m
|
|
|
|
|
|
|
|
def calculate_spin_transition_factor(self, I, transition):
|
|
|
|
"""
|
|
|
|
Calculate the prefactor for the envisaged spin transition for a given nuclear spin.
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
I (float): The nuclear spin value, which can be a half-integer or integer.
|
|
|
|
transition (int): The index of the transition.
|
|
|
|
The transition indices represent the shifts between magnetic quantum numbers m:
|
|
|
|
- 0 represents -1/2 --> 1/2
|
|
|
|
- 1 represents 1/2 --> 3/2
|
|
|
|
- 2 represents 3/2 --> 5/2
|
|
|
|
(only valid transitions based on spin value I are allowed)
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
float: The prefactor for the envisaged spin transition.
|
|
|
|
"""
|
|
|
|
m_values = np.arange(I, -I-1, -1)
|
|
|
|
if transition < 0 or transition >= len(m_values) - 1:
|
|
|
|
raise ValueError(f"Invalid transition for spin {I}. Valid range is 0 to {len(m_values) - 2}")
|
|
|
|
|
|
|
|
Jx, Jy, Jz, J_minus, J_plus, m = self.pauli_spin_matrices(I)
|
|
|
|
trindex = int(len(Jx) / 2 - transition)
|
|
|
|
spinfactor = Jx[trindex - 1, trindex]
|
|
|
|
|
|
|
|
logger.debug(f"Spin transition factor for I={I}, transition={transition}: {np.real(spinfactor)}")
|
|
|
|
logger.info(f"Jx is {Jx}")
|
|
|
|
return np.real(spinfactor)
|