DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Hysteresis.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
43#include "DspMath.h"
44
45#include <algorithm>
46#include <cmath>
47
48namespace dspark {
49
56template <FloatType T>
58{
59public:
63 void prepare(double sampleRate)
64 {
65 if (!(sampleRate > 0.0) || !std::isfinite(sampleRate)) return;
66 invFs2_ = 0.5 / sampleRate;
67 fs2_ = 2.0 * sampleRate;
68 reset();
69 }
70
72 void reset() noexcept
73 {
74 m_ = 0.0;
75 hPrev_ = 0.0;
76 hDotPrev_ = 0.0;
77 wPrev_ = 0.0;
78 }
79
94 void setParameters(double ms, double a, double alpha, double k, double c) noexcept
95 {
96 if (!std::isfinite(ms) || !std::isfinite(a) || !std::isfinite(alpha)
97 || !std::isfinite(k) || !std::isfinite(c))
98 return;
99 ms_ = std::max(ms, 1.0);
100 a_ = std::max(a, 1.0);
101 alpha_ = std::max(alpha, 0.0);
102 k_ = std::max(k, 1.0);
103 c_ = std::clamp(c, 0.0, 0.999);
104 chi_ = ms_ / a_;
105 }
106
108 [[nodiscard]] double getSaturation() const noexcept { return ms_; }
109
116 [[nodiscard]] double getSmallSignalSusceptibility() const noexcept
117 {
118 const double chiAn = ms_ / (3.0 * a_);
119 return c_ * chiAn / (1.0 - c_ * alpha_ * chiAn);
120 }
121
126 [[nodiscard]] T processSample(T fieldH) noexcept
127 {
128 // Non-finite guard: a single NaN/Inf field would poison m_/hPrev_/
129 // hDotPrev_/wPrev_ permanently (the recursive JA state never clears it,
130 // and inf-inf on the next sample becomes a sticky NaN). Ignore the bad
131 // sample and hold the last magnetization, so a transient upstream
132 // glitch cannot kill the core for the rest of the stream.
133 if (!std::isfinite(fieldH)) return static_cast<T>(m_);
134
135 const double h = static_cast<double>(fieldH);
136 const double hDot = fs2_ * (h - hPrev_) - hDotPrev_;
137
138 // Explicit predictor, then trapezoidal corrector with Newton steps.
139 double m = m_ + 2.0 * invFs2_ * wPrev_;
140 const double rhs = m_ + invFs2_ * wPrev_;
141
142 double w = 0.0, dwdm = 0.0;
143 for (int it = 0; it < 4; ++it)
144 {
145 evaluate(m, h, hDot, w, dwdm);
146 const double g = m - rhs - invFs2_ * w;
147 const double gp = 1.0 - invFs2_ * dwdm;
148 const double dm = g / gp;
149 m -= dm;
150 if (std::abs(dm) < 1e-9 * ms_)
151 break;
152 }
153 // Physical clamp: |M| cannot exceed the saturation magnetization.
154 m = std::clamp(m, -ms_, ms_);
155
156 evaluate(m, h, hDot, w, dwdm);
157 wPrev_ = w;
158 hPrev_ = h;
159 hDotPrev_ = hDot;
160 m_ = m;
161 return static_cast<T>(m);
162 }
163
164private:
166 static void langevin(double x, double& l, double& lp, double& lpp) noexcept
167 {
168 const double ax = std::abs(x);
169 if (ax < 1e-3)
170 {
171 // Taylor: L = x/3 - x^3/45, L' = 1/3 - x^2/15, L'' = -2x/15
172 const double x2 = x * x;
173 l = x * (1.0 / 3.0 - x2 / 45.0);
174 lp = 1.0 / 3.0 - x2 / 15.0;
175 lpp = -2.0 * x / 15.0;
176 }
177 else if (ax > 30.0)
178 {
179 // csch^2 underflows to 0 well before this point.
180 l = (x > 0.0 ? 1.0 : -1.0) - 1.0 / x;
181 lp = 1.0 / (x * x);
182 lpp = -2.0 / (x * x * x);
183 }
184 else
185 {
186 const double e = std::exp(x);
187 const double ie = 1.0 / e;
188 const double sh = 0.5 * (e - ie);
189 const double ch = 0.5 * (e + ie);
190 const double coth = ch / sh;
191 const double csch2 = 1.0 / (sh * sh);
192 const double ix = 1.0 / x;
193 l = coth - ix;
194 lp = ix * ix - csch2;
195 lpp = 2.0 * (csch2 * coth - ix * ix * ix);
196 }
197 }
198
200 void evaluate(double m, double h, double hDot, double& w, double& dwdm) const noexcept
201 {
202 if (hDot == 0.0)
203 {
204 w = 0.0;
205 dwdm = 0.0;
206 return;
207 }
208
209 const double q = (h + alpha_ * m) / a_;
210 double l = 0.0, lp = 0.0, lpp = 0.0;
211 langevin(q, l, lp, lpp);
212
213 const double mAn = ms_ * l;
214 const double dM = mAn - m;
215 const double delta = (hDot > 0.0) ? 1.0 : -1.0;
216 const double deltaM = (dM * delta > 0.0) ? 1.0 : 0.0; // gate reversal
217
218 // Guard the JA denominator singularity (can cross zero at hard drive).
219 const double oneMc = 1.0 - c_;
220 double d1 = oneMc * delta * k_ - alpha_ * dM;
221 const double d1Min = 0.01 * oneMc * k_;
222 if (std::abs(d1) < d1Min)
223 d1 = (d1 >= 0.0) ? d1Min : -d1Min;
224
225 const double phi1 = oneMc * deltaM * dM / d1;
226 const double cChiLp = c_ * chi_ * lp;
227
228 const double num = phi1 + cChiLp;
229 // Guard the reversible-branch denominator too: physical JA parameter
230 // sets satisfy c*alpha*Ms/(3a) < 1 so den stays near 1, but an extreme
231 // user set makes it sweep through zero as Q moves, spiking dM/dt (and
232 // wPrev_, which has no physical clamp) by orders of magnitude for a
233 // sample. The M clamp recovers afterwards; this bounds the spike.
234 double den = 1.0 - alpha_ * cChiLp;
235 if (std::abs(den) < 0.01)
236 den = (den >= 0.0) ? 0.01 : -0.01;
237 w = hDot * num / den;
238
239 // Analytic dW/dM for the Newton step.
240 const double alphaOverA = alpha_ / a_;
241 const double dmAn = ms_ * lp * alphaOverA - 1.0; // d(M_an - M)/dM
242 const double dPhi1 = oneMc * deltaM * dmAn * (oneMc * delta * k_) / (d1 * d1);
243 const double dLp = lpp * alphaOverA;
244 const double dNum = dPhi1 + c_ * chi_ * dLp;
245 const double dDen = -alpha_ * c_ * chi_ * dLp;
246 dwdm = hDot * (dNum * den - num * dDen) / (den * den);
247 }
248
249 // -- Members ----------------------------------------------------------------
250 double ms_ = 3.5e5, a_ = 2.2e4, alpha_ = 1.6e-3, k_ = 2.7e4, c_ = 1.7e-1;
251 double chi_ = 3.5e5 / 2.2e4;
252
253 double invFs2_ = 0.5 / 48000.0;
254 double fs2_ = 2.0 * 48000.0;
255
256 double m_ = 0.0;
257 double hPrev_ = 0.0;
258 double hDotPrev_ = 0.0;
259 double wPrev_ = 0.0;
260};
261
262} // namespace dspark
Per-channel Jiles-Atherton hysteresis processor (field in, M out).
Definition Hysteresis.h:58
T processSample(T fieldH) noexcept
Processes one sample of applied field H, returns magnetization M.
Definition Hysteresis.h:126
double getSaturation() const noexcept
Saturation magnetization Ms (A/m).
Definition Hysteresis.h:108
double getSmallSignalSusceptibility() const noexcept
Small-signal susceptibility dM/dH around the demagnetized state.
Definition Hysteresis.h:116
void setParameters(double ms, double a, double alpha, double k, double c) noexcept
Sets the Jiles-Atherton parameters.
Definition Hysteresis.h:94
void reset() noexcept
Clears magnetization and integrator state. RT-safe.
Definition Hysteresis.h:72
void prepare(double sampleRate)
Prepares the integrator for the given sample rate. A non-positive or non-finite rate is ignored (NaN ...
Definition Hysteresis.h:63
Main namespace for the DSPark framework.