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.
- {torchspin-0.3.0 → torchspin-0.4.0}/CHANGELOG.md +233 -17
- {torchspin-0.3.0 → torchspin-0.4.0}/CITATION.cff +2 -2
- torchspin-0.4.0/PKG-INFO +279 -0
- torchspin-0.4.0/README.md +224 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/pyproject.toml +4 -4
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/__init__.py +4 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_linalg.py +99 -20
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/angmom.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/autograd.py +10 -10
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/chili_sle.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/convspec.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/eprload.py +39 -8
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/eprsave.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/esfit.py +35 -11
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/experiment.py +41 -5
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/fitgui.py +29 -4
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/garlic.py +2 -2
- torchspin-0.4.0/torchspin/gridcheck.py +129 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_ezho.py +5 -5
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_zf.py +3 -3
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/hamsymm.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/initstate.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/isotopologues.py +2 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/levels.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/lineshape.py +14 -14
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ml.py +3 -3
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/orca2torchspin.py +8 -8
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ordering.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/orisel.py +3 -3
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/pepper.py +146 -49
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/pepper_autograd.py +14 -4
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/propint.py +2 -2
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields_batch.py +93 -38
- torchspin-0.4.0/torchspin/resfields_hybrid.py +277 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rotutils.py +6 -6
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spinops.py +1 -1
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spinsystem.py +8 -8
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/transmitter.py +1 -1
- torchspin-0.4.0/torchspin.egg-info/PKG-INFO +279 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin.egg-info/SOURCES.txt +2 -0
- {torchspin-0.3.0 → torchspin-0.4.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.4.0}/LICENSE.md +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/MANIFEST.in +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/setup.cfg +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_cardamom_istos.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_cardamom_propagatedm.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_cardamom_utils.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/_compile.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/autoguess.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/batch.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/blochsteady.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/cardamom.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/chili.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/constants.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ctafft.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/curry.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/FourierSeriesCoefficients.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/GaussianCascadeCoefficients.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/isotopedata.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/data/spacegroups.txt +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/dataproc.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/dipbackground.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/dipkernel.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/diptensor.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/endorfrq.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/endorfrq_perturb.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/evolve.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ewrls.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/excitation.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/exciteprofile.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/exponfit.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/fastmotion.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/fdaxis.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_cf.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_ee.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_ez.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_hf.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_nn.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_nq.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_nz.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_oz.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/ham_so.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/levelsplot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/lpsvd.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/makespec.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mdhmm.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mdload.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mdtraj2oripot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/mlpsvd.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/nucdata.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/nucfrq2d.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/oripotentialplot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/photoselect.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/plegendre.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/pulse.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/py.typed +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rapidscan2spc.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields_eig.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfields_perturb.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfreqs_matrix.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resfreqs_perturb.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resonator.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/resonatorprofile.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rfmixer.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/rotations.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron_pathways.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron_peaks.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/saffron_thyme.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/salt.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/sigeq.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/signalprocessing.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/sitetransforms.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/sphgrid.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spidyan.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/spinladder.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stackplot.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stev.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stochtraj_diffusion.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/stochtraj_jump.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/strainwidth.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin/utils.py +0 -0
- {torchspin-0.3.0 → torchspin-0.4.0}/torchspin.egg-info/dependency_links.txt +0 -0
- {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
|
-
|
|
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,
|
|
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
|
|
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-
|
|
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
|
-
|
|
343
|
+
discretization).
|
|
131
344
|
- `Options.AccumMethod='linear'` (EasySpin `makespec`: a line is split between
|
|
132
|
-
its two
|
|
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
|
|
181
|
-
through the regular simulators; `resfields_batch` Boltzmann
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
360
|
-
frequency sweeps had wrong
|
|
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
|
|
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
|
-
|
|
522
|
-
|
|
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.
|
|
6
|
-
date-released: "2026-10-
|
|
5
|
+
version: "0.4.0"
|
|
6
|
+
date-released: "2026-10-09"
|
|
7
7
|
license: MIT
|
|
8
8
|
authors:
|
|
9
9
|
- family-names: "Follmer"
|
torchspin-0.4.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/torchspin/)
|
|
61
|
+
[](https://pypi.org/project/torchspin/)
|
|
62
|
+
[](https://github.com/follmerlab/torchspin/actions/workflows/python-tests.yml)
|
|
63
|
+
[](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.
|