blob: aa25227b35813a9ef034222b1684df3e58c4d23d [file]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#include "main.h"
#include "fp_control.h"
#include <contrib/Eigen/StructuredMatrices>
using namespace Eigen;
// Dense inverse DFT matrix, used to synthesize generators independently of the
// FFT implementation under test.
template <typename RealScalar>
Matrix<std::complex<RealScalar>, Dynamic, Dynamic> inverse_dft_matrix(Index n) {
typedef std::complex<RealScalar> Complex;
Matrix<Complex, Dynamic, Dynamic> F(n, n);
for (Index a = 0; a < n; ++a)
for (Index c = 0; c < n; ++c)
F(a, c) =
std::polar(RealScalar(1) / RealScalar(n), RealScalar(2 * EIGEN_PI) * RealScalar((a * c) % n) / RealScalar(n));
return F;
}
// Reference dense BCCB built entry-wise from the generating array, independently
// of the operator under test: entry (i,j) with i = b1*n2+i2, j = c1*n2+j2 is
// G((i2-j2) mod n2, (b1-c1) mod n1).
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_bccb(const Matrix<Scalar, Dynamic, Dynamic>& G) {
const Index n2 = G.rows(), n1 = G.cols(), N = n1 * n2;
Matrix<Scalar, Dynamic, Dynamic> dense(N, N);
for (Index j = 0; j < N; ++j)
for (Index i = 0; i < N; ++i) {
Index k2 = i % n2 - j % n2;
if (k2 < 0) k2 += n2;
Index k1 = i / n2 - j / n2;
if (k1 < 0) k1 += n1;
dense(i, j) = G(k2, k1);
}
return dense;
}
template <typename Scalar>
void test_bccb_product(Index n2, Index n1) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
VERIFY_IS_EQUAL(C.rows(), N);
Mat Cd = C;
VERIFY_IS_APPROX(Cd, dense);
Mat accumulated = Mat::Random(N, N);
const Mat initial = accumulated;
accumulated += C;
VERIFY_IS_APPROX(accumulated, (initial + dense).eval());
accumulated = initial;
accumulated -= C;
VERIFY_IS_APPROX(accumulated, (initial - dense).eval());
for (Index t = 0; t < 5; ++t) {
Index i = internal::random<Index>(0, N - 1), j = internal::random<Index>(0, N - 1);
VERIFY_IS_APPROX(C.coeff(i, j), dense(i, j));
}
Vec x = Vec::Random(N);
VERIFY_IS_APPROX((C * x).eval(), (dense * x).eval());
Mat X = Mat::Random(N, 3);
VERIFY_IS_APPROX((C * X).eval(), (dense * X).eval());
// Accumulation form exercised by the iterative solvers.
Vec y = Vec::Random(N);
Vec y0 = y;
y.noalias() += C * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
}
template <typename Scalar>
void test_bccb_transpose(Index n2, Index n1) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
Mat Td = C.transpose();
VERIFY_IS_APPROX(Td, Mat(dense.transpose()));
Mat Ad = C.adjoint();
VERIFY_IS_APPROX(Ad, Mat(dense.adjoint()));
Mat Kd = C.conjugate();
VERIFY_IS_APPROX(Kd, Mat(dense.conjugate()));
Vec x = Vec::Random(N);
VERIFY_IS_APPROX((C.transpose() * x).eval(), (dense.transpose() * x).eval());
VERIFY_IS_APPROX((C.adjoint() * x).eval(), (dense.adjoint() * x).eval());
// Exact round trips: generators and symbols are pure permutations/conjugations.
Bccb<Scalar> Ctt = C.transpose().transpose();
VERIFY_IS_EQUAL(Ctt.generator(), G);
VERIFY_IS_EQUAL(Ctt.symbol(), C.symbol());
Bccb<Scalar> Caa = C.adjoint().adjoint();
VERIFY_IS_EQUAL(Caa.generator(), G);
VERIFY_IS_EQUAL(Caa.symbol(), C.symbol());
}
// transpose()/conjugate()/adjoint() return owning temporaries, so the product
// expression must nest the structured operand by value: a delayed-evaluated
// expression has to outlive the temporary operator it was built from. The static
// check pins the value nesting; the behavioral check would read freed memory if
// the product held a reference instead.
template <typename Scalar>
void test_bccb_delayed_product(Index n2, Index n1) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
STATIC_CHECK(!std::is_reference<typename internal::ref_selector<Bccb<Scalar>>::type>::value);
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
Vec x = Vec::Random(N);
auto expr = C.adjoint() * x; // the adjoint temporary dies with the full expression
Vec scribble = Vec::Random(2 * N); // reuses the temporary's freed heap storage
Vec y = expr;
VERIFY_IS_APPROX(y, (dense.adjoint() * x).eval());
VERIFY_IS_EQUAL(scribble.size(), 2 * N); // keep the scribble alive across the evaluation
}
// The products carry the default product tag, so plain assignment materializes
// a temporary exactly like a dense product: x = C * x and x += C * x get the
// ordinary dense-product aliasing semantics.
template <typename Scalar>
void test_bccb_aliased_product(Index n2, Index n1) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
Vec x = Vec::Random(N);
Vec y = x;
y = C * y;
VERIFY_IS_APPROX(y, (dense * x).eval());
y = x;
y += C * y;
VERIFY_IS_APPROX(y, (x + dense * x).eval());
y = x;
y -= C * y;
VERIFY_IS_APPROX(y, (x - dense * x).eval());
Mat X = Mat::Random(N, 3);
Mat Y = X;
Y = C * Y;
VERIFY_IS_APPROX(Y, (dense * X).eval());
}
// Aliasing beyond the same-object case: the default-product temporary must also
// resolve right-hand-side expressions that reference the destination and
// overlapping views of one buffer, neither of which is_same_dense can see.
template <typename Scalar>
void test_bccb_aliased_expression(Index n2, Index n1) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
// Right-hand-side expression referencing the destination.
Vec x = Vec::Random(N), x0 = x;
x = C * (x + Vec::Ones(N));
VERIFY_IS_APPROX(x, (dense * (x0 + Vec::Ones(N))).eval());
// Overlapping (shifted) segments of one buffer.
Vec buf = Vec::Random(N + 1);
Vec expected = dense * buf.tail(N);
buf.head(N) = C * buf.tail(N);
VERIFY_IS_APPROX(buf.head(N).eval(), expected);
}
// Mixed-scalar products: a real operator applied to a complex right-hand side
// (and a complex operator applied to a real one) promotes to the complex product
// scalar, so alpha and the accumulation must run in the promoted type rather than
// the operator scalar.
template <typename RealScalar>
void test_bccb_mixed_scalar(Index n2, Index n1) {
typedef std::complex<RealScalar> Complex;
typedef Matrix<RealScalar, Dynamic, 1> RVec;
typedef Matrix<RealScalar, Dynamic, Dynamic> RMat;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
RMat G = RMat::Random(n2, n1);
Bccb<RealScalar> C(G);
CMat dense = reference_bccb<RealScalar>(G).template cast<Complex>();
const Index N = n1 * n2;
CVec x = CVec::Random(N);
CVec y = C * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
CVec y0 = CVec::Random(N);
y = y0;
y.noalias() += C * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
CMat Gc = CMat::Random(n2, n1);
Bccb<Complex> Cc(Gc);
CMat denseC = reference_bccb<Complex>(Gc);
RVec xr = RVec::Random(N);
CVec z = Cc * xr;
VERIFY_IS_APPROX(z, (denseC * xr).eval());
}
// Non-5-smooth block dimensions: products transform on the padded per-axis
// circulant-embedding grid (kissfft's generic butterfly is quadratic in prime
// factors), while the spectral operations keep the exact-size symbol. Cover
// each padded combination -- n2 awkward, n1 awkward, both -- against the dense
// reference, including the transposition family (which reuses the padded
// symbol through the reversal/conjugation rules), solve, and inverse (which
// rebuilds the padded symbol for the inverse operator).
template <typename Scalar>
void test_bccb_prime_dimensions(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
const Index N = n1 * n2;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
Vec x = Vec::Random(N);
VERIFY_IS_APPROX((C * x).eval(), (dense * x).eval());
Mat X = Mat::Random(N, 3);
VERIFY_IS_APPROX((C * X).eval(), (dense * X).eval());
VERIFY_IS_APPROX((C.transpose() * x).eval(), (dense.transpose() * x).eval());
VERIFY_IS_APPROX((C.adjoint() * x).eval(), (dense.adjoint() * x).eval());
VERIFY_IS_APPROX((C.conjugate() * x).eval(), (dense.conjugate() * x).eval());
// Exact symbol round trip through the transposition family: both cached
// symbols are pure permutations/conjugations of the originals.
Bccb<Scalar> Ctt = C.transpose().transpose();
VERIFY_IS_EQUAL(Ctt.generator(), G);
VERIFY_IS_EQUAL(Ctt.symbol(), C.symbol());
// Solve (exact-size spectral path) and inverse (whose product rebuilds the
// padded symbol): diagonal dominance keeps the symbol away from zero.
Mat Gd = Mat::Random(n2, n1);
Gd(0, 0) += Scalar(RealScalar(2 * N));
Bccb<Scalar> Cd(Gd);
Mat densed = reference_bccb<Scalar>(Gd);
Vec b = Vec::Random(N);
Vec xs = Cd.solve(b);
VERIFY_IS_APPROX((densed * xs).eval(), b);
VERIFY_IS_APPROX((Cd.inverse() * b).eval(), xs);
}
// Dynamic dimension mismatches must trip the runtime assertion when the product
// or solve expression is built. (Incompatible *fixed* sizes are rejected at
// compile time by a static assertion in operator* / solve, which a runtime test
// cannot exercise.)
void test_bccb_dimension_asserts() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(3, 4);
Bccb<double> C(G);
Vec bad = Vec::Random(11);
VERIFY_RAISES_ASSERT(C * bad);
VERIFY_RAISES_ASSERT(C.solve(bad));
}
template <typename Scalar>
void test_bccb_solve(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Diagonal dominance (through the (0,0) generator entry) keeps the symbol away
// from zero, so the direct 2-D FFT solve is exact up to roundoff.
Mat G = Mat::Random(n2, n1);
G(0, 0) += Scalar(RealScalar(2 * n1 * n2));
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
Vec b = Vec::Random(N);
Vec x = C.solve(b);
VERIFY_IS_APPROX((dense * x).eval(), b);
Mat B = Mat::Random(N, 3);
Mat Xs = C.solve(B);
VERIFY_IS_APPROX((dense * Xs).eval(), B);
}
// Rank-deficient BCCB synthesized by zeroing 2-D symbol entries: the rank counts
// the surviving entries and solve() matches the SVD pseudo-inverse.
template <typename Scalar>
void test_bccb_rank_deficient(Index n2, Index n1, Index defect) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
const Index N = n1 * n2;
CMat S = CMat::Random(n2, n1);
S.array() += Complex(2); // keep the surviving moduli away from the threshold
for (Index k = 0; k < defect; ++k) S((2 * k + 1) % n2, (3 * k) % n1) = Complex(0);
const CMat F2 = inverse_dft_matrix<RealScalar>(n2), F1 = inverse_dft_matrix<RealScalar>(n1);
Mat G = (F2 * S * F1.transpose()).eval(); // Scalar is complex here
Bccb<Scalar> C(G);
VERIFY_IS_EQUAL(C.rank(), N - defect);
Mat dense = reference_bccb<Scalar>(G);
JacobiSVD<Mat> svd(dense, ComputeThinU | ComputeThinV);
Vec b = Vec::Random(N);
VERIFY_IS_APPROX(C.solve(b), svd.solve(b).eval());
}
template <typename Scalar>
void test_bccb_zero(Index n2, Index n1) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Bccb<Scalar> C(Mat(Mat::Zero(n2, n1)));
VERIFY_IS_EQUAL(C.rank(), 0);
VERIFY(C.singularValues().isZero());
Vec b = Vec::Random(n1 * n2);
VERIFY(C.solve(b).isZero());
}
void test_bccb_nan_propagation(Index n2, Index n1) {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
G(n2 / 2, n1 / 2) = std::numeric_limits<double>::quiet_NaN();
Bccb<double> C(G);
VERIFY_IS_EQUAL(C.rank(), n1 * n2);
Vec b = Vec::Random(n1 * n2);
Vec x = C.solve(b);
VERIFY(!(x.array() == x.array()).all());
}
// Inputs at the very top (and bottom) of the exponent range: the transforms sum
// up to N terms, so without scaling the FFT intermediates overflow even though
// every entry of the true result is representable. The apply path rescales the
// symbol and each right-hand-side column by exact powers of two and folds the
// exponent back at the end.
void test_bccb_finite_overflow() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const double huge = (std::numeric_limits<double>::max)() / 16;
// The 2-D FFT round trip costs a few ulps per entry; a norm-based comparison
// would itself overflow at these magnitudes, hence the entrywise bound.
const double kFftRoundTripTol = 100 * NumTraits<double>::epsilon();
// Reviewer repro: a 36x36 identity BCCB applied to a vector of DBL_MAX/16
// must return the vector, not 36 NaNs.
Mat G = Mat::Zero(6, 6);
G(0, 0) = 1.0;
Bccb<double> C(G);
Vec x = Vec::Constant(36, huge);
Vec y = C * x;
VERIFY(y.allFinite());
VERIFY(((y - x).array().abs() <= kFftRoundTripTol * x.array().abs()).all());
// A huge generator makes a huge symbol: the scaling must come from the symbol
// side as well.
Mat Gh = Mat::Zero(6, 6);
Gh(0, 0) = huge; // C == huge * Identity
Bccb<double> Ch(Gh);
Vec ones = Vec::Ones(36);
Vec z = Ch * ones;
VERIFY(z.allFinite());
VERIFY(((z.array() - huge).abs() <= kFftRoundTripTol * huge).all());
// The inverse symbol of a tiny operator is huge; solve() shares the scaled
// apply path.
Mat Gt = Mat::Zero(6, 6);
Gt(0, 0) = 1.0 / huge;
Bccb<double> Ct(Gt);
Vec w = Ct.solve(ones);
VERIFY(w.allFinite());
VERIFY(((w.array() - huge).abs() <= kFftRoundTripTol * huge).all());
// An exactly zero column maps to an exactly zero column (short-circuited
// before any scaling), while nonzero columns are still transformed.
Mat B(36, 2);
B.col(0).setConstant(huge);
B.col(1).setZero();
Mat Y = C * B;
VERIFY(Y.col(1).isZero());
VERIFY(((Y.col(0) - x).array().abs() <= kFftRoundTripTol * x.array().abs()).all());
// Genuine NaN and infinity inputs keep propagating; such right-hand sides
// take the direct kernel (see test_bccb_nonfinite_product for the entrywise
// semantics).
Vec xn = x;
xn[7] = std::numeric_limits<double>::quiet_NaN();
Vec yn = C * xn;
VERIFY(!(yn.array() == yn.array()).all());
Vec xi = Vec::Ones(36);
xi[3] = std::numeric_limits<double>::infinity();
Vec yi = C * xi;
VERIFY(!yi.allFinite());
}
// The scaling exponents are derived from component-wise magnitudes: a finite
// complex value near the overflow threshold has a non-representable modulus,
// which would otherwise disable the scaling and turn an exactly representable
// product into NaN.
template <typename RealScalar>
void test_bccb_fft_complex_boundary(Index n2, Index n1) {
typedef std::complex<RealScalar> Complex;
typedef Matrix<RealScalar, Dynamic, Dynamic> RMat;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
const RealScalar kFftRoundTripTol = RealScalar(100) * NumTraits<RealScalar>::epsilon();
const RealScalar big = RealScalar(0.75) * (std::numeric_limits<RealScalar>::max)();
const Index N = n1 * n2;
// Identity BCCB with a complex generator: the product returns the right-hand
// side unchanged even though |x_k| overflows.
CMat G = CMat::Zero(n2, n1);
G(0, 0) = Complex(1);
Bccb<Complex> C(G);
CVec x = CVec::Constant(N, Complex(big, big));
CVec y = C * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / big).maxCoeff() <= kFftRoundTripTol);
// A real identity operator applied to the same complex right-hand side takes
// the mixed-scalar product path.
RMat Gr = RMat::Zero(n2, n1);
Gr(0, 0) = RealScalar(1);
Bccb<RealScalar> Cr(Gr);
y = Cr * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / big).maxCoeff() <= kFftRoundTripTol);
}
// Entrywise IEEE comparison for the non-finite tests: NaNs match NaNs,
// infinities match by value (sign included), finite entries match to roundoff.
// VERIFY_IS_APPROX would reject any output containing NaN.
template <typename D1, typename D2>
bool ieee_entrywise_match(const D1& a, const D2& b) {
if (a.rows() != b.rows() || a.cols() != b.cols()) return false;
for (Index j = 0; j < a.cols(); ++j)
for (Index i = 0; i < a.rows(); ++i) {
const typename D1::Scalar x = a(i, j), y = b(i, j);
if (x == y) continue; // finite match or same-signed infinities
if ((numext::isnan)(x) && (numext::isnan)(y)) continue; // both NaN
if (!test_isApprox(x, y)) return false; // finite roundoff
}
return true;
}
// Scalar-loop product: the mathematically transparent IEEE reference for the
// non-finite tests. Eigen's own vectorized complex kernels can smear a single
// infinity into NaN (Inf - Inf across the split real/imaginary accumulators), so
// the dense product is not a faithful entrywise reference for non-finite data.
template <typename Scalar>
Matrix<Scalar, Dynamic, 1> reference_product_ieee(const Matrix<Scalar, Dynamic, Dynamic>& A,
const Matrix<Scalar, Dynamic, 1>& x) {
Matrix<Scalar, Dynamic, 1> y(A.rows());
for (Index i = 0; i < A.rows(); ++i) {
Scalar acc(0);
for (Index j = 0; j < A.cols(); ++j) acc += A(i, j) * x[j];
y[i] = acc;
}
return y;
}
// A single Inf or NaN in the data must propagate like the reference product --
// through the dot products that touch it -- instead of being smeared into NaNs
// across the whole output by the transforms.
template <typename Scalar>
void test_bccb_nonfinite_product(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
const RealScalar inf = std::numeric_limits<RealScalar>::infinity();
const RealScalar nan = std::numeric_limits<RealScalar>::quiet_NaN();
const Index N = n1 * n2;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
// Inf in the right-hand side.
Vec x = Vec::Random(N);
x[N / 2] = Scalar(inf);
VERIFY(ieee_entrywise_match((C * x).eval(), reference_product_ieee(dense, x)));
// NaN in the right-hand side.
Vec xn = Vec::Random(N);
xn[N - 1] = Scalar(nan);
VERIFY(ieee_entrywise_match((C * xn).eval(), reference_product_ieee(dense, xn)));
// Mixed multi-column right-hand side: the non-finite column falls back to the
// direct kernel individually while the finite column keeps the FFT path.
Mat Xm(N, 2);
Xm.col(0) = Vec::Random(N);
Xm.col(1) = x;
Mat Ym = C * Xm;
VERIFY_IS_APPROX(Ym.col(0).eval(), (dense * Xm.col(0)).eval());
VERIFY(ieee_entrywise_match(Ym.col(1).eval(), reference_product_ieee(dense, Vec(Xm.col(1)))));
// A zero column carrying a single NaN must not take the zero-column shortcut:
// the fast-max routing scan can miss a NaN among zeros (an Inf always
// surfaces), so the shortcut rechecks exactly and such a column falls back.
Vec xz = Vec::Zero(N);
xz[0] = Scalar(nan);
VERIFY(ieee_entrywise_match((C * xz).eval(), reference_product_ieee(dense, xz)));
// Inf in the generating array: the operator itself is non-finite, whatever the
// right-hand side.
Mat G2 = Mat::Random(n2, n1);
G2(n2 / 2, n1 / 2) = Scalar(-inf);
Bccb<Scalar> C2(G2);
Mat dense2 = reference_bccb<Scalar>(G2);
Vec x2 = Vec::Random(N);
VERIFY(ieee_entrywise_match((C2 * x2).eval(), reference_product_ieee(dense2, x2)));
// Non-finite right-hand sides of solve() apply the pseudo-inverse -- itself a
// BCCB operator -- through the direct kernel, so the Inf propagates entrywise
// instead of NaN-ing the whole output through the transforms. The operator is
// diagonally dominant, so no symbol entry is thresholded and the pseudo-inverse
// coincides with inverse(), whose dense form serves as the reference matrix.
Mat Gd = Mat::Random(n2, n1);
Gd(0, 0) += Scalar(RealScalar(2 * N));
Bccb<Scalar> Cd(Gd);
Mat pinv = Mat(Cd.inverse());
Vec binf = Vec::Random(N);
binf[N / 3] = Scalar(inf);
VERIFY(ieee_entrywise_match(Cd.solve(binf), reference_product_ieee(pinv, binf)));
}
// An all-zero right-hand side must not short-circuit to an exact zero when the
// operator holds non-finite data: every row of a BCCB touches every generator
// entry, so each result entry is a dot product with an Inf coefficient times
// zero -- NaN under IEEE, not zero.
void test_bccb_nonfinite_zero_rhs(Index n2, Index n1) {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const Index N = n1 * n2;
Mat G = Mat::Random(n2, n1);
G(n2 / 2, n1 / 2) = std::numeric_limits<double>::infinity();
Bccb<double> C(G);
Mat dense = reference_bccb<double>(G);
const Vec z = Vec::Zero(N);
Vec y = C * z;
VERIFY(y.hasNaN());
VERIFY(ieee_entrywise_match(y, reference_product_ieee(dense, z)));
// The same holds for solve(): the symbol of a non-finite operator holds NaN,
// so the pseudo-inverse applied to a zero right-hand side is NaN, not zero.
Vec xs = C.solve(z);
VERIFY(xs.hasNaN());
}
// Spectra with a wide dynamic range: the determinant is representable, but a
// plain product of the eigenvalues in FFT order overflows to infinity (or
// underflows to an exact zero) partway through. Pins the balanced accumulation
// in determinant().
void test_bccb_determinant_scaled() {
typedef std::complex<double> Complex;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
const Index n2 = 25, n1 = 40; // N = 1000
const Index nLead = 653;
// Symbol magnitudes `lead` on the first nLead column-major-flattened indices
// (the accumulation order of determinant()) and `rest` elsewhere, so the
// naive running product leaves the representable range partway through while
// the true determinant lead^nLead * rest^(N - nLead) is representable. The
// generator is recovered through the dense inverse 2-D DFT matrices,
// independently of the implementation under test.
auto makeOperator = [n2, n1, nLead](double lead, double rest) {
CMat S(n2, n1);
for (Index k1 = 0; k1 < n1; ++k1)
for (Index k2 = 0; k2 < n2; ++k2) S(k2, k1) = Complex(k1 * n2 + k2 < nLead ? lead : rest);
const CMat F2 = inverse_dft_matrix<double>(n2), F1 = inverse_dft_matrix<double>(n1);
return Bccb<Complex>((F2 * S * F1.transpose()).eval());
};
// The spectrum is only reproduced up to the FFT round trip's forward error,
// and the determinant multiplies ~1e3 such factors.
const double kSpectrumRoundTripTol = 1e8 * NumTraits<double>::epsilon();
{
// det = 10^653 * 10^-347 = 1e306; the naive partial product reaches 1e327.
Bccb<Complex> C = makeOperator(10.0, 0.1);
const Complex det = C.determinant();
VERIFY((numext::isfinite)(numext::abs(det)));
VERIFY(numext::abs(det / 1e306 - Complex(1)) <= kSpectrumRoundTripTol);
}
{
// det = 10^-653 * 10^347 = 1e-306; the naive partial product reaches
// 1e-327, well below the smallest subnormal, and flushes to an exact zero.
Bccb<Complex> C = makeOperator(0.1, 10.0);
const Complex det = C.determinant();
VERIFY(det != Complex(0));
VERIFY(numext::abs(det / 1e-306 - Complex(1)) <= kSpectrumRoundTripTol);
}
{
// A genuinely overflowing determinant must still saturate to infinity.
typedef Matrix<double, Dynamic, Dynamic> Mat;
Mat G = Mat::Zero(6, 6);
G(0, 0) = 1e308; // the symbol is 1e308 everywhere: det = 10^11088
Bccb<double> C(G);
VERIFY((numext::isinf)(C.determinant()));
}
}
// The rank decision at the clamped threshold (the smallest normal number): a
// smallest-normal symbol entry is still inverted -- the comparison is strict,
// matching SVDBase::rank(), which likewise reports rank one -- a subnormal entry
// is treated as an exact zero, and non-finite entries count as non-zero.
void test_bccb_rank_boundaries() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const double mn = (std::numeric_limits<double>::min)();
const Vec b = Vec::Ones(1);
{
Bccb<double> C(Mat(Mat::Constant(1, 1, mn)));
VERIFY_IS_EQUAL(C.rank(), 1);
Vec x = C.solve(b);
VERIFY((numext::isfinite)(x[0]));
VERIFY_IS_APPROX(x[0], 1.0 / mn);
}
{
Bccb<double> C(Mat(Mat::Constant(1, 1, mn / 2))); // subnormal
VERIFY_IS_EQUAL(C.rank(), 0);
VERIFY(C.solve(b).isZero());
}
{
Bccb<double> C(Mat(Mat::Constant(1, 1, std::numeric_limits<double>::infinity())));
VERIFY_IS_EQUAL(C.rank(), 1);
VERIFY((numext::isinf)(C.determinant()));
}
}
// A finite complex symbol entry near the overflow threshold has a
// non-representable modulus. The rank threshold used to be computed from the raw
// moduli, turning it into infinity: the rank was under-reported and solve()
// zeroed valid Fourier modes. Both are now evaluated in an exactly rescaled
// frame.
void test_bccb_rank_complex_boundary() {
typedef std::complex<double> Complex;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
const double mx = (std::numeric_limits<double>::max)();
// n2 = 2, n1 = 1: the symbol is exactly [g0 + g1, g0 - g1], so pick the
// generator from the desired spectrum. |s0| overflows while both of its
// components are finite.
const Complex s0(0.75 * mx, 0.75 * mx), s1(1e300, 0.0);
CMat G(2, 1);
G(0, 0) = (s0 + s1) * 0.5;
G(1, 0) = (s0 - s1) * 0.5;
Bccb<Complex> C(G);
VERIFY_IS_EQUAL(C.rank(), 2);
// The second Fourier mode must be inverted, not zeroed: a product of a small
// vector solves back to that vector (the accuracy is limited by the condition
// number |s0| / |s1| ~ 2.5e8).
CVec x0(2);
x0[0] = Complex(1e-10, -2e-10);
x0[1] = Complex(-3e-10, 1e-10);
CVec b = C * x0;
VERIFY(b.allFinite());
CVec x = C.solve(b);
VERIFY(((x - x0).cwiseAbs().maxCoeff() / x0.cwiseAbs().maxCoeff()) <= 1e-6);
// A genuinely negligible second entry still truncates in the scaled frame.
CMat G2(2, 1);
G2(0, 0) = (s0 + Complex(1)) * 0.5;
G2(1, 0) = (s0 - Complex(1)) * 0.5;
VERIFY_IS_EQUAL(Bccb<Complex>(G2).rank(), 1);
}
// A single 2^-e frame factor is itself subnormal once the frame exceeds the
// exponent range, and reads as zero under flush-to-zero: every scaled modulus
// and the threshold collapse together, the rank is over-reported, and solve()
// inverts a mode it should have truncated. Hence the two exact factors.
void test_bccb_rank_flush_to_zero() {
ScopedFlushToZero flush_to_zero;
if (!flush_to_zero.isSupported()) return;
using Complex = std::complex<double>;
using CMat = Matrix<Complex, Dynamic, Dynamic>;
const double mx = (std::numeric_limits<double>::max)();
// Spectrum [s0, 1] with |s0| at the overflow boundary: the second mode is
// negligible against it, so the operator is rank one.
const Complex s0(0.75 * mx, 0.75 * mx);
CMat G(2, 1);
G(0, 0) = (s0 + Complex(1)) * 0.5;
G(1, 0) = (s0 - Complex(1)) * 0.5;
VERIFY_IS_EQUAL(Bccb<Complex>(G).rank(), 1);
}
template <typename Scalar>
void test_bccb_eigen(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
CMat denseC = reference_bccb<Scalar>(G).template cast<Complex>();
const Index N = n1 * n2;
Matrix<Complex, Dynamic, 1> lam = C.eigenvalues();
CMat V = C.eigenvectors();
VERIFY_IS_APPROX((denseC * V).eval(), (V * lam.asDiagonal()).eval());
VERIFY_IS_APPROX((V.adjoint() * V).eval(), CMat(CMat::Identity(N, N)));
}
template <typename Scalar>
void test_bccb_svd(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
Mat G = Mat::Random(n2, n1);
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
Matrix<RealScalar, Dynamic, 1> sv = C.singularValues();
JacobiSVD<Mat> svd(dense);
VERIFY_IS_APPROX(sv, svd.singularValues());
CMat U = C.matrixU(), V = C.matrixV();
VERIFY_IS_APPROX((U * sv.template cast<Complex>().asDiagonal() * V.adjoint()).eval(),
CMat(dense.template cast<Complex>()));
VERIFY_IS_APPROX((U.adjoint() * U).eval(), CMat(CMat::Identity(N, N)));
VERIFY_IS_APPROX((V.adjoint() * V).eval(), CMat(CMat::Identity(N, N)));
}
template <typename Scalar>
void test_bccb_inverse(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
G(0, 0) += Scalar(RealScalar(2 * n1 * n2)); // safely invertible, tame determinant scale
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
const Index N = n1 * n2;
Mat inv = C.inverse();
VERIFY_IS_APPROX((inv * dense).eval(), Mat(Mat::Identity(N, N)));
Vec b = Vec::Random(N);
VERIFY_IS_APPROX((C.inverse() * b).eval(), C.solve(b));
}
template <typename Scalar>
void test_bccb_determinant(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Mat G = Mat::Random(n2, n1);
G(0, 0) += Scalar(RealScalar(2 * n1 * n2));
Bccb<Scalar> C(G);
Mat dense = reference_bccb<Scalar>(G);
VERIFY_IS_APPROX(C.determinant(), dense.determinant());
}
template <typename Scalar>
void test_bccb_matrix_free_cg(Index n2, Index n1) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Symmetrizing the generator under the two-dimensional index reversal makes
// the BCCB matrix symmetric; diagonal dominance then makes it positive
// definite.
Mat G = Mat::Random(n2, n1);
Mat Gs = G;
for (Index k1 = 0; k1 < n1; ++k1)
for (Index k2 = 0; k2 < n2; ++k2) Gs(k2, k1) = (G(k2, k1) + G((n2 - k2) % n2, (n1 - k1) % n1)) / Scalar(2);
Gs(0, 0) += Scalar(RealScalar(2 * n1 * n2));
Bccb<Scalar> C(Gs);
Mat dense = reference_bccb<Scalar>(Gs);
VERIFY_IS_APPROX(dense, Mat(dense.transpose()));
const Index N = n1 * n2;
Vec b = Vec::Random(N);
ConjugateGradient<Bccb<Scalar>, Lower | Upper, IdentityPreconditioner> cg;
cg.compute(C);
Vec x = cg.solve(b);
VERIFY(cg.info() == Success);
VERIFY_IS_APPROX((dense * x).eval(), b);
}
template <typename Scalar, int N2, int N1>
void test_bccb_fixed() {
typedef Matrix<Scalar, N2, N1> GenMat;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
GenMat G = GenMat::Random();
Bccb<Scalar, N2, N1> C(G);
STATIC_CHECK((Bccb<Scalar, N2, N1>::RowsAtCompileTime == N1 * N2));
STATIC_CHECK((internal::remove_all_t<decltype(makeBccb(G))>::RowsAtCompileTime == N1 * N2));
Mat dense = reference_bccb<Scalar>(Mat(G));
Matrix<Scalar, N1 * N2, N1 * N2> Cd = C;
VERIFY_IS_APPROX(Mat(Cd), dense);
Matrix<Scalar, N1 * N2, 1> x = Matrix<Scalar, N1 * N2, 1>::Random();
Matrix<Scalar, N1 * N2, 1> y = C * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
}
EIGEN_DECLARE_TEST(structured_bccb) {
for (int i = 0; i < g_repeat; ++i) {
// Products, dense assignment, coefficient access: scalar tier (N <= 32) and
// 2-D FFT tier, including single-row/column-of-blocks degenerate shapes.
CALL_SUBTEST_1((test_bccb_product<double>(1, 1)));
CALL_SUBTEST_1((test_bccb_product<double>(3, 4)));
CALL_SUBTEST_1((test_bccb_product<double>(5, 5)));
CALL_SUBTEST_1((test_bccb_product<double>(8, 6)));
CALL_SUBTEST_1((test_bccb_product<double>(7, 5))); // odd prime dimensions
CALL_SUBTEST_1((test_bccb_product<double>(1, 40)));
CALL_SUBTEST_1((test_bccb_product<double>(40, 1)));
CALL_SUBTEST_1((test_bccb_product<float>(6, 8)));
CALL_SUBTEST_1((test_bccb_product<std::complex<double>>(4, 3)));
CALL_SUBTEST_1((test_bccb_product<std::complex<double>>(9, 6)));
CALL_SUBTEST_1((test_bccb_product<std::complex<float>>(6, 7)));
// Transposition family with exact symbol round trips.
CALL_SUBTEST_2((test_bccb_transpose<double>(3, 4)));
CALL_SUBTEST_2((test_bccb_transpose<double>(8, 6)));
CALL_SUBTEST_2((test_bccb_transpose<double>(1, 12)));
CALL_SUBTEST_2((test_bccb_transpose<std::complex<double>>(6, 8)));
CALL_SUBTEST_2((test_bccb_transpose<std::complex<float>>(5, 7)));
// Direct and pseudo-inverse solves, degenerate operators.
CALL_SUBTEST_3((test_bccb_solve<double>(1, 1)));
CALL_SUBTEST_3((test_bccb_solve<double>(6, 8)));
CALL_SUBTEST_3((test_bccb_solve<float>(5, 6)));
CALL_SUBTEST_3((test_bccb_solve<std::complex<double>>(6, 6)));
CALL_SUBTEST_3((test_bccb_rank_deficient<std::complex<double>>(6, 8, 4)));
CALL_SUBTEST_3((test_bccb_rank_deficient<std::complex<float>>(4, 5, 2)));
CALL_SUBTEST_3((test_bccb_zero<double>(4, 5)));
CALL_SUBTEST_3(test_bccb_nan_propagation(6, 7));
// Closed-form eigendecomposition, SVD, inverse, determinant.
CALL_SUBTEST_4((test_bccb_eigen<double>(4, 5)));
CALL_SUBTEST_4((test_bccb_eigen<double>(1, 8)));
CALL_SUBTEST_4((test_bccb_eigen<std::complex<double>>(3, 5)));
CALL_SUBTEST_4((test_bccb_svd<double>(4, 5)));
CALL_SUBTEST_4((test_bccb_svd<std::complex<double>>(4, 4)));
CALL_SUBTEST_4((test_bccb_svd<float>(3, 4)));
CALL_SUBTEST_4((test_bccb_inverse<double>(6, 8)));
CALL_SUBTEST_4((test_bccb_inverse<std::complex<double>>(5, 5)));
CALL_SUBTEST_4((test_bccb_determinant<double>(3, 4)));
CALL_SUBTEST_4((test_bccb_determinant<std::complex<double>>(3, 3)));
// Matrix-free iterative solve and fixed-size generators.
CALL_SUBTEST_5((test_bccb_matrix_free_cg<double>(8, 10)));
CALL_SUBTEST_5((test_bccb_fixed<double, 3, 4>()));
CALL_SUBTEST_5((test_bccb_fixed<std::complex<float>, 4, 3>()));
// Non-5-smooth (prime) block dimensions: the padded product-embedding grid,
// in each axis combination (n2 awkward, n1 awkward, both).
CALL_SUBTEST_5((test_bccb_prime_dimensions<double>(7, 6)));
CALL_SUBTEST_5((test_bccb_prime_dimensions<double>(6, 7)));
CALL_SUBTEST_5((test_bccb_prime_dimensions<double>(7, 7)));
CALL_SUBTEST_5((test_bccb_prime_dimensions<std::complex<double>>(11, 7)));
CALL_SUBTEST_5((test_bccb_prime_dimensions<float>(7, 6)));
// Product lifetime, aliasing, mixed-scalar promotion, dimension mismatches,
// across the scalar-loop and FFT dispatch tiers.
CALL_SUBTEST_6((test_bccb_delayed_product<double>(4, 5)));
CALL_SUBTEST_6((test_bccb_delayed_product<std::complex<double>>(6, 8)));
CALL_SUBTEST_6((test_bccb_aliased_product<double>(3, 4)));
CALL_SUBTEST_6((test_bccb_aliased_product<double>(8, 6)));
CALL_SUBTEST_6((test_bccb_aliased_product<std::complex<double>>(6, 8)));
CALL_SUBTEST_6((test_bccb_aliased_expression<double>(3, 4)));
CALL_SUBTEST_6((test_bccb_aliased_expression<double>(6, 8)));
CALL_SUBTEST_6((test_bccb_aliased_expression<std::complex<double>>(6, 8)));
CALL_SUBTEST_6((test_bccb_mixed_scalar<double>(3, 4)));
CALL_SUBTEST_6((test_bccb_mixed_scalar<double>(6, 8)));
CALL_SUBTEST_6((test_bccb_mixed_scalar<float>(8, 8)));
CALL_SUBTEST_6(test_bccb_dimension_asserts());
// Finite-range robustness: scaled transforms, the complex overflow boundary,
// balanced determinant accumulation, and the rank threshold boundary.
CALL_SUBTEST_7(test_bccb_finite_overflow());
CALL_SUBTEST_7((test_bccb_fft_complex_boundary<double>(6, 8)));
CALL_SUBTEST_7((test_bccb_fft_complex_boundary<float>(6, 8)));
CALL_SUBTEST_7(test_bccb_determinant_scaled());
CALL_SUBTEST_7(test_bccb_rank_boundaries());
CALL_SUBTEST_7(test_bccb_rank_complex_boundary());
CALL_SUBTEST_7(test_bccb_rank_flush_to_zero());
// Entrywise Inf/NaN propagation: FFT-sized operators must fall back to the
// direct kernel; small ones are IEEE-exact already. A zero right-hand side
// under a non-finite operator yields NaN, not zero.
CALL_SUBTEST_7((test_bccb_nonfinite_product<double>(6, 8)));
CALL_SUBTEST_7((test_bccb_nonfinite_product<double>(3, 4)));
CALL_SUBTEST_7((test_bccb_nonfinite_product<std::complex<double>>(6, 8)));
CALL_SUBTEST_7(test_bccb_nonfinite_zero_rhs(6, 8));
}
}