Cod sursa

Biblioteca C++, suita de teste, build-ul si scriptul de figuri - evidentiate sintactic.

src/spectral_test.cpp152 linii ยท cpp
#include "spectral_test.hpp"

#include <cmath>
#include <iomanip>
#include <ostream>
#include <stdexcept>

namespace dft {

namespace {
// NIST SP 800-22 sec 2.6 constants.
constexpr double kThresholdLog = 20.0; // T = sqrt(ln(20) * n)
constexpr double kPeakFraction = 0.95; // expected fraction of peaks below T
} // namespace

SpectralTest::SpectralTest(std::shared_ptr<const DftEngine> engine, double alpha, bool wantSpectrum,
                           std::size_t spectrumPoints)
    : engine_(std::move(engine)), alpha_(alpha), wantSpectrum_(wantSpectrum),
      spectrumPoints_(spectrumPoints)
{
    if (engine_ == nullptr)
    {
        throw std::invalid_argument("SpectralTest requires a non-null DFT engine");
    }
}

namespace {
// Downsample the peak heights to at most maxPoints by keeping the tallest peak
// in each bucket (so visible peaks are preserved), as {k, mag, above}.
std::vector<SpectrumPoint> downsampleSpectrum(const std::vector<double> &mags, double threshold,
                                              std::size_t maxPoints)
{
    std::vector<SpectrumPoint> out;
    const std::size_t half = mags.size();
    if (half == 0)
    {
        return out;
    }
    const bool downsample = half > maxPoints;
    const std::size_t points = downsample ? maxPoints : half;
    out.reserve(points);
    for (std::size_t p = 0; p < points; p++)
    {
        const std::size_t lo = downsample ? (p * half) / points : p;
        const std::size_t hi = downsample ? ((p + 1) * half) / points : p + 1;
        std::size_t bestK = lo;
        double bestMag = mags[lo];
        for (std::size_t i = lo + 1; i < hi; i++)
        {
            if (mags[i] > bestMag)
            {
                bestMag = mags[i];
                bestK = i;
            }
        }
        out.push_back({bestK, bestMag, bestMag >= threshold});
    }
    return out;
}
} // namespace

TestReport SpectralTest::run(const BitSequence &seq) const
{
    std::vector<double> mags;
    const SpectralResult r = analyze(seq, wantSpectrum_ ? &mags : nullptr);

    TestReport report;
    report.n = r.n;
    report.pValue = r.pValue;
    report.statistic = r.d; // d = (N1 - N0)/sigma; reject if |d| > z_{1-alpha/2}
    report.dist = RefDist::Normal2Sided;
    report.stats = {{"threshold", r.threshold},
                    {"N0", r.n0},
                    {"N1", static_cast<double>(r.n1)},
                    {"d", r.d}};
    if (wantSpectrum_)
    {
        report.spectrum = downsampleSpectrum(mags, r.threshold, spectrumPoints_);
    }
    return report;
}

SpectralResult SpectralTest::analyze(const BitSequence &seq, std::vector<double> *outMagnitudes) const
{
    const std::size_t n = seq.size();
    if (n < 2)
    {
        throw std::invalid_argument("spectral test requires at least 2 bits");
    }
    const double length = static_cast<double>(n);

    // Steps 1-2: map bits to +/-1 and take the DFT.
    const std::vector<Complex> spectrum = engine_->transform(seq.toBipolar());

    // Step 3: peak heights are the moduli of the first n/2 components, i.e. the
    // DC term S[0] up to (but not including) the Nyquist term S[n/2].
    const std::size_t half = n / 2;

    // Step 4: 95 % peak-height threshold T = sqrt(ln(20) * n).
    const double threshold = std::sqrt(std::log(kThresholdLog) * length);

    if (outMagnitudes != nullptr)
    {
        outMagnitudes->resize(half);
    }

    // Step 6: count the peaks that fall below the threshold.
    long peaksBelow = 0;
    for (std::size_t i = 0; i < half; i++)
    {
        const double magnitude = std::abs(spectrum[i]);
        if (outMagnitudes != nullptr)
        {
            (*outMagnitudes)[i] = magnitude;
        }
        if (magnitude < threshold)
        {
            peaksBelow++;
        }
    }

    SpectralResult result;
    result.n = n;
    result.threshold = threshold;
    result.n0 = kPeakFraction * length / 2.0; // Step 5
    result.n1 = peaksBelow;

    // Step 7: normalise the deviation. The reference variance is n/4 * 0.95 * 0.05.
    const double deviationSd = std::sqrt(length / 4.0 * kPeakFraction * (1.0 - kPeakFraction));
    result.d = (static_cast<double>(peaksBelow) - result.n0) / deviationSd;
    result.pValue = std::erfc(std::fabs(result.d) / std::sqrt(2.0)); // Step 8
    result.passed = result.pValue >= alpha_;
    return result;
}

std::ostream &operator<<(std::ostream &os, const SpectralResult &result)
{
    const std::ios::fmtflags flags = os.flags();
    const std::streamsize precision = os.precision();
    os << std::fixed << std::setprecision(6) << "n         = " << result.n << "\n"
       << "T         = " << result.threshold << "\n"
       << "N0        = " << result.n0 << "\n"
       << "N1        = " << result.n1 << "\n"
       << "d         = " << result.d << "\n"
       << "p-value   = " << result.pValue << "\n";
    os.flags(flags);
    os.precision(precision);
    return os;
}

} // namespace dft