soprano.nmr.utils#

Utility functions for NMR-related properties

soprano.nmr.utils._J_constant(Kij, gi, gj)[source]#

J coupling constants for pairs ij, with reduced constant Kij and gyromagnetic ratios gi and gj

Return type:

FloatOrArray

soprano.nmr.utils._alpha_zero_euler_angles(R, convention='zyz', eps=1e-09)[source]#

Active ZYZ/ZXZ angles of R with alpha == 0 (R from _alpha_zero_gauge()).

No range normalisation is applied here: beta is in [0, pi] and gamma in [0, 2 pi). NMRTensor.euler_angles normalises afterwards, as TensorView does, which turns alpha = 0 into pi when beta >= pi/2. At gimbal lock (beta = 0 or pi) only a combination of alpha and gamma is defined, and it is all put into gamma.

Parameters:
  • R (ndarray)

  • convention (str)

  • eps (float)

Return type:

ndarray

soprano.nmr.utils._alpha_zero_gauge(E, convention='zyz')[source]#

Pick the PAS frame of a tensor with a unique x axis that makes the active alpha zero.

A tensor with eigenvalues [a, b, b] is symmetric under any rotation about its PAS x axis, so E -> E Rx(phi) describes the same tensor. TensorView fixes this freedom by asking for alpha = 0 in the active ZYZ/ZXZ angles (Eq. 70 of Svenningsson & Mueller, Solid State Nucl. Magn. Reson. 123 (2023) 101849). Here the same phi is found directly from the matrix: alpha is the azimuth of the third column of E, so phi rotates that column into the xz plane (ZYZ) or the yz plane (ZXZ), on the side with the sign that gives alpha = 0 rather than pi. There are no divisions by sin(gamma) cos(gamma) or square roots, so there are no singular orientations.

Parameters:
  • E (ndarray)

  • convention (str)

Return type:

ndarray

soprano.nmr.utils._anisotropy(haeb_evals, reduced=False)[source]#

Calculate anisotropy given eigenvalues sorted with Haeberlen convention

Return type:

FloatOrArray

soprano.nmr.utils._asymmetry(haeb_evals)[source]#

Calculate asymmetry

Return type:

FloatOrArray

soprano.nmr.utils._degeneracy_pattern(eigenvalues, eps=1e-06)[source]#

Classify the eigenvalue pattern of a tensor in its stored (sorted) order.

Returns (degeneracy, unique_axis): degeneracy is 1 (triaxial), 2 (axial) or 3 (spherical); unique_axis is the index (0, 1, 2 for x, y, z) of the eigenvalue that differs from the other two when degeneracy == 2, else None. Every pair is compared, so [a, b, b] is recognised (a unique axis along x), which comparing everything to eigenvalues[0] would miss.

Parameters:

eps (float)

soprano.nmr.utils._dip_constant(Rij, gi, gj)[source]#

Dipolar constants for pairs ij, with distances Rij and gyromagnetic ratios gi and gj

Return type:

FloatOrArray

soprano.nmr.utils._dip_tensor(d, r, rotation_axis=None)[source]#

Full dipolar tensor given a constant and a connecting vector

soprano.nmr.utils._ensure_tensor_format(tensors)[source]#

Ensure tensors are in 3x3 format, converting from flattened 1x9 if necessary.

Parameters:

tensors (np.ndarray) – Tensor array, either (N, 3, 3) or (N, 9)

Returns:

Tensors in (N, 3, 3) format

Return type:

np.ndarray

soprano.nmr.utils._equivalent_euler(euler_angles, passive=False)[source]#

Find the equivalent Euler angles for a given set of Euler angles.

This set should be correct for NMR tensors, according to TensorView for MATLAB: [6]

Parameters:
  • euler_angles (ndarray)

  • passive (bool)

Return type:

ndarray

soprano.nmr.utils._equivalent_relative_euler(euler_angles, passive=False)[source]#

Returns a list of 16 equivalent relative Euler angles for a given set of Euler angles that corresponds to the relative orientation of two NMR tensors. See TensorView for MATLAB: [6]

Parameters:
  • euler_angles (ndarray)

  • passive (bool)

