torchspin 0.3.0__tar.gz → 0.4.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.4.0}/CHANGELOG.md +233 -17
  2. {torchspin-0.3.0 → torchspin-0.4.0}/CITATION.cff +2 -2
  3. torchspin-0.4.0/PKG-INFO +279 -0
  4. torchspin-0.4.0/README.md +224 -0
  5. {torchspin-0.3.0 → torchspin-0.4.0}/pyproject.toml +4 -4
  6. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/__init__.py +4 -1
  7. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_linalg.py +99 -20
  8. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/angmom.py +1 -1
  9. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/autograd.py +10 -10
  10. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/chili_sle.py +1 -1
  11. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/convspec.py +1 -1
  12. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/eprload.py +39 -8
  13. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/eprsave.py +1 -1
  14. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/esfit.py +35 -11
  15. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/experiment.py +41 -5
  16. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/fitgui.py +29 -4
  17. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/garlic.py +2 -2
  18. torchspin-0.4.0/torchspin/gridcheck.py +129 -0
  19. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_ezho.py +5 -5
  20. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_zf.py +3 -3
  21. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/hamsymm.py +1 -1
  22. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/initstate.py +1 -1
  23. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/isotopologues.py +2 -1
  24. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/levels.py +1 -1
  25. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/lineshape.py +14 -14
  26. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ml.py +3 -3
  27. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/orca2torchspin.py +8 -8
  28. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ordering.py +1 -1
  29. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/orisel.py +3 -3
  30. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/pepper.py +146 -49
  31. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/pepper_autograd.py +14 -4
  32. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/propint.py +2 -2
  33. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields_batch.py +93 -38
  34. torchspin-0.4.0/torchspin/resfields_hybrid.py +277 -0
  35. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rotutils.py +6 -6
  36. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spinops.py +1 -1
  37. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spinsystem.py +8 -8
  38. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/transmitter.py +1 -1
  39. torchspin-0.4.0/torchspin.egg-info/PKG-INFO +279 -0
  40. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin.egg-info/SOURCES.txt +2 -0
  41. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin.egg-info/requires.txt +4 -0
  42. torchspin-0.3.0/PKG-INFO +0 -267
  43. torchspin-0.3.0/README.md +0 -216
  44. torchspin-0.3.0/torchspin.egg-info/PKG-INFO +0 -267
  45. {torchspin-0.3.0 → torchspin-0.4.0}/LICENSE.md +0 -0
  46. {torchspin-0.3.0 → torchspin-0.4.0}/MANIFEST.in +0 -0
  47. {torchspin-0.3.0 → torchspin-0.4.0}/setup.cfg +0 -0
  48. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_cardamom_istos.py +0 -0
  49. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_cardamom_propagatedm.py +0 -0
  50. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_cardamom_utils.py +0 -0
  51. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_compile.py +0 -0
  52. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/autoguess.py +0 -0
  53. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/batch.py +0 -0
  54. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/blochsteady.py +0 -0
  55. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/cardamom.py +0 -0
  56. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/chili.py +0 -0
  57. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/constants.py +0 -0
  58. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ctafft.py +0 -0
  59. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/curry.py +0 -0
  60. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/FourierSeriesCoefficients.txt +0 -0
  61. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/GaussianCascadeCoefficients.txt +0 -0
  62. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/isotopedata.txt +0 -0
  63. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/spacegroups.txt +0 -0
  64. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/dataproc.py +0 -0
  65. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/dipbackground.py +0 -0
  66. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/dipkernel.py +0 -0
  67. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/diptensor.py +0 -0
  68. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/endorfrq.py +0 -0
  69. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/endorfrq_perturb.py +0 -0
  70. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/evolve.py +0 -0
  71. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ewrls.py +0 -0
  72. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/excitation.py +0 -0
  73. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/exciteprofile.py +0 -0
  74. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/exponfit.py +0 -0
  75. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/fastmotion.py +0 -0
  76. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/fdaxis.py +0 -0
  77. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham.py +0 -0
  78. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_cf.py +0 -0
  79. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_ee.py +0 -0
  80. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_ez.py +0 -0
  81. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_hf.py +0 -0
  82. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_nn.py +0 -0
  83. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_nq.py +0 -0
  84. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_nz.py +0 -0
  85. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_oz.py +0 -0
  86. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_so.py +0 -0
  87. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/levelsplot.py +0 -0
  88. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/lpsvd.py +0 -0
  89. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/makespec.py +0 -0
  90. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mdhmm.py +0 -0
  91. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mdload.py +0 -0
  92. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mdtraj2oripot.py +0 -0
  93. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mlpsvd.py +0 -0
  94. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/nucdata.py +0 -0
  95. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/nucfrq2d.py +0 -0
  96. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/oripotentialplot.py +0 -0
  97. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/photoselect.py +0 -0
  98. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/plegendre.py +0 -0
  99. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/pulse.py +0 -0
  100. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/py.typed +0 -0
  101. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rapidscan2spc.py +0 -0
  102. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields.py +0 -0
  103. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields_eig.py +0 -0
  104. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields_perturb.py +0 -0
  105. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfreqs_matrix.py +0 -0
  106. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfreqs_perturb.py +0 -0
  107. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resonator.py +0 -0
  108. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resonatorprofile.py +0 -0
  109. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rfmixer.py +0 -0
  110. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rotations.py +0 -0
  111. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron.py +0 -0
  112. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron_pathways.py +0 -0
  113. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron_peaks.py +0 -0
  114. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron_thyme.py +0 -0
  115. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/salt.py +0 -0
  116. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/sigeq.py +0 -0
  117. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/signalprocessing.py +0 -0
  118. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/sitetransforms.py +0 -0
  119. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/sphgrid.py +0 -0
  120. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spidyan.py +0 -0
  121. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spinladder.py +0 -0
  122. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stackplot.py +0 -0
  123. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stev.py +0 -0
  124. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stochtraj_diffusion.py +0 -0
  125. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stochtraj_jump.py +0 -0
  126. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/strainwidth.py +0 -0
  127. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/utils.py +0 -0
  128. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin.egg-info/dependency_links.txt +0 -0
  129. {torchspin-0.3.0 → torchspin-0.4.0}/torchspin.egg-info/top_level.txt +0 -0
