torchspin 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {torchspin-0.3.0 → torchspin-0.5.0}/CHANGELOG.md +392 -17
- {torchspin-0.3.0 → torchspin-0.5.0}/CITATION.cff +11 -4
- torchspin-0.5.0/PKG-INFO +280 -0
- torchspin-0.5.0/README.md +225 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/pyproject.toml +4 -4
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/__init__.py +4 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_linalg.py +99 -20
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/angmom.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/autograd.py +10 -10
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/chili_sle.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/convspec.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/eprload.py +39 -8
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/eprsave.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/esfit.py +35 -11
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/experiment.py +41 -5
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/fitgui.py +29 -4
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/garlic.py +2 -2
- torchspin-0.5.0/torchspin/gridcheck.py +129 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_ezho.py +5 -5
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_zf.py +3 -3
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/hamsymm.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/initstate.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/isotopologues.py +2 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/levels.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/lineshape.py +14 -14
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ml.py +3 -3
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/orca2torchspin.py +8 -8
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ordering.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/orisel.py +3 -3
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/pepper.py +146 -49
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/pepper_autograd.py +14 -4
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/propint.py +2 -2
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields_batch.py +93 -38
- torchspin-0.5.0/torchspin/resfields_hybrid.py +277 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rotutils.py +6 -6
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron.py +54 -16
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron_peaks.py +5 -5
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron_thyme.py +17 -2
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spidyan.py +30 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spinops.py +1 -1
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spinsystem.py +8 -8
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/transmitter.py +1 -1
- torchspin-0.5.0/torchspin.egg-info/PKG-INFO +280 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/SOURCES.txt +2 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/requires.txt +4 -0
- torchspin-0.3.0/PKG-INFO +0 -267
- torchspin-0.3.0/README.md +0 -216
- torchspin-0.3.0/torchspin.egg-info/PKG-INFO +0 -267
- {torchspin-0.3.0 → torchspin-0.5.0}/LICENSE.md +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/MANIFEST.in +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/setup.cfg +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_cardamom_istos.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_cardamom_propagatedm.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_cardamom_utils.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_compile.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/autoguess.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/batch.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/blochsteady.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/cardamom.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/chili.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/constants.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ctafft.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/curry.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/FourierSeriesCoefficients.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/GaussianCascadeCoefficients.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/isotopedata.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/spacegroups.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/dataproc.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/dipbackground.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/dipkernel.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/diptensor.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/endorfrq.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/endorfrq_perturb.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/evolve.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ewrls.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/excitation.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/exciteprofile.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/exponfit.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/fastmotion.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/fdaxis.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_cf.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_ee.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_ez.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_hf.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_nn.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_nq.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_nz.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_oz.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_so.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/levelsplot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/lpsvd.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/makespec.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mdhmm.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mdload.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mdtraj2oripot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mlpsvd.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/nucdata.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/nucfrq2d.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/oripotentialplot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/photoselect.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/plegendre.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/pulse.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/py.typed +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rapidscan2spc.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields_eig.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields_perturb.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfreqs_matrix.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfreqs_perturb.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resonator.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resonatorprofile.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rfmixer.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rotations.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron_pathways.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/salt.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/sigeq.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/signalprocessing.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/sitetransforms.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/sphgrid.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spinladder.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stackplot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stev.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stochtraj_diffusion.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stochtraj_jump.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/strainwidth.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/utils.py +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/dependency_links.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,378 @@ All notable changes to torchspin will be documented in this file.
|
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] — 2026-10-19
|
|
8
|
+
|
|
9
|
+
Found by a flavodoxin HYSCORE feasibility study that compared `saffron`
|
|
10
|
+
against MATLAB EasySpin on its own 13C model and got the modulation wrong
|
|
11
|
+
while the raw cosine stayed at 0.99999. The existing `saffron` suite scored
|
|
12
|
+
agreement with a cosine similarity, which for a pulse-EPR time trace is
|
|
13
|
+
dominated by the DC offset: a HYSCORE trace can have a ~100% wrong modulation
|
|
14
|
+
and still score >0.999. Every defect below lived in that blind spot.
|
|
15
|
+
|
|
16
|
+
New oracle: `tests/data/generate_saffron_phase_refs.m` →
|
|
17
|
+
`torchspin/tests/test_saffron_phase_matlab_validation.py` (25 cases, MATLAB
|
|
18
|
+
R2024b), plus a strict `spidyan` amplitude case. It asserts the **mean-centered** relative L2 error — the modulation
|
|
19
|
+
— as well as the raw error, at tolerances near double-precision roundoff. All
|
|
20
|
+
26 strict cases, 25 of them fail on 0.4.0 and pass here (the 26th, two-nucleus
|
|
21
|
+
Mims ENDOR, is the one case 0.4.0's hardcoded `EndorMethod=1` got right). Agreement across the predefined
|
|
22
|
+
2p/3p/4pESEEM, HYSCORE and Mims-ENDOR routes, direct and frequency-binned,
|
|
23
|
+
powder and explicit single crystal, S=1/2 and S=3/2, with and without
|
|
24
|
+
orientation selection and nuclear quadrupole coupling, is now 1e-16 to 1e-14
|
|
25
|
+
rather than "cosine ≥ 0.98".
|
|
26
|
+
|
|
27
|
+
### Fixed — `saffron` tau phase factor in all four predefined ESEEM/HYSCORE routes
|
|
28
|
+
|
|
29
|
+
`saffron.m` builds the tau-dependent phase factor as
|
|
30
|
+
`Q_ = exp(-2i*pi*Ea*tau)*exp(-2i*pi*Eb*tau)'`, and MATLAB's `'` is a
|
|
31
|
+
**conjugate** transpose, so element *(i,j)* is `exp(-2i*pi*(Ea_i - Eb_j)*tau)`
|
|
32
|
+
— a *difference* of the two manifold energies. torchspin formed the outer
|
|
33
|
+
product without conjugating the second factor, giving their *sum*. The error
|
|
34
|
+
vanishes at `tau = 0`, which is why it survived.
|
|
35
|
+
|
|
36
|
+
Fixed for `exp_id` 1, 2, 3 and 4 (2pESEEM, 3pESEEM, 4pESEEM, HYSCORE); the
|
|
37
|
+
Mims-ENDOR branch already conjugated correctly. On the in-repo
|
|
38
|
+
`saffron_HYSCORE.mat` case (tau = 0.08 µs) the mean-centered error against
|
|
39
|
+
MATLAB drops from **112% to roundoff**, while the raw cosine moves only from
|
|
40
|
+
0.999991 to 1.000000.
|
|
41
|
+
|
|
42
|
+
### Fixed — `saffron` alpha/beta manifold assignment for S=1/2
|
|
43
|
+
|
|
44
|
+
EasySpin's `Transitions = [1 2]` makes `Ea` the **upper** manifold
|
|
45
|
+
(`<S> = +1/2·n`) and `Eb` the lower. The S=1/2 fast path had them swapped,
|
|
46
|
+
while torchspin's own S>1/2 path assigned them correctly — an internal
|
|
47
|
+
inconsistency. Invisible while the nuclear Hamiltonian is linear in **I**
|
|
48
|
+
(the two HYSCORE coherence-transfer pathways then symmetrize the swap away),
|
|
49
|
+
it becomes a 7–33% error as soon as a nuclear quadrupole makes the manifold
|
|
50
|
+
eigenvectors non-trivial.
|
|
51
|
+
|
|
52
|
+
### Fixed — `saffron` dimension order of the 2D frequency-bin accumulation
|
|
53
|
+
|
|
54
|
+
`sf_peaks.c` bins a 2D spectrum at `idx = nu1 + nu2*nPoints1`, i.e. dimension
|
|
55
|
+
1 carries `nu1`. `saffron_peaks.py` computed `idx2 * n_points1 + idx1` into a
|
|
56
|
+
row-major `(n_points1, n_points2)` buffer, which **transposes** the spectrum
|
|
57
|
+
and mis-strides it when the two dimensions differ in length. Fixed at all
|
|
58
|
+
five 2D increment schemes (11–15, 17). The binned and direct paths of
|
|
59
|
+
torchspin itself disagreed by 2% before this; a new test asserts they agree,
|
|
60
|
+
which catches this class of bug without any MATLAB reference.
|
|
61
|
+
|
|
62
|
+
### Fixed — `saffron` `Opt.OriThreshold` reference level under orientation selection
|
|
63
|
+
|
|
64
|
+
EasySpin takes the pruning threshold from the **unweighted**
|
|
65
|
+
`|<i|SyLab|j>|` maximum and only then multiplies in the excitation amplitude,
|
|
66
|
+
so a narrow `Exp.ExciteWidth` discards more transitions. torchspin took the
|
|
67
|
+
maximum after excitation weighting and so kept transitions EasySpin drops,
|
|
68
|
+
giving a 75% modulation error for S=3/2 + 14N at `ExciteWidth = 100` MHz.
|
|
69
|
+
Affects S>1/2 with `Exp.ExciteWidth` only.
|
|
70
|
+
|
|
71
|
+
### Fixed — `saffron` `Opt.Expand` and `Opt.EndorMethod` defaults are conditional in EasySpin
|
|
72
|
+
|
|
73
|
+
Both were hardcoded to a single value, so the default-argument path silently
|
|
74
|
+
ran a different algorithm than EasySpin:
|
|
75
|
+
|
|
76
|
+
- `Opt.Expand` defaults to **4 for 1D and 2 for 2D** sequences
|
|
77
|
+
(`saffron.m:751`); torchspin always used 4, changing the zero-fill of every
|
|
78
|
+
binned HYSCORE.
|
|
79
|
+
- `Opt.EndorMethod` defaults to **0 for a single SHF nucleus or under
|
|
80
|
+
`ProductRule`, else 1** (`saffron.m:729`); torchspin always used 1, which
|
|
81
|
+
selects a different RF-pulse model. For one nucleus this put the ENDOR
|
|
82
|
+
amplitude 40% out and changed the lineshape, at cosine 0.996.
|
|
83
|
+
|
|
84
|
+
Both now accept `None` (the default) to mean EasySpin's rule; an explicit
|
|
85
|
+
integer still overrides it.
|
|
86
|
+
|
|
87
|
+
### Fixed — `saffron`/`saffron_thyme` treated `Exp.SampleFrame` alone as a single crystal
|
|
88
|
+
|
|
89
|
+
EasySpin decides sample type from `CrystalSymmetry` or `MolFrame` only
|
|
90
|
+
(`p_sampletype.m:26`): `Exp.SampleFrame` rotates the sample, but a disordered
|
|
91
|
+
powder stays a powder. `saffron` and `saffron_thyme` also keyed on
|
|
92
|
+
`SampleFrame`, so `SampleFrame` alone silently simulated a **single crystal
|
|
93
|
+
where EasySpin simulates a powder** — a different physical sample, not a
|
|
94
|
+
numerical discrepancy. `pepper` already had this right; `saffron` now matches
|
|
95
|
+
it. Code that relied on `SampleFrame` alone to select a crystal must now pass
|
|
96
|
+
`CrystalSymmetry` (e.g. `'P1'`) or `MolFrame`, as EasySpin requires.
|
|
97
|
+
|
|
98
|
+
### Fixed — `saffron` custom sequences were not divided by their pathway prefactor
|
|
99
|
+
|
|
100
|
+
For a user-defined sequence (`Exp.Sequence` given as pulses and delays, not a
|
|
101
|
+
predefined name) `saffron.m:1669` divides the signal by the largest surviving
|
|
102
|
+
pathway prefactor. torchspin omitted this, leaving a custom HYSCORE-style
|
|
103
|
+
sequence a factor of **8** too small. Shape was unaffected, so the cosine was
|
|
104
|
+
1.000000 and the existing `cosine > 0.95` test passed. Predefined sequences
|
|
105
|
+
are deliberately *not* renormalized this way, and are unchanged.
|
|
106
|
+
|
|
107
|
+
### Fixed — `spidyan` complex detection operators were half amplitude
|
|
108
|
+
|
|
109
|
+
`signalprocessing.m` multiplies a **complex** detection trace (`S+`/`S-`) by
|
|
110
|
+
two when it is demodulated, because the analytic-signal normalization is
|
|
111
|
+
otherwise off by that factor; a purely real or purely imaginary trace (`Sz`) is
|
|
112
|
+
left alone. torchspin applied the demodulation phase but not the factor, so a
|
|
113
|
+
demodulated `<S+>` channel came out at exactly half EasySpin's amplitude — at
|
|
114
|
+
cosine 1.000000, under a `cosine >= 0.99` assertion. The `<Sz>` channel in the
|
|
115
|
+
same simulation was already correct, which is why it went unnoticed.
|
|
116
|
+
|
|
117
|
+
### Audit of the remaining pulse-EPR routes
|
|
118
|
+
|
|
119
|
+
Prompted by the above, the routes that were scored only by cosine were
|
|
120
|
+
re-checked against fresh MATLAB references with strict metrics. Now at
|
|
121
|
+
roundoff and covered by tests: custom user sequences (direct and binned),
|
|
122
|
+
`ProductRule` x `TimeDomain` for two nuclei (all four combinations),
|
|
123
|
+
multi-component input with `Sys.weight`, and S=1 systems.
|
|
124
|
+
|
|
125
|
+
Several apparent discrepancies turned out to be **test-harness** faults rather
|
|
126
|
+
than library faults, and are worth knowing about before trusting an old
|
|
127
|
+
number: `test_3psimple`, `test_4psimple`, `test_hyscore14n`,
|
|
128
|
+
`test_hyscore_ganiso`, `test_mimssimple` and `test_twocomponents` compare
|
|
129
|
+
torchspin's unnormalized output against a MATLAB reference its generator had
|
|
130
|
+
normalized (`y = y/max(abs(y))`), so they sat at a fixed scale factor of 4*pi
|
|
131
|
+
or similar; and `test_saffron_2pESEEM`, `test_saffron_3pESEEM`,
|
|
132
|
+
`test_saffron_HYSCORE_cosine_1d`, `test_S1_*`, `test_S32_HYSCORE`,
|
|
133
|
+
`test_orientation_selective` and `test_customsequence` run with
|
|
134
|
+
`TimeDomain=True` against references generated *without* `Opt.TimeDomain`,
|
|
135
|
+
i.e. they compare direct time evolution against EasySpin's own
|
|
136
|
+
frequency-binned approximation. Both faults are invisible to a cosine.
|
|
137
|
+
Checked against matched references, the library agrees to roundoff in every
|
|
138
|
+
one of these cases.
|
|
139
|
+
|
|
140
|
+
Still open, and recorded in `KNOWN_LIMITATIONS.md`:
|
|
141
|
+
|
|
142
|
+
- `spidyan` multi-spin systems show a small residual that grows linearly
|
|
143
|
+
through free evolution — a phase drift of order 1e-5 relative — for two
|
|
144
|
+
coupled electrons (4.7e-3 overall), an electron-nucleus pair (6.4e-5) and
|
|
145
|
+
S=3/2 (8.8e-6). Single-electron cases are exact to 1e-13. Not diagnosed.
|
|
146
|
+
- `saffron` with real pulses returns per-increment transients
|
|
147
|
+
(`nPoints x nDet x nTime`) where EasySpin returns one detected point per
|
|
148
|
+
increment, and no simple reduction of those transients recovers EasySpin's
|
|
149
|
+
trace — the closest is ~3% off after fitting an arbitrary scale. Treat the
|
|
150
|
+
real-pulse route as unvalidated; `saffron_thyme` remains without MATLAB
|
|
151
|
+
cross-validation.
|
|
152
|
+
- `saffron` accepts a predefined `Exp.Sequence` together with real pulses
|
|
153
|
+
(`Exp.tp`) and silently routes it to the thyme engine; EasySpin rejects that
|
|
154
|
+
combination outright. The output is therefore unvalidated.
|
|
155
|
+
- EasySpin's shaped/chirped pulses (`Type`, `trise`, `Frequency`),
|
|
156
|
+
`Exp.DetWindow`/`DetPhase` and `Opt.SimulationMode` are not implemented in
|
|
157
|
+
`saffron_thyme`, so EasySpin's own thyme test cases cannot be run against it.
|
|
158
|
+
|
|
159
|
+
### Note
|
|
160
|
+
|
|
161
|
+
`saffron` gradients are unaffected in form: a finite-difference check against
|
|
162
|
+
the corrected simulator at nonsymmetric hyperfine values agrees to 1e-10.
|
|
163
|
+
Gradients taken before this release were consistent with the *old*, incorrect
|
|
164
|
+
forward model.
|
|
165
|
+
|
|
166
|
+
## [0.4.0] — 2026-10-09
|
|
167
|
+
|
|
168
|
+
Found by fitting a Cu(II) phthalocyanine spectrum (CuPc in ZnPc: S=1/2,
|
|
169
|
+
A∥(Cu) ≈ 647 MHz, four equivalent 14N) — a system that hit every limit of
|
|
170
|
+
`pepper`'s resonance solvers at once. Every change below is validated against
|
|
171
|
+
EasySpin on MATLAB R2024b through a new 34-case oracle,
|
|
172
|
+
`tests/data/generate_pepper_cupc_refs.m` →
|
|
173
|
+
`torchspin/tests/test_pepper_cupc_matlab_validation.py`, covering all three
|
|
174
|
+
solvers on the same systems plus natural-abundance isotopologues, tilted and
|
|
175
|
+
anisotropic ligand A tensors, nuclear quadrupole coupling, an enlarged exact
|
|
176
|
+
core, and a two-component model.
|
|
177
|
+
|
|
178
|
+
### Added — `Options.Method='hybrid'` (EasySpin `Opt.Method='hybrid'`)
|
|
179
|
+
|
|
180
|
+
Exact diagonalization of a core — all electron spins plus the nuclei named in
|
|
181
|
+
the new `Options.HybridCoreNuclei` (1-based, as in EasySpin) — with the
|
|
182
|
+
remaining nuclei shifting and splitting the core lines. This is the method for
|
|
183
|
+
a large central hyperfine coupling surrounded by small ligand couplings, where
|
|
184
|
+
the exact treatment is unaffordable and perturbation theory is wrong about the
|
|
185
|
+
central coupling. Cu + 4×14N needs a 648-dimensional Hilbert space exactly,
|
|
186
|
+
which neither code can put in a fit loop; as hybrid the core is 8-dimensional
|
|
187
|
+
and each nitrogen is a 3×3 problem, and the spectrum takes **0.27 s against
|
|
188
|
+
EasySpin's 0.28 s**.
|
|
189
|
+
|
|
190
|
+
The implementation follows `resfields.m`: the electron spin operators in the
|
|
191
|
+
hyperfine term are replaced by their expectation values in the two core
|
|
192
|
+
eigenstates at the resonance field, and the resulting nuclear sub-Hamiltonian
|
|
193
|
+
is then diagonalized *exactly*, so the quadrupole and nuclear Zeeman terms and
|
|
194
|
+
the nuclear state mixing are not approximated. Line amplitudes come from the
|
|
195
|
+
Mims overlap matrix |⟨u_r|v_c⟩|², sub-lines below
|
|
196
|
+
`Options.HybridIntThreshold` (0.005, as EasySpin) are dropped, and the nuclei
|
|
197
|
+
are combined by an outer sum of shifts and an outer product of amplitudes.
|
|
198
|
+
Matches EasySpin's hybrid to cosine 0.9998 on every case.
|
|
199
|
+
|
|
200
|
+
Against the exact `matrix` result hybrid reaches cosine 0.9805 (Cu + 1×14N) and
|
|
201
|
+
0.9740 (Cu + 2×14N). **EasySpin's own hybrid gives 0.982 and 0.974 on the same
|
|
202
|
+
comparison** — that gap is the method's first-order decoupling, not a port
|
|
203
|
+
defect. Second-order perturbation theory gives 0.896 and 0.918 on the same
|
|
204
|
+
systems. See § Pepper in `KNOWN_LIMITATIONS.md` for when to use which.
|
|
205
|
+
|
|
206
|
+
### Added — `pepper` supports sets of equivalent nuclei
|
|
207
|
+
|
|
208
|
+
`SpinSystem.n > 1` now works on the `hybrid` and `perturb` paths, which treat
|
|
209
|
+
nuclei one at a time and combine them combinatorially, so a multiplicity is a
|
|
210
|
+
pure saving rather than an obstacle: four equivalent 14N written as `n=[1,4]`
|
|
211
|
+
take 0.30 s instead of 2.0 s and agree with four explicit nitrogens to cosine
|
|
212
|
+
0.99999999. A set of `n` equivalent nuclei shares one sub-Hamiltonian, so its
|
|
213
|
+
copies are combined once by multiset enumeration — for four 14N that is 495
|
|
214
|
+
combinations instead of 9⁴ = 6561.
|
|
215
|
+
|
|
216
|
+
The `matrix` path still rejects `n > 1`, since there the nuclei multiply the
|
|
217
|
+
Hilbert space and cannot be collapsed. EasySpin's `pepper` rejects `n > 1` for
|
|
218
|
+
every method, so this goes beyond it.
|
|
219
|
+
|
|
220
|
+
### Fixed — `Method='matrix'` crashed on highly degenerate systems
|
|
221
|
+
|
|
222
|
+
`pepper(Sys, Exp)` on S=1/2 Cu + 4 equivalent 14N aborted with
|
|
223
|
+
`torch._C._LinAlgError: linalg.eigh: (Batch element 5): The algorithm failed to
|
|
224
|
+
converge … too many repeated eigenvalues (error code: 394590)` after 13 s. One
|
|
225
|
+
matrix out of a batch of 59 complex 648×648 Hermitians: the input is Hermitian
|
|
226
|
+
to 0.0 and finite, but LAPACK's divide-and-conquer driver stalls on the
|
|
227
|
+
hundreds of exactly equal eigenvalue gaps that four identical nuclei produce.
|
|
228
|
+
|
|
229
|
+
`_linalg.eigh` and `_linalg.eigvalsh` had no error handling, so one bad matrix
|
|
230
|
+
killed the whole orientation batch. They now retry a rejected batch matrix by
|
|
231
|
+
matrix — the torch routine alone, then NumPy (a different LAPACK build, and
|
|
232
|
+
`zheevr`/`zheev` rather than divide-and-conquer), then a 1e-14 relative diagonal
|
|
233
|
+
perturbation that breaks the exact ties. Spectra are invariant under unitary
|
|
234
|
+
rotations inside a degenerate subspace, so none of these changes the result
|
|
235
|
+
beyond round-off. On the autograd path the NumPy detour is skipped, because it
|
|
236
|
+
would silently drop gradients.
|
|
237
|
+
|
|
238
|
+
### Fixed — `Options.IsoCutoff` had no effect
|
|
239
|
+
|
|
240
|
+
`expand_components` passed the cutoff as the second positional argument of
|
|
241
|
+
`isotopologues`, which is `n`, so it was silently discarded and pruning always
|
|
242
|
+
used the 1e-4 default. Affected `pepper` and `garlic`. No test caught it
|
|
243
|
+
because every test passed `rel_threshold` by keyword.
|
|
244
|
+
|
|
245
|
+
### Fixed — the perturbation path built a Hamiltonian it never used
|
|
246
|
+
|
|
247
|
+
`pepper` built the full `H0`/`mux`/`muy`/`muz` operators before dispatching on
|
|
248
|
+
`Options.Method`, but `resfields_perturb_batch` works from the spin system and
|
|
249
|
+
never receives them. For Cu + 4×14N that is a 648×648 Kronecker product
|
|
250
|
+
costing about 80 % of the run time, repeated for every isotopologue — which is
|
|
251
|
+
why the cost barely depended on `GridSize`. They are now built on first use.
|
|
252
|
+
Natural-abundance Cu + 4×N at `[91,4]` goes from **2.70 s to 0.41 s**, now
|
|
253
|
+
faster than EasySpin's 0.71 s, with bit-identical output (verified on 14 systems
|
|
254
|
+
spanning both solvers, strain broadening and multi-component input).
|
|
255
|
+
|
|
256
|
+
### Fixed — `Options.Method` accepted perturbation orders `pepper` cannot run
|
|
257
|
+
|
|
258
|
+
`'perturb3'`, `'perturb4'` and `'perturb5'` were accepted and silently run as
|
|
259
|
+
second order, and the requested order was never passed to the solver at all, so
|
|
260
|
+
`'perturb1'` also ran second order. `pepper` now rejects the orders it does not
|
|
261
|
+
implement and forwards the order it does, and the `Options.Method` docstring
|
|
262
|
+
describes the methods per simulator instead of claiming `'perturb'` means fifth
|
|
263
|
+
order everywhere (that is garlic's convention; pepper's `'perturb'` is second
|
|
264
|
+
order, as in EasySpin).
|
|
265
|
+
|
|
266
|
+
### Fixed — two examples did not compute what they claimed
|
|
267
|
+
|
|
268
|
+
`examples/solidstate/copper_nitrogens.py` was titled "Four Equivalent 14N
|
|
269
|
+
Ligands" and labelled its plot "4×14N" while simulating **two**, and had dropped
|
|
270
|
+
the `AFrame` tilts that make the four nitrogens inequivalent in the first place.
|
|
271
|
+
It is now the system from the EasySpin original (Buchanan & Dismukes 1987) and
|
|
272
|
+
shows `perturb` against `hybrid`. `examples/solidstate/matrixperturb.py` ran
|
|
273
|
+
the matrix method twice, labelled one curve "perturb", and carried a comment
|
|
274
|
+
claiming perturbation theory "is not yet exposed as a pepper option in
|
|
275
|
+
torchspin" — untrue since 0.2.x. It now calls `Method='perturb'`.
|
|
276
|
+
|
|
277
|
+
### Fixed — concurrent eigendecompositions could corrupt the thread count
|
|
278
|
+
|
|
279
|
+
`_linalg._pooled` and `pool_map` save, set and restore `torch.set_num_threads`,
|
|
280
|
+
which is process-global. `esfit` evaluates objective functions in a thread
|
|
281
|
+
pool, so two concurrent batches could interleave and leave the process pinned
|
|
282
|
+
at one intra-op thread for the rest of the session. Both now hold a lock
|
|
283
|
+
across the critical section.
|
|
284
|
+
|
|
285
|
+
### Added — `grid_convergence`, to answer whether `Options.GridSize` is enough
|
|
286
|
+
|
|
287
|
+
The default `GridSize=[19,4]` is badly unconverged for a narrow line on a large
|
|
288
|
+
hyperfine coupling — for the reported Cu(II) case the derivative peak is 3 % out
|
|
289
|
+
at `[19,4]` and still moving 4 % between `[91,4]` and `[361,4]`, in EasySpin
|
|
290
|
+
equally — and a single spectrum gives no hint of it.
|
|
291
|
+
|
|
292
|
+
`torchspin.grid_convergence(sys, exp, opt)` simulates again on a `refine`×
|
|
293
|
+
finer coarse grid and reports the cosine and amplitude change, with
|
|
294
|
+
`.converged` against tolerances a tenth of the repository's parity bar, and
|
|
295
|
+
`.report()` giving a sentence that names a grid to use.
|
|
296
|
+
|
|
297
|
+
A heuristic was tried first and deliberately abandoned: comparing the field step
|
|
298
|
+
between knots against the linewidth sounds right, but the SOPHE projection
|
|
299
|
+
integrates analytically over each grid segment, so a large step per knot is
|
|
300
|
+
normal and harmless. That heuristic flagged ~60 simulations in this repository's
|
|
301
|
+
own suite which agree with EasySpin to cosine 0.9999. A check that cries wolf on
|
|
302
|
+
validated-correct cases is worse than none, so the honest version costs a second
|
|
303
|
+
simulation and is opt-in rather than a warning inside `pepper`.
|
|
304
|
+
|
|
305
|
+
### Fixed — `torchspin.fitgui` now says which extra to install
|
|
306
|
+
|
|
307
|
+
`FitPanel(...)` failed with a bare `ModuleNotFoundError: No module named
|
|
308
|
+
'ipywidgets'` and no hint that an optional dependency group provides it. The
|
|
309
|
+
imports now raise an `ImportError` naming the module, what it is needed for, and
|
|
310
|
+
`pip install "torchspin[gui]"`.
|
|
311
|
+
|
|
312
|
+
The `gui` extra was also incomplete: it omitted `ipython`, which
|
|
313
|
+
`FitPanel.show()` needs, and `cloudpickle`, which the panel's own `n_workers`
|
|
314
|
+
control needs. Both are now in it, and `ipywidgets` is in `test` and `dev` so
|
|
315
|
+
the four `test_fitgui.py` tests run instead of skipping.
|
|
316
|
+
|
|
317
|
+
### Fixed — `vary` could put a linewidth or a weight below zero
|
|
318
|
+
|
|
319
|
+
`vary` becomes `p0 ± vary`, so `Vary.lw = 1` around `lw = 0.54`, or
|
|
320
|
+
`Vary.weight = 0.2` around `weight = 0.001` — both taken from a real fit script
|
|
321
|
+
— handed the optimizer a negative lower bound for a quantity that cannot be
|
|
322
|
+
negative. In the dict style the field name is known, so the lower bound is now
|
|
323
|
+
clipped at zero for `lw`, `lwpp`, `lwEndor`, `HStrain`, `gStrain`, `AStrain`,
|
|
324
|
+
`DStrain`, `weight`, `tcorr`, `Diff`, `T1` and `T2`, which is what EasySpin's
|
|
325
|
+
`esfit` does (`nonnegFieldNames`). Explicit `lb`/`ub` are still used exactly as
|
|
326
|
+
given — clipping a bound the user stated would be worse than the problem.
|
|
327
|
+
|
|
328
|
+
A plain parameter vector carries no field names, so neither `esfit` nor
|
|
329
|
+
`FitSession` can clip there, as in EasySpin; both docstrings now say so and
|
|
330
|
+
point at `lb`/`ub`.
|
|
331
|
+
|
|
332
|
+
### Fixed — `eprload` units are now documented where they bite
|
|
333
|
+
|
|
334
|
+
`eprload` returns the abscissa in the file's own units, so a Bruker field axis
|
|
335
|
+
is in **gauss** while the rest of torchspin works in mT. That is deliberate and
|
|
336
|
+
matches EasySpin, and it was recorded in `KNOWN_LIMITATIONS.md` — but the
|
|
337
|
+
`eprload` docstring said only "Abscissa (magnetic field, time, frequency,
|
|
338
|
+
etc.)", which is where a user actually looks before feeding the axis straight
|
|
339
|
+
into `Experiment.Range`. The docstring now states the units per format, shows
|
|
340
|
+
the `/ 10` conversion in a worked example, and notes that the file's own unit
|
|
341
|
+
string survives in `params`.
|
|
342
|
+
|
|
343
|
+
`eprload_info` now prints the unit and, for a gauss axis, that it needs dividing
|
|
344
|
+
by 10 (it also no longer crashes on multi-dimensional data, where `x` is a list
|
|
345
|
+
of axes and `y` can be a list of datasets).
|
|
346
|
+
|
|
347
|
+
### Documented — derivative amplitude at a turning point is grid-limited
|
|
348
|
+
|
|
349
|
+
Investigated because it first looked like a parity defect of the exact solver,
|
|
350
|
+
and turned out not to be one. Comparing Cu + 1×14N by `matrix` against EasySpin
|
|
351
|
+
at `GridSize=[91,4]` gives cosine 0.99948 but amplitude 0.990, and 0.978 at two
|
|
352
|
+
nitrogens — outside the 2 % amplitude convention. Refining the grid removes it:
|
|
353
|
+
0.9903 → 0.9978 → **1.0010** and cosine 0.99948 → 0.99998 → **1.00000** over
|
|
354
|
+
`[91,4]` → `[181,4]` → `[361,4]`.
|
|
355
|
+
|
|
356
|
+
Neither code is converged at `[91,4]`: EasySpin's own derivative peak moves
|
|
357
|
+
53.70 → 51.62 → 51.44 across the same refinement, a 4 % change in its own
|
|
358
|
+
answer. The two are simply unconverged by slightly different amounts. The
|
|
359
|
+
derivative extremum at a turning point is the most grid-sensitive quantity in a
|
|
360
|
+
powder spectrum, much more so than the integrated intensity.
|
|
361
|
+
|
|
362
|
+
Absolute intensity agrees throughout, including with nuclei: integrated
|
|
363
|
+
absorption against EasySpin is 578.38/578.38, 559.77/559.90 and 535.49/534.99
|
|
364
|
+
for 0, 1 and 2 nitrogens (0.02–0.09 %), and absorption peak heights agree to
|
|
365
|
+
0.2–0.8 %. That the integral falls as nuclei are added is real but **EasySpin
|
|
366
|
+
does the same to 0.1 %** — it is the matrix method's transition pre-selection
|
|
367
|
+
dropping weak transitions, and both codes select exactly the same number of
|
|
368
|
+
level pairs (60 and 215). `hybrid` and `perturb` hold at 578 because they put
|
|
369
|
+
all the nuclear intensity on the core lines.
|
|
370
|
+
|
|
371
|
+
The two `cupc_*_2N_matrix` cases are kept at `[91,4]` with the amplitude
|
|
372
|
+
tolerance set to the measured grid error, as the regression anchor for this.
|
|
373
|
+
|
|
374
|
+
### Added — `benchmarks/python/cupc_claims.py`
|
|
375
|
+
|
|
376
|
+
Reproduces the five reported defects and records the measured before/after
|
|
377
|
+
state, so the performance claims have a ledger rather than an anecdote.
|
|
378
|
+
|
|
7
379
|
## [0.3.0] — 2026-10-04
|
|
8
380
|
|
|
9
381
|
First public release.
|
|
@@ -49,7 +421,7 @@ one; none changes any numerical result.
|
|
|
49
421
|
interrupted=False` with SciPy's message; a stop or interrupt returns the
|
|
50
422
|
best point so far with `interrupted=True`; `n_iterations` is the
|
|
51
423
|
Jacobian-evaluation count (one per trust-region iteration). TRF is an
|
|
52
|
-
|
|
424
|
+
optimization method, not a CPU-acceleration feature: it runs sequentially;
|
|
53
425
|
simulation threading and the population-worker `n_workers` parallelism are
|
|
54
426
|
separate. The default method is unchanged (`simplex`). Parity with a direct
|
|
55
427
|
`scipy.optimize.least_squares` call is tested (`tests/test_esfit_trf.py`).
|
|
@@ -71,7 +443,7 @@ one; none changes any numerical result.
|
|
|
71
443
|
`SpinSystem` built from a template, on the autograd graph when the vector
|
|
72
444
|
requires grad), `simulate`/`simulate_batch` (any differentiable simulator,
|
|
73
445
|
`(N, nPoints)` output, optional spawned pool for no-grad generation),
|
|
74
|
-
`SpectrumDataset` (seeded on-the-fly samples with resampling,
|
|
446
|
+
`SpectrumDataset` (seeded on-the-fly samples with resampling, normalization,
|
|
75
447
|
noise and baseline), and torch losses (`cosine_loss`, `rmsd`, `normalize`,
|
|
76
448
|
`resample`) so network predictions can be refined through the simulators.
|
|
77
449
|
- `convspec` accepts tensor widths: differentiable ones are routed to the
|
|
@@ -98,7 +470,7 @@ one; none changes any numerical result.
|
|
|
98
470
|
- `pepper_autograd` / `differentiable_spectrum` handle strains (HStrain,
|
|
99
471
|
gStrain, AStrain, DStrain): the per-transition widths from
|
|
100
472
|
`compute_strain_widths_batch` are already torch, and the EasySpin summation
|
|
101
|
-
branch (facet
|
|
473
|
+
branch (facet centers, spread-smoothed widths, bin-integrated Gaussians —
|
|
102
474
|
`gaussian_bins_t`) is now torch too. Forward equals `pepper()` to 2e-12 on
|
|
103
475
|
nitroxide HStrain+gStrain (D2h), axial gStrain (Dinfh), AStrain and S=1
|
|
104
476
|
DStrain cases; gradients vs finite differences of `pepper()` 1e-6–1e-8 for
|
|
@@ -125,11 +497,11 @@ one; none changes any numerical result.
|
|
|
125
497
|
autograd graph for g, A, Q, tcorr/logtcorr and lw/lwpp. Forward unchanged to
|
|
126
498
|
1e-9 (Fremy's salt, nitroxide fast motion, methyl perturb2, frequency sweep,
|
|
127
499
|
Temperature, Harmonic 2, ModAmp); gradients vs finite differences 1e-8
|
|
128
|
-
(`test_garlic_autograd.py`; the fine accumulation grid is re-
|
|
500
|
+
(`test_garlic_autograd.py`; the fine accumulation grid is re-discretized with
|
|
129
501
|
the smallest width, so tcorr finite differences need a step within one
|
|
130
|
-
|
|
502
|
+
discretization).
|
|
131
503
|
- `Options.AccumMethod='linear'` (EasySpin `makespec`: a line is split between
|
|
132
|
-
its two
|
|
504
|
+
its two neighboring bins) is the differentiable form of the default
|
|
133
505
|
nearest-bin stick spectrum and is selected automatically when the spin
|
|
134
506
|
system carries grad tensors; it differs from nearest-bin binning by the
|
|
135
507
|
sub-bin quantisation only (≈(Δx/2)/σ relative, 1 % for a 0.3 mT Gaussian at
|
|
@@ -177,8 +549,8 @@ one; none changes any numerical result.
|
|
|
177
549
|
pepper 0.9998–1.0000 / 0.999). Training a CNN on their output was training on
|
|
178
550
|
a model that differs from pepper/EasySpin by more than the features to learn.
|
|
179
551
|
- `SpinSystem` keeps tensors that `requires_grad` (no copy) and accepts lists of
|
|
180
|
-
tensors (`g=[g_tensor]`, `A=[[ax, ay, az]]`), so parameters can be
|
|
181
|
-
through the regular simulators; `resfields_batch` Boltzmann
|
|
552
|
+
tensors (`g=[g_tensor]`, `A=[[ax, ay, az]]`), so parameters can be optimized
|
|
553
|
+
through the regular simulators; `resfields_batch` Boltzmann normalization is
|
|
182
554
|
out of place (autograd through `exp`); `hamsymm` detaches before comparing.
|
|
183
555
|
- Not yet differentiable: strains, `ModAmp`, `mwPhase`, ordering,
|
|
184
556
|
photoselection, crystals, frequency sweeps (`pepper_autograd` raises
|
|
@@ -263,11 +635,11 @@ notebooks 01–08, benchmark report against MATLAB/EasySpin. Details below.
|
|
|
263
635
|
### Changed — performance round 1 (2026-09-02)
|
|
264
636
|
Results are unchanged (MATLAB parity suites and the full test suite green after
|
|
265
637
|
every step; resonance positions within 1e-11 mT of the previous search).
|
|
266
|
-
- `resfields_batch`: one exact
|
|
638
|
+
- `resfields_batch`: one exact diagonalization per resonance candidate at the
|
|
267
639
|
cubic-model root (EasySpin's scheme) with a free Hellmann–Feynman Newton
|
|
268
640
|
correction, instead of three polish evaluations plus a final pass; the
|
|
269
641
|
transition eigenvectors are gathered there and reused for intensities;
|
|
270
|
-
knot and candidate
|
|
642
|
+
knot and candidate diagonalizations chunked to ~400 MB; per-orientation
|
|
271
643
|
reassembly and strain widths as tensor ops (one host sync per call).
|
|
272
644
|
- `torchspin/_linalg.py`: batched `eigh`/`eigvalsh` split over a thread pool of
|
|
273
645
|
single-threaded LAPACK calls (torch loops serially over CPU batches; 21–23×
|
|
@@ -294,7 +666,7 @@ every step; resonance positions within 1e-11 mT of the previous search).
|
|
|
294
666
|
Lanczos/direct/eigen solvers, field-sweep methods, post-convolution nuclei,
|
|
295
667
|
multi-component input; 60-case MATLAB suite (59 at cosine ≥0.999).
|
|
296
668
|
- `pepper`: first-principles absolute intensity (dBdE, nuclear-sublevel
|
|
297
|
-
sharing, density
|
|
669
|
+
sharing, density normalization) — the empirical scale factors are gone;
|
|
298
670
|
symmetry-frame fix for tilted axial tensors; level-pair transition
|
|
299
671
|
bookkeeping and EasySpin's strain summation on the interpolated grid;
|
|
300
672
|
Delaunay triangulation, rectified interpolation and projection for the
|
|
@@ -335,7 +707,7 @@ every step; resonance positions within 1e-11 mT of the previous search).
|
|
|
335
707
|
### Changed — EasySpin parity round 3
|
|
336
708
|
- `cardamom`: field-swept spectra no longer apply `Experiment.Harmonic`
|
|
337
709
|
(EasySpin returns the imaginary FFT of the time-weighted FID); the output
|
|
338
|
-
scale follows EasySpin's double orientation
|
|
710
|
+
scale follows EasySpin's double orientation normalization; the jump model
|
|
339
711
|
no longer requires a correlation time.
|
|
340
712
|
- `hamsymm`: principal-value equality at 1e-12 relative (was `torch.allclose`).
|
|
341
713
|
- `FitOptions.lm_delta` default 1e-3 and `lm_gradient_tol` 1e-5 (EasySpin).
|
|
@@ -356,8 +728,8 @@ every step; resonance positions within 1e-11 mT of the previous search).
|
|
|
356
728
|
(sub-increment widths differed by up to 3.7× from the analytic kernel).
|
|
357
729
|
- pepper: resonances were matched across orientations by list index;
|
|
358
730
|
strain widths were evaluated at one field per orientation; slots with
|
|
359
|
-
missing resonances were filled from
|
|
360
|
-
frequency sweeps had wrong
|
|
731
|
+
missing resonances were filled from neighbors; isotropic systems and
|
|
732
|
+
frequency sweeps had wrong normalization/units; single components ignored
|
|
361
733
|
`Sys.weight`.
|
|
362
734
|
|
|
363
735
|
### Added — EasySpin port completion (2026-09-01)
|
|
@@ -457,7 +829,7 @@ every step; resonance positions within 1e-11 mT of the previous search).
|
|
|
457
829
|
- `TestAutoTargetAndGlobal` and `TestSimplexExploration` regression tests
|
|
458
830
|
in `tests/test_esfit.py` (6 new tests total).
|
|
459
831
|
- `benchmarks/PERFORMANCE_SUMMARY.md` and `benchmarks/RUNLOG_2026-04-20.md`
|
|
460
|
-
(development records, not distributed; their results are
|
|
832
|
+
(development records, not distributed; their results are summarized in
|
|
461
833
|
`benchmarks/results/BENCHMARK_REPORT.md` and embedded in
|
|
462
834
|
`benchmarks/analysis/make_manuscript_figures.py`)
|
|
463
835
|
— performance and accuracy summary (MATLAB parity
|
|
@@ -518,5 +890,8 @@ ported to Python/PyTorch with 1820+ passing tests.
|
|
|
518
890
|
- Euler angles: radians, z-y'-z'' passive rotation
|
|
519
891
|
- Default dtype: `torch.complex128`
|
|
520
892
|
|
|
521
|
-
|
|
522
|
-
|
|
893
|
+
<!-- 0.3.0 was the first public release and the history before it was squashed,
|
|
894
|
+
so v0.1.0-v0.2.2 have no tags in this repository to link to. -->
|
|
895
|
+
[0.5.0]: https://github.com/follmerlab/torchspin/compare/v0.4.0...v0.5.0
|
|
896
|
+
[0.4.0]: https://github.com/follmerlab/torchspin/compare/v0.3.0...v0.4.0
|
|
897
|
+
[0.3.0]: https://github.com/follmerlab/torchspin/releases/tag/v0.3.0
|
|
@@ -2,15 +2,22 @@ cff-version: 1.2.0
|
|
|
2
2
|
message: "If you use torchspin in your research, please cite both torchspin and the upstream EasySpin paper."
|
|
3
3
|
title: "torchspin: Differentiable PyTorch port of EasySpin for EPR spectroscopy"
|
|
4
4
|
type: software
|
|
5
|
-
version: "0.
|
|
6
|
-
date-released: "2026-10-
|
|
5
|
+
version: "0.5.0"
|
|
6
|
+
date-released: "2026-10-19"
|
|
7
7
|
license: MIT
|
|
8
8
|
authors:
|
|
9
9
|
- family-names: "Follmer"
|
|
10
10
|
given-names: "Alec"
|
|
11
11
|
email: "ahfollmer@ucdavis.edu"
|
|
12
12
|
repository-code: "https://github.com/follmerlab/torchspin"
|
|
13
|
-
|
|
13
|
+
preferred-citation:
|
|
14
|
+
type: article
|
|
15
|
+
authors:
|
|
16
|
+
- family-names: "Follmer"
|
|
17
|
+
given-names: "A. H."
|
|
18
|
+
title: "TorchSpin: Differentiable Electron Paramagnetic Resonance Simulation in PyTorch"
|
|
19
|
+
journal: "arXiv preprint arXiv:2610.18924"
|
|
20
|
+
year: 2026
|
|
14
21
|
keywords:
|
|
15
22
|
- "EPR"
|
|
16
23
|
- "ESR"
|
|
@@ -18,9 +25,9 @@ keywords:
|
|
|
18
25
|
- "spin-Hamiltonian"
|
|
19
26
|
- "pytorch"
|
|
20
27
|
- "autograd"
|
|
28
|
+
- "differentiable simulation"
|
|
21
29
|
- "magnetic resonance"
|
|
22
30
|
- "EasySpin"
|
|
23
|
-
|
|
24
31
|
references:
|
|
25
32
|
- type: article
|
|
26
33
|
title: "EasySpin, a comprehensive software package for spectral simulation and analysis in EPR"
|