Return type:

ndarray

soprano.nmr.utils._evals_sort(evals, convention='i', return_indices=False)[source]#

Sort a list of eigenvalue triplets by various conventions

The sorting conventions are: - i: increasing order - d: decreasing order - h: Haeberlen order - n: NQR order (absolute values)

Parameters:
  • evals (np.ndarray) – Array of eigenvalue triplets to sort.

  • convention (str) – The sorting convention to use (‘i’, ‘d’, ‘h’, ‘n’).

  • return_indices (bool) – Whether to return the sorting indices.

  • Returns – Union[np.ndarray, tuple[np.ndarray, np.ndarray]]: Sorted eigenvalues, and optionally the sorting indices.

Return type:

ndarray | tuple[ndarray, ndarray]

soprano.nmr.utils._evecs_2_quat(evecs)[source]#

Convert a set of eigenvectors to a Quaternion expressing the rotation of the tensor’s PAS with respect to the Cartesian axes

soprano.nmr.utils._get_tensor_array(s, tensor_tag)[source]#

Get tensor array from structure, ensuring 3x3 format.

Parameters:
  • s (Atoms) – The structure

  • tensor_tag (str) – Name of the tensor array

Returns:

Tensor array in (N, 3, 3) format

Return type:

np.ndarray

Raises:

RuntimeError – If tensor array not found

soprano.nmr.utils._gradients_to_list(gradients, elements)[source]#

Convert gradients input to a list with one value per element in elements.

Parameters: gradients (Union[float, dict[str, float], list[float]]): The gradients input, which can be a float, a dictionary, or a list. elements (list[str]): The list of elements.

Returns: Optional[list[Optional[float]]]: A list of gradients corresponding to the elements, or None if gradients is None.

Raises: ValueError: If the gradients input is not a float, dictionary, or list, or if the length of the gradients list does not match the length of elements.

Parameters:
  • gradients (float | dict[str, float] | list[float])

  • elements (list[str])

Return type:

list[float]

soprano.nmr.utils._handle_euler_edge_cases(euler_angles, eigenvalues, convention='zyz', passive=False, eps=1e-06, angles_are_passive=False)[source]#

Handle edge cases in the Euler angle conventions for degenerate tensors.

Handled here: spherical tensors (all angles zero) and tensors with the unique axis along z (the free angle is set to zero). Tensors with the unique axis along x need the PAS frame to be chosen before the angles are extracted (_alpha_zero_gauge(), done by NMRTensor.euler_angles); if they arrive here their angles are just normalised.

Parameters:
  • euler_angles (np.ndarray) – Euler angles in radians

  • eigenvalues (np.ndarray) – Eigenvalues of the tensor

  • convention (str, optional) – Euler angle convention. Defaults to “zyz”.

  • passive (bool, optional) – Whether the angles are passive. Defaults to False.

  • eps (float, optional) – Tolerance for degeneracy. Defaults to 1e-6.

  • angles_are_passive (bool, optional) – Whether euler_angles are passive angles on entry. Only affects which angle is the free one for a tensor with its unique axis along z: gamma if the angles are active, alpha if they are passive. Defaults to False.

soprano.nmr.utils._matrix_to_euler(R, convention='zyz', passive=False)[source]#

Convert an NMR tensor eigenframe matrix to NMR-canonical Euler angles (in radians).

NMR-specific function. This function is designed for use with NMR tensor eigenframes, where eigenvector signs are physically arbitrary. It applies _normalise_euler_angles() after extracting angles from scipy, which restricts β to [0, π/2] by exploiting the sign freedom of eigenvectors. This transformation does not preserve a general rotation matrix: for arbitrary rotations with β > π/2 the returned angles reconstruct a different matrix. Do not use this function for general rotation matrices.

SciPy’s upper-case convention strings denote intrinsic rotations; all input convention strings are converted to upper case so the behaviour is always intrinsic.