@@ -4,6 +4,219 @@ 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.4.0] — 2026-10-09
8
+
9
+ Found by fitting a Cu(II) phthalocyanine spectrum (CuPc in ZnPc: S=1/2,
10
+ A∥(Cu) ≈ 647 MHz, four equivalent 14N) — a system that hit every limit of
11
+ `pepper`'s resonance solvers at once. Every change below is validated against
12
+ EasySpin on MATLAB R2024b through a new 34-case oracle,
13
+ `tests/data/generate_pepper_cupc_refs.m` →
14
+ `torchspin/tests/test_pepper_cupc_matlab_validation.py`, covering all three
15
+ solvers on the same systems plus natural-abundance isotopologues, tilted and
16
+ anisotropic ligand A tensors, nuclear quadrupole coupling, an enlarged exact
17
+ core, and a two-component model.
18
+
19
+ ### Added — `Options.Method='hybrid'` (EasySpin `Opt.Method='hybrid'`)
20
+
21
+ Exact diagonalization of a core — all electron spins plus the nuclei named in
22
+ the new `Options.HybridCoreNuclei` (1-based, as in EasySpin) — with the
23
+ remaining nuclei shifting and splitting the core lines. This is the method for
24
+ a large central hyperfine coupling surrounded by small ligand couplings, where
25
+ the exact treatment is unaffordable and perturbation theory is wrong about the
26
+ central coupling. Cu + 4×14N needs a 648-dimensional Hilbert space exactly,
27
+ which neither code can put in a fit loop; as hybrid the core is 8-dimensional
28
+ and each nitrogen is a 3×3 problem, and the spectrum takes **0.27 s against
29
+ EasySpin's 0.28 s**.
30
+
31
+ The implementation follows `resfields.m`: the electron spin operators in the
32
+ hyperfine term are replaced by their expectation values in the two core
33
+ eigenstates at the resonance field, and the resulting nuclear sub-Hamiltonian
34
+ is then diagonalized *exactly*, so the quadrupole and nuclear Zeeman terms and
35
+ the nuclear state mixing are not approximated. Line amplitudes come from the
36
+ Mims overlap matrix |⟨u_r|v_c⟩|², sub-lines below
37
+ `Options.HybridIntThreshold` (0.005, as EasySpin) are dropped, and the nuclei
38
+ are combined by an outer sum of shifts and an outer product of amplitudes.
39
+ Matches EasySpin's hybrid to cosine 0.9998 on every case.
40
+
41
+ Against the exact `matrix` result hybrid reaches cosine 0.9805 (Cu + 1×14N) and
42
+ 0.9740 (Cu + 2×14N). **EasySpin's own hybrid gives 0.982 and 0.974 on the same
43
+ comparison** — that gap is the method's first-order decoupling, not a port
44
+ defect. Second-order perturbation theory gives 0.896 and 0.918 on the same
45
+ systems. See § Pepper in `KNOWN_LIMITATIONS.md` for when to use which.
46
+
47
+ ### Added — `pepper` supports sets of equivalent nuclei
48
+
49
+ `SpinSystem.n > 1` now works on the `hybrid` and `perturb` paths, which treat
50
+ nuclei one at a time and combine them combinatorially, so a multiplicity is a
51
+ pure saving rather than an obstacle: four equivalent 14N written as `n=[1,4]`
52
+ take 0.30 s instead of 2.0 s and agree with four explicit nitrogens to cosine
53
+ 0.99999999. A set of `n` equivalent nuclei shares one sub-Hamiltonian, so its
54
+ copies are combined once by multiset enumeration — for four 14N that is 495
55
+ combinations instead of 9⁴ = 6561.
56
+
57
+ The `matrix` path still rejects `n > 1`, since there the nuclei multiply the
58
+ Hilbert space and cannot be collapsed. EasySpin's `pepper` rejects `n > 1` for
59
+ every method, so this goes beyond it.
60
+
61
+ ### Fixed — `Method='matrix'` crashed on highly degenerate systems
62
+
63
+ `pepper(Sys, Exp)` on S=1/2 Cu + 4 equivalent 14N aborted with
64
+ `torch._C._LinAlgError: linalg.eigh: (Batch element 5): The algorithm failed to
65
+ converge … too many repeated eigenvalues (error code: 394590)` after 13 s. One
66
+ matrix out of a batch of 59 complex 648×648 Hermitians: the input is Hermitian
67
+ to 0.0 and finite, but LAPACK's divide-and-conquer driver stalls on the
68
+ hundreds of exactly equal eigenvalue gaps that four identical nuclei produce.
69
+
70
+ `_linalg.eigh` and `_linalg.eigvalsh` had no error handling, so one bad matrix
71
+ killed the whole orientation batch. They now retry a rejected batch matrix by
72
+ matrix — the torch routine alone, then NumPy (a different LAPACK build, and
73
+ `zheevr`/`zheev` rather than divide-and-conquer), then a 1e-14 relative diagonal
74
+ perturbation that breaks the exact ties. Spectra are invariant under unitary
75
+ rotations inside a degenerate subspace, so none of these changes the result
76
+ beyond round-off. On the autograd path the NumPy detour is skipped, because it
77
+ would silently drop gradients.
78
+
79
+ ### Fixed — `Options.IsoCutoff` had no effect
80
+
81
+ `expand_components` passed the cutoff as the second positional argument of
82
+ `isotopologues`, which is `n`, so it was silently discarded and pruning always
83
+ used the 1e-4 default. Affected `pepper` and `garlic`. No test caught it
84
+ because every test passed `rel_threshold` by keyword.
85
+
86
+ ### Fixed — the perturbation path built a Hamiltonian it never used
87
+
88
+ `pepper` built the full `H0`/`mux`/`muy`/`muz` operators before dispatching on
89
+ `Options.Method`, but `resfields_perturb_batch` works from the spin system and
90
+ never receives them. For Cu + 4×14N that is a 648×648 Kronecker product
91
+ costing about 80 % of the run time, repeated for every isotopologue — which is
92
+ why the cost barely depended on `GridSize`. They are now built on first use.
93
+ Natural-abundance Cu + 4×N at `[91,4]` goes from **2.70 s to 0.41 s**, now
94
+ faster than EasySpin's 0.71 s, with bit-identical output (verified on 14 systems
95
+ spanning both solvers, strain broadening and multi-component input).
96
+
97
+ ### Fixed — `Options.Method` accepted perturbation orders `pepper` cannot run
98
+
99
+ `'perturb3'`, `'perturb4'` and `'perturb5'` were accepted and silently run as
100
+ second order, and the requested order was never passed to the solver at all, so
101
+ `'perturb1'` also ran second order. `pepper` now rejects the orders it does not
102
+ implement and forwards the order it does, and the `Options.Method` docstring
103
+ describes the methods per simulator instead of claiming `'perturb'` means fifth
104
+ order everywhere (that is garlic's convention; pepper's `'perturb'` is second
105
+ order, as in EasySpin).
106
+
107
+ ### Fixed — two examples did not compute what they claimed
108
+
109
+ `examples/solidstate/copper_nitrogens.py` was titled "Four Equivalent 14N
110
+ Ligands" and labelled its plot "4×14N" while simulating **two**, and had dropped
111
+ the `AFrame` tilts that make the four nitrogens inequivalent in the first place.
112
+ It is now the system from the EasySpin original (Buchanan & Dismukes 1987) and
113
+ shows `perturb` against `hybrid`. `examples/solidstate/matrixperturb.py` ran
114
+ the matrix method twice, labelled one curve "perturb", and carried a comment
115
+ claiming perturbation theory "is not yet exposed as a pepper option in
116
+ torchspin" — untrue since 0.2.x. It now calls `Method='perturb'`.
117
+
118
+ ### Fixed — concurrent eigendecompositions could corrupt the thread count
119
+
120
+ `_linalg._pooled` and `pool_map` save, set and restore `torch.set_num_threads`,
121
+ which is process-global. `esfit` evaluates objective functions in a thread
122
+ pool, so two concurrent batches could interleave and leave the process pinned
123
+ at one intra-op thread for the rest of the session. Both now hold a lock
124
+ across the critical section.
125
+
126
+ ### Added — `grid_convergence`, to answer whether `Options.GridSize` is enough
127
+
128
+ The default `GridSize=[19,4]` is badly unconverged for a narrow line on a large
129
+ hyperfine coupling — for the reported Cu(II) case the derivative peak is 3 % out
130
+ at `[19,4]` and still moving 4 % between `[91,4]` and `[361,4]`, in EasySpin
131
+ equally — and a single spectrum gives no hint of it.
132
+
133
+ `torchspin.grid_convergence(sys, exp, opt)` simulates again on a `refine`×
134
+ finer coarse grid and reports the cosine and amplitude change, with
135
+ `.converged` against tolerances a tenth of the repository's parity bar, and
136
+ `.report()` giving a sentence that names a grid to use.
137
+
138
+ A heuristic was tried first and deliberately abandoned: comparing the field step
139
+ between knots against the linewidth sounds right, but the SOPHE projection
140
+ integrates analytically over each grid segment, so a large step per knot is
141
+ normal and harmless. That heuristic flagged ~60 simulations in this repository's
142
+ own suite which agree with EasySpin to cosine 0.9999. A check that cries wolf on
143
+ validated-correct cases is worse than none, so the honest version costs a second
144
+ simulation and is opt-in rather than a warning inside `pepper`.
145
+
146
+ ### Fixed — `torchspin.fitgui` now says which extra to install
147
+
148
+ `FitPanel(...)` failed with a bare `ModuleNotFoundError: No module named
149
+ 'ipywidgets'` and no hint that an optional dependency group provides it. The
150
+ imports now raise an `ImportError` naming the module, what it is needed for, and
151
+ `pip install "torchspin[gui]"`.
152
+
153
+ The `gui` extra was also incomplete: it omitted `ipython`, which
154
+ `FitPanel.show()` needs, and `cloudpickle`, which the panel's own `n_workers`
155
+ control needs. Both are now in it, and `ipywidgets` is in `test` and `dev` so
156
+ the four `test_fitgui.py` tests run instead of skipping.
157
+
158
+ ### Fixed — `vary` could put a linewidth or a weight below zero
159
+
160
+ `vary` becomes `p0 ± vary`, so `Vary.lw = 1` around `lw = 0.54`, or
161
+ `Vary.weight = 0.2` around `weight = 0.001` — both taken from a real fit script
162
+ — handed the optimizer a negative lower bound for a quantity that cannot be
163
+ negative. In the dict style the field name is known, so the lower bound is now
164
+ clipped at zero for `lw`, `lwpp`, `lwEndor`, `HStrain`, `gStrain`, `AStrain`,
165
+ `DStrain`, `weight`, `tcorr`, `Diff`, `T1` and `T2`, which is what EasySpin's
166
+ `esfit` does (`nonnegFieldNames`). Explicit `lb`/`ub` are still used exactly as
167
+ given — clipping a bound the user stated would be worse than the problem.
168
+
169
+ A plain parameter vector carries no field names, so neither `esfit` nor
170
+ `FitSession` can clip there, as in EasySpin; both docstrings now say so and
171
+ point at `lb`/`ub`.
172
+
173
+ ### Fixed — `eprload` units are now documented where they bite
174
+
175
+ `eprload` returns the abscissa in the file's own units, so a Bruker field axis
176
+ is in **gauss** while the rest of torchspin works in mT. That is deliberate and
177
+ matches EasySpin, and it was recorded in `KNOWN_LIMITATIONS.md` — but the
178
+ `eprload` docstring said only "Abscissa (magnetic field, time, frequency,
179
+ etc.)", which is where a user actually looks before feeding the axis straight
180
+ into `Experiment.Range`. The docstring now states the units per format, shows
181
+ the `/ 10` conversion in a worked example, and notes that the file's own unit
182
+ string survives in `params`.
183
+
184
+ `eprload_info` now prints the unit and, for a gauss axis, that it needs dividing
185
+ by 10 (it also no longer crashes on multi-dimensional data, where `x` is a list
186
+ of axes and `y` can be a list of datasets).
187
+
188
+ ### Documented — derivative amplitude at a turning point is grid-limited
189
+
190
+ Investigated because it first looked like a parity defect of the exact solver,
191
+ and turned out not to be one. Comparing Cu + 1×14N by `matrix` against EasySpin
192
+ at `GridSize=[91,4]` gives cosine 0.99948 but amplitude 0.990, and 0.978 at two
193
+ nitrogens — outside the 2 % amplitude convention. Refining the grid removes it:
194
+ 0.9903 → 0.9978 → **1.0010** and cosine 0.99948 → 0.99998 → **1.00000** over
195
+ `[91,4]` → `[181,4]` → `[361,4]`.
196
+
197
+ Neither code is converged at `[91,4]`: EasySpin's own derivative peak moves
198
+ 53.70 → 51.62 → 51.44 across the same refinement, a 4 % change in its own
199
+ answer. The two are simply unconverged by slightly different amounts. The
200
+ derivative extremum at a turning point is the most grid-sensitive quantity in a
201
+ powder spectrum, much more so than the integrated intensity.
202
+
203
+ Absolute intensity agrees throughout, including with nuclei: integrated
204
+ absorption against EasySpin is 578.38/578.38, 559.77/559.90 and 535.49/534.99
205
+ for 0, 1 and 2 nitrogens (0.02–0.09 %), and absorption peak heights agree to
206
+ 0.2–0.8 %. That the integral falls as nuclei are added is real but **EasySpin
207
+ does the same to 0.1 %** — it is the matrix method's transition pre-selection
208
+ dropping weak transitions, and both codes select exactly the same number of
209
+ level pairs (60 and 215). `hybrid` and `perturb` hold at 578 because they put
210
+ all the nuclear intensity on the core lines.
211
+
212
+ The two `cupc_*_2N_matrix` cases are kept at `[91,4]` with the amplitude
213
+ tolerance set to the measured grid error, as the regression anchor for this.
214
+
215
+ ### Added — `benchmarks/python/cupc_claims.py`
216
+
217
+ Reproduces the five reported defects and records the measured before/after
218
+ state, so the performance claims have a ledger rather than an anecdote.
219
+
7
220
  ## [0.3.0] — 2026-10-04
