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