Parameters:
  • R (list[list[float]] | ndarray) – Rotation matrix (3×3), typically the eigenvector matrix of an NMR tensor. Array-like inputs are accepted. If det(R) < 0, the last column is negated before proceeding. For eigenvector matrices this is valid because eigenvector signs are arbitrary; for general improper rotations it is not the nearest proper rotation in a metric sense.

  • convention (str) – Euler-angle convention, e.g. "zyz" or "zxz". Converted to upper case internally. Defaults to "zyz".

  • passive (bool) – If True, return the passive (alias) angles, i.e. those describing the rotation of the coordinate frame rather than the object. Defaults to False.

Returns:

NMR-canonical Euler angles in radians, shape (3,).

Return type:

np.ndarray

soprano.nmr.utils._normalise_euler_angles(euler_angles, passive=False, eps=1e-06)[source]#

Normalise Euler angles to the orientation-equivalent set with beta in [0, pi/2).

The four equivalent sets of a tensor (a half turn about each PAS axis) are those of TensorView for MATLAB [6], Eqs. 16-23. The paper allows 0 <= beta <= pi; the member chosen here is the one TensorView’s code reports (MFtoEuler.m and RelativeAngles.m): beta in [0, pi/2), then gamma in [0, pi) for active angles or alpha in [0, pi) for passive ones. The result always describes the same tensor.

Parameters:
  • euler_angles (np.ndarray) – Euler angles in radians

  • passive (bool, optional) – Whether the angles are passive rotations. Defaults to False.

  • eps (float, optional) – Tolerance for degeneracy. Defaults to 1e-6.

Return type:

ndarray

soprano.nmr.utils._references_to_list(references, elements)[source]#

Convert references input to a list with one value per element in elements.

Parameters: references (Union[None, dict[str, float], list[float]]): The references input, which can be None, a dictionary, or a list. elements (list[str]): The list of elements.

Returns: Optional[list[Optional[float]]]: A list of references corresponding to the elements, or None if references is None.

Raises: ValueError: If the references input is not None, a dictionary, or a list, or if the length of the references list does not match the length of elements.

Parameters:
  • references (None | dict[str, float] | list[float])

  • elements (list[str])

Return type:

list[float | None]

soprano.nmr.utils._skew(evals)[source]#

Calculate skew

\[\kappa = 3 (\sigma_{iso} - \sigma_{22}) / \Omega\]

where \(\Omega\) is the span of the tensor.

Note that for chemical shift tensors (\(\delta\)), the sign is reversed.

Return type:

FloatOrArray

soprano.nmr.utils._span(evals)[source]#

Calculate span

\[\Omega = \sigma_{33} - \sigma_{11}\]

where \(\sigma_{33}\) is the largest, and \(\sigma_{11}\) is the smallest eigenvalue.

Return type:

FloatOrArray

soprano.nmr.utils._split_species(species)[source]#

Validate the species string and extract the isotope number and element.

Parameters:

species (str) – The species string to validate. e.g. ‘13C’ or ‘1H’.

Returns:

A tuple containing the isotope number (int), and element symbol (str, such as ‘C’ or ‘H’).

Return type:

tuple

soprano.nmr.utils._test_euler_rotation(euler_angles, eigenvalues, eigenvecs, convention='zyz', passive=False, eps=1e-06)[source]#

Test that the Euler angles correctly rotate the tensor.

We compare the tensor rotated by the Euler angles to that you get by rotating the tensor with the rotation matrix corresponding to the Euler angles. i.e.

PAS = np.diag(eigenvalues) R = Rotation.from_euler(convention, euler_angles).as_matrix() A_rot = np.dot(R, np.dot(PAS, R.T))

B_rot = np.dot(eigenvecs, np.dot(PAS, eigenvecs.T))

Parameters:
  • euler_angles (np.ndarray) – Euler angles in radians

  • eigenvalues (np.ndarray) – Eigenvalues of the tensor

  • eigenvecs (np.ndarray) – Eigenvectors of the tensor

  • convention (str, optional) – Euler angle convention. Defaults to “zyz”.

  • passive (bool, optional) – Whether the angles are passive rotations. Defaults to False.

  • eps (float, optional) – Tolerance for degeneracy. Defaults to 1e-6.

Returns:

True if the Euler angles correctly rotate the tensor. False otherwise.

Return type:

bool