8
221
 
9
222
  First public release.
@@ -49,7 +262,7 @@ one; none changes any numerical result.
49
262
  interrupted=False` with SciPy's message; a stop or interrupt returns the
50
263
  best point so far with `interrupted=True`; `n_iterations` is the
51
264
  Jacobian-evaluation count (one per trust-region iteration). TRF is an
52
- optimisation method, not a CPU-acceleration feature: it runs sequentially;
265
+ optimization method, not a CPU-acceleration feature: it runs sequentially;
53
266
  simulation threading and the population-worker `n_workers` parallelism are
54
267
  separate. The default method is unchanged (`simplex`). Parity with a direct
55
268
  `scipy.optimize.least_squares` call is tested (`tests/test_esfit_trf.py`).
@@ -71,7 +284,7 @@ one; none changes any numerical result.
71
284
  `SpinSystem` built from a template, on the autograd graph when the vector
72
285
  requires grad), `simulate`/`simulate_batch` (any differentiable simulator,
73
286
  `(N, nPoints)` output, optional spawned pool for no-grad generation),
74
- `SpectrumDataset` (seeded on-the-fly samples with resampling, normalisation,
287
+ `SpectrumDataset` (seeded on-the-fly samples with resampling, normalization,
75
288
  noise and baseline), and torch losses (`cosine_loss`, `rmsd`, `normalize`,
76
289
  `resample`) so network predictions can be refined through the simulators.
77
290
  - `convspec` accepts tensor widths: differentiable ones are routed to the
@@ -98,7 +311,7 @@ one; none changes any numerical result.
98
311
  - `pepper_autograd` / `differentiable_spectrum` handle strains (HStrain,
99
312
  gStrain, AStrain, DStrain): the per-transition widths from
100
313
  `compute_strain_widths_batch` are already torch, and the EasySpin summation
101
- branch (facet centres, spread-smoothed widths, bin-integrated Gaussians —
314
+ branch (facet centers, spread-smoothed widths, bin-integrated Gaussians —
102
315
  `gaussian_bins_t`) is now torch too. Forward equals `pepper()` to 2e-12 on
103
316
  nitroxide HStrain+gStrain (D2h), axial gStrain (Dinfh), AStrain and S=1
104
317
  DStrain cases; gradients vs finite differences of `pepper()` 1e-6–1e-8 for
@@ -125,11 +338,11 @@ one; none changes any numerical result.
125
338
  autograd graph for g, A, Q, tcorr/logtcorr and lw/lwpp. Forward unchanged to
126
339
  1e-9 (Fremy's salt, nitroxide fast motion, methyl perturb2, frequency sweep,
127
340
  Temperature, Harmonic 2, ModAmp); gradients vs finite differences 1e-8
128
- (`test_garlic_autograd.py`; the fine accumulation grid is re-discretised with
341
+ (`test_garlic_autograd.py`; the fine accumulation grid is re-discretized with
129
342
  the smallest width, so tcorr finite differences need a step within one
130
- discretisation).
343
+ discretization).
131
344
  - `Options.AccumMethod='linear'` (EasySpin `makespec`: a line is split between
132
- its two neighbouring bins) is the differentiable form of the default
345
+ its two neighboring bins) is the differentiable form of the default
133
346
  nearest-bin stick spectrum and is selected automatically when the spin
134
347
  system carries grad tensors; it differs from nearest-bin binning by the
135
348
  sub-bin quantisation only (≈(Δx/2)/σ relative, 1 % for a 0.3 mT Gaussian at
@@ -177,8 +390,8 @@ one; none changes any numerical result.
177
390
  pepper 0.9998–1.0000 / 0.999). Training a CNN on their output was training on
178
391
  a model that differs from pepper/EasySpin by more than the features to learn.
179
392
  - `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
