Source code for spinguin.api.relaxation_properties

"""
This module provides a RelaxationProperties class that stores information on the
relaxation theory settings. It is assigned as part of `SpinSystem` upon its
instantiation. The relaxation properties can be accessed as follows::

    import spinguin as sg                       # Import the package     
    spin_system = sg.SpinSystem(["1H"])         # Create an example spin system
    spin_system.relaxation.theory = "redfield"  # Set relaxation theory
"""

# Referencing SpinSystem class
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from spinguin.api.spin_system import SpinSystem

# Imports
import numpy as np
from typing import Literal
from spinguin.core.data_io import read_array
from spinguin.core.la import arraylike_to_array

[docs] class RelaxationProperties: """ This class stores information on the relaxation properties of a spin system. """ # Relaxation properties _antisymmetric: bool=False _dynamic_frequency_shift: bool=False _relative_error: float = 1e-6 _sr2k: bool=False _tau_c: float = None _theory: Literal["redfield", "phenomenological"] = None _thermalization: bool = False _T1: np.ndarray = None _T2: np.ndarray = None def __init__(self, spin_system: SpinSystem): print("Relaxation theory settings have been initialized with the " "following defaults: ") print(f"antisymmetric: {self.antisymmetric}") print(f"dynamic_frequency_shift: {self.dynamic_frequency_shift}") print(f"relative_error: {self.relative_error}") print(f"sr2k: {self.sr2k}") print(f"tau_c: {self.tau_c}") print(f"theory: {self.theory}") print(f"thermalization: {self.thermalization}") print(f"T1: {self.T1}") print(f"T2: {self.T2}") print() # Store a reference to the SpinSystem self._spin_system = spin_system @property def antisymmetric(self) -> bool: """ Specifies whether to consider the antisymmetric part of the interaction tensors in the Redfield relaxation theory. """ return self._antisymmetric @antisymmetric.setter def antisymmetric(self, antisymmetric: bool): self._antisymmetric = antisymmetric print("Antisymmetric part of the interaction tensors set to: " f"{self.antisymmetric}\n") @property def dynamic_frequency_shift(self) -> bool: """ Specifies whether to include the dynamic frequency shift in the Redfield relaxation theory. This corresponds to the imaginary part of the relaxation superoperator. """ return self._dynamic_frequency_shift @dynamic_frequency_shift.setter def dynamic_frequency_shift(self, dynamic_frequency_shift: bool): self._dynamic_frequency_shift = dynamic_frequency_shift print("Dynamic frequency shift set to: " f"{self.dynamic_frequency_shift}\n") @property def relative_error(self) -> float: """ Specifies the relative error for the Redfield relaxation theory. This corresponds to the convergence criterion for the Redfield integral. """ return self._relative_error @relative_error.setter def relative_error(self, relative_error: float): self._relative_error = relative_error print(f"Relative error set to: {self.relative_error}\n") @property def sr2k(self) -> bool: """ Specifies whether to include the scalar relaxation of the second kind (SR2K) in the relaxation superoperator. """ return self._sr2k @sr2k.setter def sr2k(self, sr2k: bool): self._sr2k = sr2k print(f"SR2K set to: {self.sr2k}\n") @property def tau_c(self) -> float: """ Specifies the correlation time (in seconds) for the Redfield relaxation theory. """ return self._tau_c @tau_c.setter def tau_c(self, tau_c: float): self._tau_c = tau_c print(f"Correlation time set to: {self.tau_c} s\n") @property def theory(self) -> str: """ Specifies the relaxation theory to be used. Can be either "redfield" or "phenomenological". """ return self._theory @theory.setter def theory(self, theory: Literal["redfield", "phenomenological"]): if theory not in ["redfield", "phenomenological"]: raise ValueError("Relaxation theory must be either 'redfield' or " "'phenomenological'.") self._theory = theory print(f"Relaxation theory set to: {self.theory}\n") @property def thermalization(self) -> bool: """ Specifies whether to apply Levitt-di Bari thermalization to the relaxation superoperator. """ return self._thermalization @thermalization.setter def thermalization(self, thermalization: bool): self._thermalization = thermalization print(f"Thermalization set to: {self.thermalization}\n") @property def T1(self) -> np.ndarray: """ Specifies the longitudinal relaxation time constants for each spin. These are used to create the phenomenological relaxation superoperator. Two input types are supported: - If `ArrayLike`: A 1D array of size N containing T1 times. - If `str`: Path to the file containing the T1 times. The input will be converted and stored as a NumPy array. Examples:: # Using array input spin_system.relaxation.T1 = np.array([5.5, 6.0, 2.7]) # Using string input spin_system.relaxation.T1 = "/path/to/the/file/T1.txt" """ return self._T1 @T1.setter def T1(self, T1: list | tuple | np.ndarray | str): # Handle string input if isinstance(T1, str): T1 = read_array(T1, data_type=float) # Handle array like input elif isinstance(T1, (list, tuple, np.ndarray)): T1 = arraylike_to_array(T1) # Otherwise throw an error else: raise TypeError("T1 should be a 1-dimensional array or a string.") # Check that the input is valid if T1.shape != self._spin_system.isotopes.shape: raise ValueError("Mismatch between the given T1 times and the " "number of spins in the system.") self._T1 = T1 print(f"T1 set to: {self.T1}\n") @property def T2(self) -> np.ndarray: """ Specifies the transverse relaxation time constants for each spin. These are used to create the phenomenological relaxation superoperator. Two input types are supported: - If `ArrayLike`: A 1D array of size N containing T2 times. Example: - If `str`: Path to the file containing the T2 times. The input will be stored as a NumPy array. Examples:: # Using array input spin_system.relaxation.T2 = np.array([5.5, 6.0, 2.7]) # Using string input spin_system.relaxation.T2 = "/path/to/the/file/T2.txt" """ return self._T2 @T2.setter def T2(self, T2: np.ndarray): # Handle string input if isinstance(T2, str): T2 = read_array(T2, data_type=float) # Handle array like input elif isinstance(T2, (list, tuple, np.ndarray)): T2 = arraylike_to_array(T2) # Otherwise throw an error else: raise TypeError("T2 should be a 1-dimensional array or a string.") # Check that the input is valid if T2.shape != self._spin_system.isotopes.shape: raise ValueError("Mismatch between the given T2 times and the " "number of spins in the system.") self._T2 = T2 print(f"T2 set to: {self.T2}\n") @property def R1(self) -> np.ndarray: """ Contains the longitudinal relaxation rates for each spin in the system. """ return 1 / self.T1 @property def R2(self) -> np.ndarray: """ Contains the transverse relaxation rates for each spin in the system. """ return 1 / self.T2