Changelog#
All notable changes to Soprano are documented here.
v0.12.0 (unreleased)#
Breaking changes#
NMRTensor.euler_toandequivalent_euler_tonow return the inverse rotation for triaxial and mixed axial/triaxial pairs, and the same call gives different numbers. This is a change of convention, not a bug fix.A.euler_to(B)is now the orientation ofBin the PAS ofA(E_B = E_A R), as the method name reads. It used to be the orientation ofAin the PAS ofB, which is TensorView’s positional order forRelativeAngles(Tensor1, Tensor2). The argument order is now the reverse of TensorView’s: to compare with TensorView, putBfirst. To reproduce old values, callB.euler_to(A); the result is equivalent but may be a different one of the 16 equivalent angle sets. Code that uses relative angles (for example to set up mrsimulator couplings) must be recomputed or have its arguments swapped. There is no deprecation period, because a warning cannot tell old callers from new ones. Details are under “Relative Euler angles” below.Exported Euler angles change (
to_simpson()for axial tensors;to_mrsimulator()for every tensor). Re-export any spin systems generated with earlier versions. See below.NMRTensor.euler_towith two axial tensors, either of which has its unique axis along y, now raisesValueErroraseuler_anglesdoes (this order is not reachable with the supported tensor orders).
Fixes#
Tensor orientations in the SIMPSON and mrsimulator exports#
Several bugs made the exported Euler angles wrong whenever more than one oriented interaction was present (single-interaction powder spectra do not depend on orientation, so these went unnoticed). Re-export any spin systems you generated with earlier versions.
mrsimulator angles.
Site.to_mrsimulator(),ISCoupling.to_mrsimulator()andDipolarCoupling.to_mrsimulator()now write the angles in the convention of mrsimulator’s own code (E = Rz(gamma) Ry(beta) Rz(alpha), as inmrsimulator.utils.cartesian_tensor), via the newNMRTensor.mrsimulator_euler_angles(). Previously they wrote TensorView’s active ZYZ angles,E = Rz(alpha) Ry(beta) Rz(gamma). mrsimulator’s code uses the same matrices withalphaandgammain each other’s places, so those numbers described a different relative orientation in mrsimulator. This is a difference between two conventions, not a bug in mrsimulator. These are not the numbersto_simpson()writes: SIMPSON uses passive angles in degrees.Axial tensors (SIMPSON and mrsimulator). For axial tensors (every dipolar coupling, and any CSA, EFG or J tensor with
eta = 0) the passive angles had the wrong angle zeroed, so the unique axis kept its polar angle but lost its azimuth. A dipole at azimuth 40° was written as pointing at 180°. Spectra with an axial tensor tilted out of the xz-plane relative to another tensor were wrong in both programs.mrsimulator
zetaand referencing.zetanow follows the same referencing as the isotropic shift andto_simpson()(-shift_reduced_anisotropy). With a customgradientit was previously unscaled; with the default full formula it changes by a factor1 / (1 - reference * 1e-6)(about 3e-5 for ¹H, 1e-4 for ¹³C).mrsimulator and tensor order.
Site.to_mrsimulator()now uses Haeberlen ordering for the shielding angles, asto_simpson()does, so they refer to the same axes aszetaandetafor tensors built with anotherorder.Coupling exports and tensor order.
ISCoupling.to_simpson()andto_mrsimulator()took the Euler angles in the tensor’s own eigenvalue order, whilezetaandetaare always Haeberlen, so the angles could refer to different axes. They now use Haeberlen order.DipolarCoupling.coupling_constant,to_simpson()andto_mrsimulator()assumed NQR order (the unique eigenvalue last). A tensor in another order gave a wrong coupling constant, for exampled/2with the opposite sign in increasing order withd < 0, and wrong angles. The coupling constant is now half the tensor’s reduced anisotropy, which doesn’t depend on the order, and the angles use Haeberlen order like the other exports. The angles are taken from a copy, and the coupling’s own tensor is not changed. Couplings fromJCouplingList,DipolarCouplingListandDipolarCoupling.from_distance_vectoralready used these orders and are unaffected. Only couplings built from anNMRTensorin another order change.Site.to_mrsimulator()no longer changes the order of the site’s own EFG tensor (it works on a copy, asto_simpson()does).ElectricFieldGradient.__str__and a comment gave the quadrupole moment units as barns; it is millibarn.
Relative Euler angles (euler_to) and axial tensors#
NMRTensor.euler_toandequivalent_euler_toreturned the orientation of the first tensor in the PAS of the second for triaxial and for mixed axial/triaxial pairs (TensorView’s positional order), i.e. both active and passive angles described the inverse of what they now return. They now return the rotation carrying the first tensor’s PAS onto the second’s (E_other = E_self R(alpha, beta, gamma)), the orientation of the second tensor in the PAS of the first (Eq. 31 of the TensorView paper).A.euler_to(B)therefore matches TensorView’sRelativeAngles(Tensor1, Tensor2)with Tensor 1 =B, Tensor 2 =A. Anything computed from relative angles with an earlier version (for example, to set up mrsimulator couplings) should be recomputed. The docstring anddocs/simpson-conventions.mdnow say which way round this is.Two axial tensors: the angle between the unique axes was wrong when either tensor has its unique axis along x (as for increasing order), and two tensors whose axes were not aligned could be reported as “perfectly aligned” for particular geometries.
A tensor with its unique axis along x (e.g. eigenvalues
[1, 2, 2]in increasing order) is now given TensorView’s gauge, activealpha = 0, followed by the usual range normalisation (soalphais 0 or pi), exactly as TensorView reports it; before, the code for this was never reached.When exactly one of the two tensors in
euler_tois axial, its free angle is now fixed as for a single tensor (written in the PAS of the other), which also covers a unique axis along x.All angles, including the relative ones of two axial tensors (Table 1 of the paper), are now reported in TensorView’s range: beta in [0, pi/2), then gamma (active) or alpha (passive) in [0, pi). Table 1’s
(0, 90°, g)is therefore returned as(180°, 90°, 180° - g), and the passive ZYZ angles of two axial tensors as(0, beta, 180°). These are equivalent angle sets; only the numbers change.The meaning of the angles is now defined in the
euler_anglesandeuler_todocstrings and indocs/simpson-conventions.mdin TensorView’s terms.tests/nmr_euler_convention_tests.pychecks the conventions against matrices and angle sets written out from the paper, and against output of TensorView itself (tests/test_data/tensorview_reference.json, made by running TensorView in Octave withtests/reference_generators/tensorview.py).
The exact angle convention is now documented in SIMPSON Tensor Conventions, with a test
suite (tests/nmr_export_orientation_tests.py) that rebuilds each tensor from the written
angles, and checks the formulas it uses against stored SIMPSON and mrsimulator spectra of the
written spin systems, and single-crystal SIMPSON simulations that fix the absolute frame of the SIMPSON angles for a CSA with a dipolar or a J coupling (run in CI with SIMPSON 6.1.4 from the jkshenton/simpson container). Note that mrsimulator’s default octant powder integration is wrong for
spin systems with tensors in general relative orientations; use
sim.config.integration_volume = "hemisphere".
v0.11.1 (2026-02-25)#
Changes#
Removed
bottleneckas a dependency — it was never imported by Soprano.Widened the
clickversion constraint from>=8.2.1to>=8.0, restoring compatibility with environments that ship older click releases (e.g. Pyodide 0.27, which provides click 8.1.7).
v0.11.0 (2026-02-25)#
New features#
Pluggable symmetry backends: spglib and moyopy#
Soprano now supports two interchangeable symmetry backends. spglib is no longer
a hard dependency. The new abstraction in soprano.properties.symmetry.backend
exposes two public functions:
get_symmetry_dataset(atoms, *, symprec=1e-5, backend="auto")→SpacegroupDatasetget_symmetry_ops_from_hall(hall_no, *, backend="auto")→(rotations, translations)
The SpacegroupDataset dataclass provides a unified interface regardless of
which backend performed the analysis:
SpacegroupDataset(
international, # H-M symbol, e.g. "Fm-3m"
hall_number, # integer 1–530
std_lattice, # (3,3) ndarray, rows are lattice vectors
transformation_matrix,
origin_shift,
rotations, # (N,3,3)
translations, # (N,3)
equivalent_atoms, # (nat,) integer orbit labels
)
The backend keyword ("auto" / "moyo" / "spglib") is accepted by:
SymmetryDataset, WyckoffPoints, XRDCalculator, compute_asymmetric_distmat,
and UniqueSites.
moyopy: faster Rust-based symmetry backend#
moyopy is a Rust-based rewrite of spglib approximately 4× faster. Install it with:
pip install soprano[moyo]
spglib moved to optional extras#
pip install soprano[spglib] # spglib only
pip install soprano[moyo] # moyopy only (recommended)
pip install soprano[symmetry] # both (useful for CI / validation)
If neither backend is installed, symmetry functions raise an ImportError with
installation instructions.
New helpers in soprano.optional#
from soprano.optional import has_spglib, has_moyopy, has_symmetry_backend
requireSpglib is still importable but emits a DeprecationWarning.
Bug fixes#
UniqueSites: replaced deprecated ase.spacegroup.get_spacegroup#
UniqueSites previously used ASE’s get_spacegroup, deprecated in ASE 3.24
and removed in ASE 3.25. The function was also incorrect for non-standard
cell settings: it returned symmetry operations for the standard setting of the
detected space group, so tag_sites() could silently produce wrong site
equivalences (see ASE !3455).
The replacement calls the symmetry backend directly on the input structure and
uses equivalent_atoms / orbits from the dataset, which are always computed
from the actual input cell.
scipy 1.15 compatibility: sph_harm removed#
scipy.special.sph_harm was removed in scipy 1.15 and replaced by sph_harm_y,
which also swapped argument order and the θ/φ angle convention. A compatibility
shim is now in place for scipy ≥ 1.15 with a fallback to the original import on
older versions. Fixes TestGenes::test_coordhist and all TestPhylogen tests
on scipy ≥ 1.15.
Notes#
Wyckoff points and transformation-matrix conventions#
_find_wyckoff_points internally prefers spglib’s transformation-matrix
convention when spglib is available. The two backends may return different (but
equally valid) matrices for the same space group due to freedom in the choice of
Euclidean normalizer (see spglib/moyo#198).
Moyo is only used for Wyckoff analysis when spglib is absent.
v0.10.2 (2025-12-09) — NumPy 2.0 bugfix#
Fixed
average_quaternionsfor NumPy 2.0 compatibility (#42)
v0.10.1 (2025-07-28)#
v0.10.0 (2025-07-24) — NumPy 2.0 compatibility and ASE upgrade#
Full NumPy 2.0 compatibility while maintaining backward compatibility with NumPy 1.x. The minimum ASE version is now 3.26.
Dependencies: removed
numpy<2.0restriction; now supportsnumpy>=1.18.5(#39)NumPy 2.0 fixes: replaced deprecated
np.array(copy=False)withnp.asarray()throughoutsoprano/utils.pyandsoprano/calculate/xrd/xrd.pyCollection: fixed ASE calculator import compatibility; optimised
get_array()for NumPy 2.0 copy semanticsNMR tensor averaging: new averaging functionality for tensors and other properties (#34)
Simpson interface: updates to the Simpson NMR simulation interface (#28, @carlosbornes)
New contributor: @carlosbornes (#28).
v0.9.2 (2024-10-03)#
Added
sopranoCLI (#26)
v0.8.14 (2024-07-04)#
For older releases see the GitHub releases page.