393
+ tensors (`g=[g_tensor]`, `A=[[ax, ay, az]]`), so parameters can be optimized
394
+ through the regular simulators; `resfields_batch` Boltzmann normalization is
182
395
  out of place (autograd through `exp`); `hamsymm` detaches before comparing.
183
396
  - Not yet differentiable: strains, `ModAmp`, `mwPhase`, ordering,
184
397
  photoselection, crystals, frequency sweeps (`pepper_autograd` raises
@@ -263,11 +476,11 @@ notebooks 01–08, benchmark report against MATLAB/EasySpin. Details below.
263
476
  ### Changed — performance round 1 (2026-09-02)
264
477
  Results are unchanged (MATLAB parity suites and the full test suite green after
265
478
  every step; resonance positions within 1e-11 mT of the previous search).
266
- - `resfields_batch`: one exact diagonalisation per resonance candidate at the
479
+ - `resfields_batch`: one exact diagonalization per resonance candidate at the
267
480
  cubic-model root (EasySpin's scheme) with a free Hellmann–Feynman Newton
268
481
  correction, instead of three polish evaluations plus a final pass; the
269
482
  transition eigenvectors are gathered there and reused for intensities;
270
- knot and candidate diagonalisations chunked to ~400 MB; per-orientation
483
+ knot and candidate diagonalizations chunked to ~400 MB; per-orientation
271
484
  reassembly and strain widths as tensor ops (one host sync per call).
272
485
  - `torchspin/_linalg.py`: batched `eigh`/`eigvalsh` split over a thread pool of
273
486
  single-threaded LAPACK calls (torch loops serially over CPU batches; 21–23×
@@ -294,7 +507,7 @@ every step; resonance positions within 1e-11 mT of the previous search).
294
507
  Lanczos/direct/eigen solvers, field-sweep methods, post-convolution nuclei,
295
508
  multi-component input; 60-case MATLAB suite (59 at cosine ≥0.999).
296
509
  - `pepper`: first-principles absolute intensity (dBdE, nuclear-sublevel
297
- sharing, density normalisation) — the empirical scale factors are gone;
510
+ sharing, density normalization) — the empirical scale factors are gone;
298
511
  symmetry-frame fix for tilted axial tensors; level-pair transition
299
512
  bookkeeping and EasySpin's strain summation on the interpolated grid;
300
513
  Delaunay triangulation, rectified interpolation and projection for the
@@ -335,7 +548,7 @@ every step; resonance positions within 1e-11 mT of the previous search).
335
548
  ### Changed — EasySpin parity round 3
