You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
octave/doppler_spread.m hands the Gaussian Doppler power spectrum directly
to fir2 as the filter's amplitude target:
sigma =dopplerSpreadHz/2;
x =0:lowFs/100:lowFs/2;
y = (1/(sigma*sqrt(2*pi)))*exp(-(x.^2)/(2*sigma*sigma));
b = fir2(Ntaps-1, x/(lowFs/2), y);
Since the output PSD of filtered white noise is |H(f)|^2, the realized
fading-process power spectrum is the square of that Gaussian — a Gaussian
with standard deviation sigma/sqrt(2). The realized 2-sigma Doppler spread
is therefore 1/sqrt(2) of the specified value. Measured against current
main with the script below: asking for a 1.0 Hz spread yields 0.71 Hz
(PSD second moment 0.715, autocorrelation fit 0.709 — both right on the
1/sqrt(2) = 0.707 theory). All copies I checked carry the same line: codec2/octave/doppler_spread.m, the legacy codec2-dev history back to
its 2016 introduction, misc/freedv_low/doppler_spread.m (the version
issues #16/#18 reference as bug-fixed — those fixes address the resampler,
not the spectral shape), and radae/doppler_spread.m.
The existing unit test (doppler_spread_ut.m) compares the designed
filter's amplitude response against y, so it passes by construction
and cannot see this.
Why the spread is defined on the power spectrum
Every standard and implementation I could check specifies the Gaussian and
its 2-sigma "frequency spread" / "Doppler spread" on the power spectrum
of the tap-gain process, which means a shaping filter needs |H| = sqrt(PSD):
ITU-R F.1487 (Annex 1, eq. 2): "Each tap-gain function has a power
spectrum, f_i(ν) … each of which is a Gaussian function of frequency",
and (Annex 3) "the frequency spread is the 2σ value as used in
equation (2)".
PathSim — the reference the code comment credits ("Used PathSim
technical guide as a reference - thanks Moe!") — is also on this side,
though its prose is easy to misread: it builds a Gaussian impulse
response with σ_taps = Fs·sqrt(2)/(2π·F2σ) (pathsimtech100 §4.1.5),
which gives an amplitude response of width F2σ/sqrt(2), exactly sqrt(PSD) of a Gaussian PSD with 2-sigma width F2σ. The sqrt lives
in the width constant. (I've sent the modernized PathSim port a doc PR
spelling this out at the constant, so the guide's prose stops catching
people: docs(GaussFIR): explain the sqrt(2) in the Doppler filter width constant bubnikv/pathsim#4.)
Reproduction (Octave, run from codec2/octave)
Two independent estimators, both needed: the FFT bins must be much finer
than the spectrum (coarse bins smear the second moment upward), and the
autocorrelation fit is window-free.
pkgload signal
d =1.0; Fs =8000; N =Fs*3600; % ask for 1.0 Hz spread
s = doppler_spread(d, Fs, N);
% (a) PSD second moment, fine bins (2^19 -> 0.015 Hz/bin)
seg =2^19; nseg = floor(length(s)/seg); P = zeros(seg,1);
for k=1:nseg
xk = s((k-1)*seg+1:k*seg)(:) .* hanning(seg);
P =P+ abs(fft(xk)).^2;
end
f = ((0:seg-1)'/seg)*Fs; f(f>Fs/2) -= Fs;
m = abs(f) <5*d; % exclude resampler images
sigma_psd = sqrt(sum(P(m).*f(m).^2)/sum(P(m)));
% (b) autocorrelation small-lag fit: R(tau) = exp(-2*pi^2*sig^2*tau^2)
sig_ac =0; L =0;
for lag_s = [0.050.10.150.20.25]
k = round(lag_s*Fs);
R = abs(sum(conj(s(1:end-k)).*s(1+k:end)) / sum(abs(s).^2));
sig_ac += sqrt(-log(R)/(2*pi^2*(k/Fs)^2)); L += 1;
endsig_ac /= L;
printf("specified 2-sigma: %.1f Hz | realized PSD: %.3f Hz | autocorr: %.3f Hz\n", ...
d, 2*sigma_psd, 2*sig_ac);
Every fading simulation built on doppler_spread (the ch tool's
MPP/MPD-style channels, freedv/FreeDATA test campaigns) runs its fading
~29% slower than labeled — e.g. a "1 Hz poor channel" is effectively a
~0.7 Hz channel. Relative comparisons made with the same tool are
unaffected; absolute comparisons to other simulators (PathSim, IONOS,
MIL-STD-compliant instruments) or to published performance curves are
biased optimistic, since slower fading is generally easier on a modem.
Suggested fix
Minimal (one token — keeps the design approach):
b = fir2(Ntaps-1, x/(lowFs/2), sqrt(y));
Or the MIL-STD-188-110C App E closed-form time-domain taps, which also
remove the frequency-sampling design approximation entirely (note this
changes the filter length, so the Ntaps used for the memory-fill
bookkeeping needs to follow):
tau = sqrt(log(100))/(pi*d); % 1%-of-peak truncation
t = (-ceil(tau*lowFs):ceil(tau*lowFs))/lowFs;
b = sqrt(2)*exp(-pi^2*t.^2*d^2);
b =b/sqrt(sum(b.^2)); % unit noise-power gain
Either way, a regression test on the realized spread (autocorrelation fit
or fine-bin second-moment PSD, per the repro above) would keep this pinned;
the current amplitude-response unit test cannot catch it.
Summary
octave/doppler_spread.mhands the Gaussian Doppler power spectrum directlyto
fir2as the filter's amplitude target:Since the output PSD of filtered white noise is
|H(f)|^2, the realizedfading-process power spectrum is the square of that Gaussian — a Gaussian
with standard deviation
sigma/sqrt(2). The realized 2-sigma Doppler spreadis therefore 1/sqrt(2) of the specified value. Measured against current
main with the script below: asking for a 1.0 Hz spread yields 0.71 Hz
(PSD second moment 0.715, autocorrelation fit 0.709 — both right on the
1/sqrt(2) = 0.707 theory). All copies I checked carry the same line:
codec2/octave/doppler_spread.m, the legacycodec2-devhistory back toits 2016 introduction,
misc/freedv_low/doppler_spread.m(the versionissues #16/#18 reference as bug-fixed — those fixes address the resampler,
not the spectral shape), and
radae/doppler_spread.m.The existing unit test (
doppler_spread_ut.m) compares the designedfilter's amplitude response against
y, so it passes by constructionand cannot see this.
Why the spread is defined on the power spectrum
Every standard and implementation I could check specifies the Gaussian and
its 2-sigma "frequency spread" / "Doppler spread" on the power spectrum
of the tap-gain process, which means a shaping filter needs
|H| = sqrt(PSD):spectrum, f_i(ν) … each of which is a Gaussian function of frequency",
and (Annex 3) "the frequency spread is the 2σ value as used in
equation (2)".
with the square explicit,
|H_j(f)|² = e^(−2f²/d_j²)/sqrt(π d_j²/2),and gives the corresponding time-domain taps
f_j(t) = k·sqrt(2)·e^(−π²t²d²)(truncated where the tap falls to 1% ofpeak).
https://everyspec.com/MIL-STD/MIL-STD-0100-0299/download.php?spec=MIL-STD-188_110C.037889.PDF
(pp. 218–226)
citing Watterson et al. 1970): random processes "are produced by passing
Rayleigh-distributed noise through a filter whose |H(jΩ)|² has the
required Gaussian shape."
https://its.ntia.gov/umbraco/surface/download/publication?reportNumber=HF+Simulator.pdf
technical guide as a reference - thanks Moe!") — is also on this side,
though its prose is easy to misread: it builds a Gaussian impulse
response with
σ_taps = Fs·sqrt(2)/(2π·F2σ)(pathsimtech100 §4.1.5),which gives an amplitude response of width
F2σ/sqrt(2), exactlysqrt(PSD)of a Gaussian PSD with 2-sigma widthF2σ. The sqrt livesin the width constant. (I've sent the modernized PathSim port a doc PR
spelling this out at the constant, so the guide's prose stops catching
people: docs(GaussFIR): explain the sqrt(2) in the Doppler filter width constant bubnikv/pathsim#4.)
Reproduction (Octave, run from
codec2/octave)Two independent estimators, both needed: the FFT bins must be much finer
than the spectrum (coarse bins smear the second moment upward), and the
autocorrelation fit is window-free.
Output on current main (Octave 8.x):
i.e. right on the 1/sqrt(2) = 0.707 prediction.
Impact
Every fading simulation built on
doppler_spread(thechtool'sMPP/MPD-style channels, freedv/FreeDATA test campaigns) runs its fading
~29% slower than labeled — e.g. a "1 Hz poor channel" is effectively a
~0.7 Hz channel. Relative comparisons made with the same tool are
unaffected; absolute comparisons to other simulators (PathSim, IONOS,
MIL-STD-compliant instruments) or to published performance curves are
biased optimistic, since slower fading is generally easier on a modem.
Suggested fix
Minimal (one token — keeps the design approach):
Or the MIL-STD-188-110C App E closed-form time-domain taps, which also
remove the frequency-sampling design approximation entirely (note this
changes the filter length, so the
Ntapsused for the memory-fillbookkeeping needs to follow):
Either way, a regression test on the realized spread (autocorrelation fit
or fine-bin second-moment PSD, per the repro above) would keep this pinned;
the current amplitude-response unit test cannot catch it.
Happy to send a PR for whichever form you prefer.