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.
Files changed (129) hide show
  1. {torchspin-0.3.0 → torchspin-0.5.0}/CHANGELOG.md +392 -17
  2. {torchspin-0.3.0 → torchspin-0.5.0}/CITATION.cff +11 -4
  3. torchspin-0.5.0/PKG-INFO +280 -0
  4. torchspin-0.5.0/README.md +225 -0
  5. {torchspin-0.3.0 → torchspin-0.5.0}/pyproject.toml +4 -4
  6. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/__init__.py +4 -1
  7. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_linalg.py +99 -20
  8. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/angmom.py +1 -1
  9. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/autograd.py +10 -10
  10. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/chili_sle.py +1 -1
  11. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/convspec.py +1 -1
  12. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/eprload.py +39 -8
  13. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/eprsave.py +1 -1
  14. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/esfit.py +35 -11
  15. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/experiment.py +41 -5
  16. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/fitgui.py +29 -4
  17. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/garlic.py +2 -2
  18. torchspin-0.5.0/torchspin/gridcheck.py +129 -0
  19. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_ezho.py +5 -5
  20. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_zf.py +3 -3
  21. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/hamsymm.py +1 -1
  22. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/initstate.py +1 -1
  23. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/isotopologues.py +2 -1
  24. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/levels.py +1 -1
  25. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/lineshape.py +14 -14
  26. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ml.py +3 -3
  27. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/orca2torchspin.py +8 -8
  28. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ordering.py +1 -1
  29. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/orisel.py +3 -3
  30. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/pepper.py +146 -49
  31. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/pepper_autograd.py +14 -4
  32. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/propint.py +2 -2
  33. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields_batch.py +93 -38
  34. torchspin-0.5.0/torchspin/resfields_hybrid.py +277 -0
  35. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rotutils.py +6 -6
  36. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron.py +54 -16
  37. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron_peaks.py +5 -5
  38. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron_thyme.py +17 -2
  39. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spidyan.py +30 -1
  40. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spinops.py +1 -1
  41. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spinsystem.py +8 -8
  42. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/transmitter.py +1 -1
  43. torchspin-0.5.0/torchspin.egg-info/PKG-INFO +280 -0
  44. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/SOURCES.txt +2 -0
  45. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/requires.txt +4 -0
  46. torchspin-0.3.0/PKG-INFO +0 -267
  47. torchspin-0.3.0/README.md +0 -216
  48. torchspin-0.3.0/torchspin.egg-info/PKG-INFO +0 -267
  49. {torchspin-0.3.0 → torchspin-0.5.0}/LICENSE.md +0 -0
  50. {torchspin-0.3.0 → torchspin-0.5.0}/MANIFEST.in +0 -0
  51. {torchspin-0.3.0 → torchspin-0.5.0}/setup.cfg +0 -0
  52. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_cardamom_istos.py +0 -0
  53. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_cardamom_propagatedm.py +0 -0
  54. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_cardamom_utils.py +0 -0
  55. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/_compile.py +0 -0
  56. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/autoguess.py +0 -0
  57. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/batch.py +0 -0
  58. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/blochsteady.py +0 -0
  59. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/cardamom.py +0 -0
  60. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/chili.py +0 -0
  61. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/constants.py +0 -0
  62. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ctafft.py +0 -0
  63. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/curry.py +0 -0
  64. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/FourierSeriesCoefficients.txt +0 -0
  65. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/GaussianCascadeCoefficients.txt +0 -0
  66. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/isotopedata.txt +0 -0
  67. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/data/spacegroups.txt +0 -0
  68. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/dataproc.py +0 -0
  69. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/dipbackground.py +0 -0
  70. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/dipkernel.py +0 -0
  71. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/diptensor.py +0 -0
  72. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/endorfrq.py +0 -0
  73. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/endorfrq_perturb.py +0 -0
  74. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/evolve.py +0 -0
  75. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ewrls.py +0 -0
  76. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/excitation.py +0 -0
  77. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/exciteprofile.py +0 -0
  78. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/exponfit.py +0 -0
  79. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/fastmotion.py +0 -0
  80. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/fdaxis.py +0 -0
  81. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham.py +0 -0
  82. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_cf.py +0 -0
  83. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_ee.py +0 -0
  84. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_ez.py +0 -0
  85. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_hf.py +0 -0
  86. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_nn.py +0 -0
  87. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_nq.py +0 -0
  88. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_nz.py +0 -0
  89. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_oz.py +0 -0
  90. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/ham_so.py +0 -0
  91. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/levelsplot.py +0 -0
  92. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/lpsvd.py +0 -0
  93. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/makespec.py +0 -0
  94. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mdhmm.py +0 -0
  95. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mdload.py +0 -0
  96. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mdtraj2oripot.py +0 -0
  97. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/mlpsvd.py +0 -0
  98. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/nucdata.py +0 -0
  99. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/nucfrq2d.py +0 -0
  100. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/oripotentialplot.py +0 -0
  101. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/photoselect.py +0 -0
  102. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/plegendre.py +0 -0
  103. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/pulse.py +0 -0
  104. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/py.typed +0 -0
  105. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rapidscan2spc.py +0 -0
  106. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields.py +0 -0
  107. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields_eig.py +0 -0
  108. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfields_perturb.py +0 -0
  109. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfreqs_matrix.py +0 -0
  110. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resfreqs_perturb.py +0 -0
  111. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resonator.py +0 -0
  112. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/resonatorprofile.py +0 -0
  113. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rfmixer.py +0 -0
  114. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/rotations.py +0 -0
  115. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/saffron_pathways.py +0 -0
  116. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/salt.py +0 -0
  117. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/sigeq.py +0 -0
  118. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/signalprocessing.py +0 -0
  119. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/sitetransforms.py +0 -0
  120. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/sphgrid.py +0 -0
  121. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/spinladder.py +0 -0
  122. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stackplot.py +0 -0
  123. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stev.py +0 -0
  124. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stochtraj_diffusion.py +0 -0
  125. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/stochtraj_jump.py +0 -0
  126. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/strainwidth.py +0 -0
  127. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin/utils.py +0 -0
  128. {torchspin-0.3.0 → torchspin-0.5.0}/torchspin.egg-info/dependency_links.txt +0 -0
  129. {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
- optimisation method, not a CPU-acceleration feature: it runs sequentially;
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, normalisation,
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 centres, spread-smoothed widths, bin-integrated Gaussians —
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-discretised with
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
- discretisation).
502
+ discretization).
131
503
  - `Options.AccumMethod='linear'` (EasySpin `makespec`: a line is split between
132
- its two neighbouring bins) is the differentiable form of the default
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 optimised
181
- through the regular simulators; `resfields_batch` Boltzmann normalisation is
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 diagonalisation per resonance candidate at the
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 diagonalisations chunked to ~400 MB; per-orientation
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 normalisation) — the empirical scale factors are gone;
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 normalisation; the jump model
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 neighbours; isotropic systems and
360
- frequency sweeps had wrong normalisation/units; single components ignored
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 summarised in
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
- [Unreleased]: https://github.com/follmerlab/torchspin/compare/v0.1.0...HEAD
522
- [0.1.0]: https://github.com/follmerlab/torchspin/releases/tag/v0.1.0
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.3.0"
6
- date-released: "2026-10-04"
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
- url: "https://github.com/follmerlab/torchspin"
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"