336
549
  - `cardamom`: field-swept spectra no longer apply `Experiment.Harmonic`
337
550
  (EasySpin returns the imaginary FFT of the time-weighted FID); the output
338
- scale follows EasySpin's double orientation normalisation; the jump model
551
+ scale follows EasySpin's double orientation normalization; the jump model
339
552
  no longer requires a correlation time.
340
553
  - `hamsymm`: principal-value equality at 1e-12 relative (was `torch.allclose`).
341
554
  - `FitOptions.lm_delta` default 1e-3 and `lm_gradient_tol` 1e-5 (EasySpin).
@@ -356,8 +569,8 @@ every step; resonance positions within 1e-11 mT of the previous search).
356
569
  (sub-increment widths differed by up to 3.7× from the analytic kernel).
357
570
  - pepper: resonances were matched across orientations by list index;
358
571
  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
572
+ missing resonances were filled from neighbors; isotropic systems and
573
+ frequency sweeps had wrong normalization/units; single components ignored
361
574
  `Sys.weight`.
362
575
 
363
576
  ### Added — EasySpin port completion (2026-09-01)
@@ -457,7 +670,7 @@ every step; resonance positions within 1e-11 mT of the previous search).
457
670
  - `TestAutoTargetAndGlobal` and `TestSimplexExploration` regression tests
458
671
  in `tests/test_esfit.py` (6 new tests total).
459
672
  - `benchmarks/PERFORMANCE_SUMMARY.md` and `benchmarks/RUNLOG_2026-04-20.md`
460
- (development records, not distributed; their results are summarised in
673
+ (development records, not distributed; their results are summarized in
461
674
  `benchmarks/results/BENCHMARK_REPORT.md` and embedded in
462
675
  `benchmarks/analysis/make_manuscript_figures.py`)
463
676
  — performance and accuracy summary (MATLAB parity
@@ -518,5 +731,8 @@ ported to Python/PyTorch with 1820+ passing tests.
518
731
  - Euler angles: radians, z-y'-z'' passive rotation
519
732
  - Default dtype: `torch.complex128`
520
733
 
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
734
+ <!-- 0.3.0 was the first public release and the history before it was squashed,
735
+ so v0.1.0-v0.2.2 have no tags in this repository to link to. -->
736
+ [Unreleased]: https://github.com/follmerlab/torchspin/compare/v0.4.0...HEAD
737
+ [0.4.0]: https://github.com/follmerlab/torchspin/compare/v0.3.0...v0.4.0
738
+ [0.3.0]: https://github.com/follmerlab/torchspin/releases/tag/v0.3.0
@@ -2,8 +2,8 @@ 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.4.0"
6
+ date-released: "2026-10-09"
7
7
  license: MIT
8
8
  authors:
9
9
  - family-names: "Follmer"
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: torchspin
3
+ Version: 0.4.0
4
+ Summary: Differentiable PyTorch port of EasySpin - EPR/ESR spin Hamiltonian and spectrum simulation
5
+ Author-email: Alec Follmer <ahfollmer@ucdavis.edu>
6
+ Maintainer-email: Alec Follmer <ahfollmer@ucdavis.edu>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/follmerlab/torchspin
9
+ Project-URL: Source, https://github.com/follmerlab/torchspin
10
+ Project-URL: Issues, https://github.com/follmerlab/torchspin/issues
11
+ Project-URL: Upstream (EasySpin), https://easyspin.org
12
+ Keywords: EPR,ESR,spectroscopy,spin-Hamiltonian,pytorch,autograd,magnetic-resonance,quantum,EasySpin,simulation
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
23
+ Classifier: Topic :: Scientific/Engineering :: Physics
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE.md
27
+ Requires-Dist: torch<3,>=2.0
28
+ Requires-Dist: numpy<3,>=1.23
29
+ Requires-Dist: scipy>=1.10
30
+ Provides-Extra: plot
31
+ Requires-Dist: matplotlib>=3.6; extra == "plot"
32
+ Provides-Extra: fit
33
+ Requires-Dist: cloudpickle>=2.0; extra == "fit"
34
+ Provides-Extra: gui
35
+ Requires-Dist: ipywidgets>=7.6; extra == "gui"
36
+ Requires-Dist: matplotlib>=3.6; extra == "gui"
37
+ Requires-Dist: ipympl>=0.9; extra == "gui"
38
+ Requires-Dist: ipython>=7; extra == "gui"
39
+ Requires-Dist: cloudpickle>=2.0; extra == "gui"
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest>=7; extra == "test"
42
+ Requires-Dist: pytest-cov; extra == "test"
43
+ Requires-Dist: matplotlib>=3.6; extra == "test"
44
+ Requires-Dist: cloudpickle>=2.0; extra == "test"
45
+ Requires-Dist: ipywidgets>=7.6; extra == "test"
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7; extra == "dev"
48
+ Requires-Dist: pytest-cov; extra == "dev"
49
+ Requires-Dist: build; extra == "dev"
50
+ Requires-Dist: twine; extra == "dev"
51
+ Requires-Dist: matplotlib>=3.6; extra == "dev"
52
+ Requires-Dist: cloudpickle>=2.0; extra == "dev"
53
+ Requires-Dist: ipywidgets>=7.6; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # torchspin
57
+
58
+ **Simulate and fit EPR/ESR spectra in Python — differentiable, GPU-capable, and validated against [EasySpin](https://easyspin.org).**
59
+
60
+ [![PyPI](https://img.shields.io/pypi/v/torchspin)](https://pypi.org/project/torchspin/)
61
+ [![Python](https://img.shields.io/pypi/pyversions/torchspin)](https://pypi.org/project/torchspin/)
62
+ [![Tests](https://github.com/follmerlab/torchspin/actions/workflows/python-tests.yml/badge.svg)](https://github.com/follmerlab/torchspin/actions/workflows/python-tests.yml)
63
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
64
+
65
+ torchspin reimplements the core physics engine of EasySpin (MATLAB) in
66
+ Python/PyTorch. You get the simulators you already know — `pepper`, `garlic`,
67
+ `chili`, `salt`, `saffron`, `curry`, `cardamom`, `esfit` — with the same units
68
+ and conventions, plus three things MATLAB cannot give you:
69
+
70
+ - **Gradients.** Spectra are differentiable end to end, so you can fit with
71
+ gradient descent, propagate uncertainty, or put a simulator inside a neural
72
+ network.
73
+ - **A Python workflow.** NumPy and torch tensors in and out, no license server,
74
+ installable with `pip`.
75
+ - **Optional GPU.** `Options(device='cuda')` for batched and gradient-based
76
+ work.
77
+
78
+ torchspin is a reimplementation, not a drop-in replacement for every EasySpin
79
+ feature. Before relying on it for published results, read
80
+ [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) — it states per simulator what is
81
+ validated against MATLAB and what is not.
82
+
83
+ ---
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ pip install torchspin # core
89
+ pip install "torchspin[plot]" # + matplotlib for the plotting helpers
90
+ pip install "torchspin[gui]" # + the interactive fitting panel (torchspin.fitgui)
91
+ ```
92
+
93
+ CPU-only PyTorch, to avoid the large CUDA wheels:
94
+
95
+ ```bash
96
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
97
+ pip install torchspin
98
+ ```
99
+
100
+ Requires Python 3.10+, PyTorch 2.0+, NumPy 1.23+, SciPy 1.10+.
101
+
102
+ ---
103
+
104
+ ## Quick start
105
+
106
+ ### CW powder spectrum (`pepper`)
107
+
108
+ ```python
109
+ from torchspin import SpinSystem, Experiment, Options, pepper
110
+
111
+ sys = SpinSystem(
112
+ S=[0.5],
113
+ g=[[2.009, 2.006, 2.002]],
114
+ Nucs=['14N'],
115
+ A=[[10.0, 10.0, 95.0]], # MHz
116
+ lw=[1.0, 0.0], # Gaussian FWHM (mT)
117
+ )
118
+ exp = Experiment(mwFreq=9.5, Range=[330, 350], nPoints=1024, Harmonic=1)
119
+ B, spec = pepper(sys, exp, Options(GridSize=50))
120
+ ```
121
+
122
+ ### Solution EPR (`garlic`)
123
+
124
+ ```python
125
+ from torchspin import SpinSystem, Experiment, garlic
126
+
127
+ sys = SpinSystem(S=[0.5], g=2.003, Nucs='1H', A=10.0, lw=[0.2, 0.0])
128
+ exp = Experiment(mwFreq=9.5, Range=[336, 342], nPoints=512, Harmonic=1)
129
+ B, spec = garlic(sys, exp)
130
+ ```
131
+
132
+ ### Fitting (`esfit`)
133
+
134
+ ```python
135
+ import numpy as np
136
+ from torchspin import esfit, FitOptions
137
+
138
+ def model(p):
139
+ sys_mod = SpinSystem(S=[0.5], g=[[p[0], p[1], p[2]]], lw=[1.0, 0.0])
140
+ _, spc = pepper(sys_mod, exp, Options(GridSize=31, Verbosity=0))
141
+ return spc.numpy()
142
+
143
+ result = esfit(
144
+ data_measured, model,
145
+ p0=np.array([2.00, 2.10, 2.20]),
146
+ lb=np.array([1.95, 2.05, 2.15]),
147
+ ub=np.array([2.05, 2.15, 2.25]),
148
+ options=FitOptions(method='global', progress='text'),
149
+ )
150
+ print('Best-fit g:', result.pfit)
151
+ ```
152
+
153
+ `progress='text'` prints RMSD, evaluation count and ETA as it runs, and
154
+ interrupting the kernel returns the best fit so far. Population-based methods
155
+ parallelize over processes with `FitOptions(n_workers='auto')`.
156
+
157
+ ### Differentiable spectrum
158
+
159
+ ```python
160
+ import torch
161
+ from torchspin import differentiable_spectrum
162
+
163
+ g = torch.tensor([2.0, 2.1, 2.2], dtype=torch.float64, requires_grad=True)
164
+ B, spec = differentiable_spectrum(g, mwFreq_GHz=9.5, B_range=(300, 380),
165
+ nPoints=512, lw_mT=1.0)
166
+ loss = ((spec - target_spec) ** 2).sum()
167
+ loss.backward()
168
+ print('dL/dg:', g.grad)
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Units and conventions
174
+
175
+ These match EasySpin, so parameters transfer directly:
176
+
177
+ | | |
178
+ |---|---|
179
+ | Energy | MHz |
180
+ | Magnetic field | mT |
181
+ | Euler angles | radians, z-y'-z'' **passive** rotation |
182
+ | Basis ordering | m = S, S−1, …, −S (descending) |
183
+ | Default dtype | `torch.complex128` |
184
+
185
+ ---
186
+
187
+ ## What's included
188
+
189
+ | Category | Modules |
190
+ |----------|---------|
191
+ | CW EPR | `pepper` (powder; `matrix`, `perturb` and `hybrid` resonance solvers, `grid_convergence` to check `GridSize`), `garlic` (solution), `chili` (slow-motion), `salt` (ENDOR) |
192
+ | Pulse EPR | `saffron` (ESEEM/HYSCORE), `spidyan` (arbitrary sequences), `saffron_thyme` (real pulses) |
193
+ | Trajectory | `cardamom` (MD/diffusion/jump), `mdload`, `mdhmm` |
194
+ | Magnetometry | `curry` (susceptibility, magnetization) |
195
+ | Fitting | `esfit` — 10 methods (simplex, L-BFGS-B, Powell, Levenberg–Marquardt, trust-region reflective, grid, Monte Carlo, genetic, swarm, and `global` = swarm + simplex polish); `torchspin.fitgui` interactive panel |
196
+ | Autograd | `differentiable_spectrum`; `pepper`, `garlic`, `salt`, `saffron` and `curry` are differentiable |
197
+ | Machine learning | `torchspin.ml` — parameter packing, batched simulation, datasets, differentiable losses |
198
+ | Data I/O | `eprload` (14 vendor formats), `eprsave` (BES3T), `orca2torchspin` |
199
+ | Batch / GPU | `batch_pepper`, `batch_simulate`; `Options(device='cuda')` |
200
+
201
+ ---
202
+
203
+ ## Tutorials
204
+
205
+ Start with `01_torchspin_quickstart.ipynb`, then branch to autograd or fitting
206
+ depending on what you need. Full index: [docs/index.md](docs/index.md).
207
+
208
+ | Notebook | What it covers |
209
+ |---|---|
210
+ | `01_torchspin_quickstart` | Core API: spin systems, Hamiltonians, powder and solution spectra |
211
+ | `02_gpu_and_autograd` | GPU execution and differentiable spectra |
212
+ | `03_spectral_fitting` | Parameter recovery with `esfit` |
213
+ | `04_benchmarks` | Timings against MATLAB/EasySpin |
214
+ | `05_fitting_real_world` | Bad starting guesses, loss landscapes, global vs local search |
215
+ | `06_blue_copper_fitting` | End-to-end Cu(II) case study |
216
+ | `07_easyspin_parity` | Overlays against stored EasySpin references |
217
+ | `08_pepper_playground` | Simulate, add noise, fit with every `esfit` method |
218
+ | `09_fit_gui` | Interactive fitting panel: sliders, live fit, RMSD trace |
219
+
220
+ They live in `examples/notebooks/`, with scripted equivalents of EasySpin's
221
+ example collection in `examples/{solidstate,liquids,slowmotion,endor,fitting,magnetometry}`.
222
+
223
+ ---
224
+
225
+ ## Accuracy and performance
226
+
227
+ Every simulator with an EasySpin counterpart is checked against stored MATLAB
228
+ reference output on each commit. Agreement is cosine ≥ 0.999 (most
229
+ 0.9999–1.0000) for `pepper`, `garlic`, `salt`, `saffron`, `spidyan` and
230
+ `curry`, with absolute intensities matched where EasySpin defines them.
231
+ `chili` and the stochastic `cardamom` have documented caveats. Details per
232
+ simulator, with the evidence: [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md).
233
+
234
+ On speed, torchspin is competitive rather than uniformly faster. On an audited
235
+ nine-workload comparison it beat EasySpin on six — most strongly on
236
+ slow-motion `chili` (7.3×) and trajectory `cardamom` (2.0×) — and was slower on
237
+ three, including matrix-method Cu(II) powders (0.34×). The GPU path pays off
238
+ for batched and gradient-based work, not for single simulations. Numbers,
239
+ replicates and a per-workload device audit:
240
+ [BENCHMARK_VERIFICATION.md](benchmarks/results/workstation_20260904_verified/BENCHMARK_VERIFICATION.md).
241
+
242
+ ---
243
+
244
+ ## Citation
245
+
246
+ If you use torchspin in published work, please cite both:
247
+
248
+ - **EasySpin**, the MATLAB toolbox this port is based on —
249
+ Stoll, S. & Schweiger, A. *J. Magn. Reson.* **178** (2006) 42–55.
250
+ - **torchspin** — see [CITATION.cff](CITATION.cff).
251
+
252
+ ---
253
+
254
+ ## Contributing
255
+
256
+ Bug reports, documentation fixes and pull requests are welcome. Development
257
+ install and the test layout are in [CONTRIBUTING.md](CONTRIBUTING.md):
258
+
259
+ ```bash
260
+ git clone https://github.com/follmerlab/torchspin.git
261
+ cd torchspin
262
+ pip install -e ".[dev]"
263
+ pytest -q -m "not slow" -k "not matlab_validation" # fast pass, ~4 min
264
+ ```
265
+
266
+ The MATLAB reference data ships in the repository but not in the PyPI package,
267
+ so the cross-validation suite only runs from a checkout.
268
+
269
+ ---
270
+
271
+ ## Related
272
+
273
+ - [EasySpin](https://easyspin.org) — the MATLAB reference implementation
274
+ - [PyTorch](https://pytorch.org) — the autograd engine
275
+
276
+ ## License
277
+
278
+ MIT — see [LICENSE.md](LICENSE.md). torchspin is derived from EasySpin, which is
279
+ also MIT licensed; both copyrights are reproduced there.