diff --git a/include/dca/phys/dca_step/symmetrization/solve_orbital_op_signs.hpp b/include/dca/phys/dca_step/symmetrization/solve_orbital_op_signs.hpp index 80e602c55..f40ffb39f 100644 --- a/include/dca/phys/dca_step/symmetrization/solve_orbital_op_signs.hpp +++ b/include/dca/phys/dca_step/symmetrization/solve_orbital_op_signs.hpp @@ -9,9 +9,12 @@ // Derives the orbital-operation matrix U_S of each point-group operation from H0(k) and populates // cluster_symmetry::get_orbital_op(), without reading hand-coded Lattice::transformationSignOf*. // -// Scope: for every analytic model, U_S is a real signed permutation U_S = D P, where the -// permutation P is already encoded in the symmetry table (the band image .second) and the only -// unknown is the diagonal of +/-1 signs D = diag(sigma). +// Scope: for every analytic model, U_S is a real signed permutation U_S = D P. Both halves are +// derived from H0: the permutation P (which orbital maps to which) and the diagonal of +/-1 signs +// D = diag(sigma). The symmetry table's band image (.second) is only the first candidate for P; +// when H0 rejects it -- geometry cannot see an on-site orbital rotation, such as the dxz/dyz swap +// under C4 of two orbitals sharing a_vec = 0 -- the remaining permutations are searched. n_b <= 3 +// for every shipped model, so that is at most 3! = 6 candidates. // // The symmetry table stores not the exact momentum image S k but its folded representative // k_new = S k - G, and with intra-cell orbital offsets a_b the fold is not free: H0 picks up the @@ -27,22 +30,25 @@ // the odd parity of p_x under a mirror at a zone-boundary momentum) instead of the intrinsic // orbital transform. // -// The analytic models shipped with DCA++ have at most n_b = 3 orbitals, so once the gauge -// sigma(0) = +1 is fixed there are at most 2^(n_b-1) <= 4 candidate sign vectors. We simply -// enumerate them and keep the one that satisfies every coupling constraint. Four conditions make -// an operation fail: +// For a candidate permutation, the signs are found by enumerating the at most 2^(n_b-1) <= 4 +// vectors with the gauge sigma(0) = +1 and keeping the one that satisfies every constraint. +// A malformed candidate (an out-of-range or repeated band image -- only the geometric one can be) +// is rejected up front; an admissible one is rejected, and the next tried, when: // -// * a missing band image in the symmetry table (the -1 value recorded by set_symmetry_matrices -// when it finds no admissible image) means the op has no candidate permutation at all; -// * a magnitude mismatch (an entry vanishing on only one side) means the permutation is not a -// symmetry of H0; -// * a non-real or non-unit ratio means the op needs a non-signed-permutation U_S (genuine orbital -// mixing, not yet supported) or is not a symmetry; -// * no candidate sign vector satisfying all couplings means no signed-permutation U_S exists. +// * a coupling vanishes on only one side (a magnitude mismatch -- the permutation is not a +// symmetry of H0) +// * a fold-corrected ratio is not a real +/-1 (this permutation would need a +// non-signed-permutation U_S) +// * no sign vector satisfies all couplings +// +// Only when every band permutation is rejected does the operation fail outright: it then needs a +// genuinely non-signed-permutation U_S (orbital mixing or a complex phase out of scope) or is not +// a symmetry of H0. #ifndef DCA_PHYS_DCA_STEP_SYMMETRIZATION_SOLVE_ORBITAL_OP_SIGNS_HPP #define DCA_PHYS_DCA_STEP_SYMMETRIZATION_SOLVE_ORBITAL_OP_SIGNS_HPP +#include #include #include #include @@ -67,37 +73,25 @@ constexpr double kCouplingZeroTol = 1e-10; // magnitude from 1 must both be below this. constexpr double kSignRealTol = 1e-9; -// Solves the +/-1 signs of one operation s and writes that op's U_S block into u_s. fold_phase is -// the diagonal folding phase phi (callable as fold_phase(k, band, s)) that undresses the folded -// H0 samples, so the solved signs are the intrinsic orbital transform. Throws if op s is not a -// signed-permutation symmetry of H0. Shared by the whole-group populator -- which lets the throw -// propagate, since a model must be a genuine symmetry group -- and by the group verification, -// which catches it to classify the op. +// Tests one candidate band permutation `image` against H0. Returns true, and writes that op into +// u_s, iff `image` is a signed-permutation symmetry of H0: every nonzero coupling gives a real +// +/-1 (fold-corrected) ratio, and some sign vector satisfies the resulting products. template -void solveSignsForOp(int s, int nb, int nk, const SymFunc& sym, const FoldFunc& fold_phase, - const H0Function& H0, UFunc& u_s) { - // Band permutation P: row b's single entry lands in column image[b]. This is the geometry-derived - // half of U_S (which orbital maps to which); it is k-independent for a point-group op, so read it - // at k = 0. Only the signs that decorate it are unknown. - std::vector image(nb); +bool tryPermutation(const std::vector& image, int s, int nb, int nk, const SymFunc& sym, + const FoldFunc& fold_phase, const H0Function& H0, UFunc& u_s) { + // Reject a malformed candidate before it indexes H0: an out-of-range entry (the -1 recorded when + // set_symmetry_matrices finds no admissible image) or a repeated one (two orbitals sharing a site + // and a flavor collide). + std::vector is_hit(nb, false); for (int b = 0; b < nb; ++b) { - image[b] = sym(0, b, s).second; - // set_symmetry_matrices records a silent -1 sentinel when its position/flavor matching finds - // no admissible image (its own throw is commented out), so the table cannot be assumed - // complete here. Reject the op before the sentinel is used to index H0. - if (image[b] < 0 || image[b] >= nb) - throw std::out_of_range( - "solveOrbitalOpSignsFromH0: no band image in the symmetry table for band " + - std::to_string(b) + " under op " + std::to_string(s) + - " -- the geometric position/flavor matching found no admissible image, so the op has no " - "candidate permutation."); + if (image[b] < 0 || image[b] >= nb || is_hit[image[b]]) + return false; + is_hit[image[b]] = true; } // Gather one sign-product constraint per nonzero coupling, over all cluster momenta. Each nonzero - // coupling forces sigma(b0) sigma(b1) to the +/-1 fold-corrected ratio of the two H0 entries. The - // ratio must be a real +/-1 for a signed permutation to exist at all; the two checks below - // (magnitude match, real +/-1) are independent of the signs themselves, so they are tested here - // and reported as the first two rejection branches. + // coupling forces sigma(b0) sigma(b1) to the +/-1 fold-corrected ratio of the two H0 entries; the + // ratio must be a real +/-1 for a signed permutation to exist under this permutation at all. struct Constraint { int b0, b1, product; }; @@ -114,10 +108,7 @@ void solveSignsForOp(int s, int nb, int nk, const SymFunc& sym, const FoldFunc& if (zero_lhs && zero_rhs) continue; // coupling absent on both sides: consistent, but no constraint. if (zero_lhs != zero_rhs) - throw std::logic_error( - "solveOrbitalOpSignsFromH0: H0 magnitude mismatch for op " + std::to_string(s) + - " -- the band permutation is not a symmetry of H0 (a coupling vanishes on only one " - "side)."); + return false; // magnitude mismatch: this permutation is not a symmetry of H0. // Undress the fold before taking the ratio. phi lives at the image bands -- the rhs entry // sits at (image(b0), image(b1)) -- with G determined by (k, s); the stored table carries // the band as a free index, so the image-band lookup is direct. @@ -125,10 +116,7 @@ void solveSignsForOp(int s, int nb, int nk, const SymFunc& sym, const FoldFunc& const std::complex ratio = lhs / (fold * rhs); if (std::abs(std::imag(ratio)) > kSignRealTol || std::abs(std::abs(ratio) - 1.) > kSignRealTol) - throw std::domain_error( - "solveOrbitalOpSignsFromH0: H0 ratio is not a real +/-1 for op " + std::to_string(s) + - " -- the operation requires a non-signed-permutation U_S (genuine orbital mixing) or " - "is not a symmetry of H0."); + return false; // non-signed-permutation ratio: this permutation would need a dense U_S. constraints.push_back({b0, b1, std::real(ratio) > 0. ? 1 : -1}); } @@ -152,15 +140,44 @@ void solveSignsForOp(int s, int nb, int nk, const SymFunc& sym, const FoldFunc& break; } } - // No sign vector satisfied every constraint, so no signed permutation reproduces H0 under this op if (!found) - throw std::logic_error( - "solveOrbitalOpSignsFromH0: no consistent +/-1 signs for op " + std::to_string(s) + - " -- the H0 couplings admit no signed-permutation U_S."); + return false; // no sign vector reproduces H0 under this permutation. // Materialize U_S = D P one entry at a time: sign sigma(b) at (row b, column image[b]). for (int b = 0; b < nb; ++b) u_s(b, image[b], s) = static_cast(sigma[b]); + return true; +} + +// Derives op s's orbital operation U_S = D P from H0 -- both the permutation P and the signs D -- +// and writes it into u_s. H0 is the authority: U_S is whatever signed permutation makes +// H0(S k) = U_S H0(k) U_S^dagger hold. +template +void deriveOrbitalOpForOp(int s, int nb, int nk, const SymFunc& sym, const FoldFunc& fold_phase, + const H0Function& H0, UFunc& u_s) { + // The geometric image is the canonical candidate: verify it against H0 first. + std::vector geometric(nb); + for (int b = 0; b < nb; ++b) + geometric[b] = sym(0, b, s).second; + + if (tryPermutation(geometric, s, nb, nk, sym, fold_phase, H0, u_s)) + return; + + // H0 did not admit it; check the remaining permutations, in std::next_permutation order so the + // search is deterministic, skipping the geometric candidate already tried. + std::vector image(nb); + for (int b = 0; b < nb; ++b) + image[b] = b; + do { + if (image != geometric && tryPermutation(image, s, nb, nk, sym, fold_phase, H0, u_s)) + return; + } while (std::next_permutation(image.begin(), image.end())); + + throw std::logic_error( + "solveOrbitalOpSignsFromH0: no signed-permutation U_S reproduces H0 under op " + + std::to_string(s) + + " for any band permutation -- the operation needs a non-signed-permutation U_S (genuine " + "orbital mixing or a complex phase) or is not a symmetry of H0."); } } // namespace detail @@ -185,7 +202,7 @@ void solveOrbitalOpSignsFromH0(const H0Function& H0) { const int n_ops = SymDmn::dmn_size(); for (int s = 0; s < n_ops; ++s) - detail::solveSignsForOp(s, nb, nk, sym, fold_phase, H0, u_s); + detail::deriveOrbitalOpForOp(s, nb, nk, sym, fold_phase, H0, u_s); } // Returns the sorted op indices that pass the sign-consistency (H0-invariance) check -- the @@ -210,7 +227,7 @@ std::vector verifiedSymmetryOps(const H0Function& H0) { std::vector verified; for (int s = 0; s < n_ops; ++s) { try { - detail::solveSignsForOp(s, nb, nk, sym, fold_phase, H0, u_s); + detail::deriveOrbitalOpForOp(s, nb, nk, sym, fold_phase, H0, u_s); verified.push_back(s); } catch (const std::exception&) { diff --git a/test/integration/phys/symmetrization/CMakeLists.txt b/test/integration/phys/symmetrization/CMakeLists.txt index 7653c0dea..07d66c8a6 100644 --- a/test/integration/phys/symmetrization/CMakeLists.txt +++ b/test/integration/phys/symmetrization/CMakeLists.txt @@ -22,8 +22,8 @@ dca_add_gtest(symmetrize_characterization_threeband_D4_test TEST_DEFINES ThreebandD4 ) -# Regression case for solveSignsForOp's -1 guard: Kagome produces the -1 which the solver -# must reject instead of indexing H0 out of bounds. +# Regression case for deriveOrbitalOpForOp's -1 guard: Kagome produces the -1 which the solver +# must skip instead of indexing H0 out of bounds. dca_add_gtest(symmetrize_characterization_kagome_test FAST GTEST_MPI_MAIN @@ -33,6 +33,17 @@ dca_add_gtest(symmetrize_characterization_kagome_test TEST_DEFINES KagomeNoSym ) +# The motivating case for deriving P from H0: FeAs's two orbitals share a site, so the geometric +# band image is always the identity and half of D4 is recoverable only by searching for the swap. +dca_add_gtest(symmetrize_characterization_fe_as_test + FAST + GTEST_MPI_MAIN + INCLUDE_DIRS ${DCA_INCLUDES};${PROJECT_SOURCE_DIR};${FFTW_INCLUDE_DIR} + LIBS ${DCA_LIBS} + CUSTOM_SOURCE symmetrize_characterization_test.cpp + TEST_DEFINES FeAs + ) + dca_add_gtest(symmetrize_characterization_singleband_chain_test FAST GTEST_MPI_MAIN diff --git a/test/integration/phys/symmetrization/fe_as_input.json b/test/integration/phys/symmetrization/fe_as_input.json new file mode 100644 index 000000000..2be024ef7 --- /dev/null +++ b/test/integration/phys/symmetrization/fe_as_input.json @@ -0,0 +1,37 @@ +{ + "physics": { + "beta" : 1., + "chemical-potential" : 0. + }, + + "FeAs-model": + { + "t1" : -1., + "t2" : 1.3, + "t3" : -0.85, + "t4" : -0.85, + "U" : 4., + "V" : 2., + "J" : 0.5, + "Jp" : 0.5 + }, + + "domains": { + "real-space-grids": { + "cluster": [[4, 0], [0, 4]], + "sp-host": [[8, 0], [0, 8]] + }, + + "imaginary-time": { + "sp-time-intervals": 128 + }, + + "imaginary-frequency": { + "sp-fermionic-frequencies": 64 + } + }, + + "DCA": { + "interacting-orbitals": [0, 1] + } +} diff --git a/test/integration/phys/symmetrization/symmetrize_characterization_test.cpp b/test/integration/phys/symmetrization/symmetrize_characterization_test.cpp index 495ab4729..80d7f75b3 100644 --- a/test/integration/phys/symmetrization/symmetrize_characterization_test.cpp +++ b/test/integration/phys/symmetrization/symmetrize_characterization_test.cpp @@ -64,6 +64,7 @@ #include "dca/phys/parameters/parameters.hpp" #include "dca/profiling/null_profiler.hpp" #include "dca/phys/models/analytic_hamiltonians/Kagome_hubbard.hpp" +#include "dca/phys/models/analytic_hamiltonians/fe_as_lattice.hpp" #include "dca/phys/models/analytic_hamiltonians/singleband_chain.hpp" #include "dca/phys/models/analytic_hamiltonians/square_lattice.hpp" #include "dca/phys/models/analytic_hamiltonians/threeband_hubbard.hpp" @@ -150,10 +151,10 @@ struct SinglebandChain { // CASE 4 -- Kagome lattice declared with no_symmetry<2>, mirroring the production // instantiation in test/unit/phys/dca_step/cluster_solver/test_setup.hpp. This is the -// regression case for solveSignsForOp's -1 guard: the hexagonal has the 12 D6 ops, -// but the orbital position+flavor matching in set_symmetry_matrices filters them all out, -// and records (-1, -1). The H0 sign solver must reject the ops instead of indexing H0 with -// band -1 (an OOB). +// regression case for deriveOrbitalOpForOp's -1 guard: the hexagonal has the 12 D6 ops, +// but the orbital position+flavor matching in set_symmetry_matrices filters them all out +// and records (-1, -1). Deriving P from H0 recovers all 12 -- the search skips the -1 +// candidate and finds the sublattice permutation geometry could not place. struct KagomeNoSym { using Scalar = double; using Lattice = dca::phys::models::KagomeHubbard>; @@ -163,7 +164,26 @@ struct KagomeNoSym { static std::vector expectedFailingKOps() { return {}; } static std::vector expectedFailingROps() { return {}; } static std::vector expectedUnverifiedKOps() { return {}; } - static constexpr int expected_num_derived_symmetries = 2; + static constexpr int expected_num_derived_symmetries = 12; +}; + +// CASE 5 -- FeAs (minimal two-band iron-pnictide model, PRB 77, 220503): the motivating case for +// deriving P from H0. Its two orbitals (d_xz, d_yz) share a site (a_vec = 0 for both), so +// position/flavor matching can only return the IDENTITY band image. But H0's two diagonals are the +// same dispersion with k_x and k_y exchanged, so the four D4 ops exchanging the axes (C4, C4^3, the +// two diagonal mirrors) are symmetries only with the band SWAP: geometry derives 4 ops, H0 all 8. +struct FeAs { + using Scalar = double; + // FeAsLattice ignores its template argument: it hardcodes FeAsPointGroup = {identity, C2}, the + // largest group the geometry-derived permutation could support. + using Lattice = dca::phys::models::FeAsLattice; + static constexpr char Input[] = "fe_as_input.json"; + static constexpr int expected_num_symmetries = 2; + static std::vector expectedFailingReps() { return {}; } + static std::vector expectedFailingKOps() { return {}; } + static std::vector expectedFailingROps() { return {}; } + static std::vector expectedUnverifiedKOps() { return {}; } + static constexpr int expected_num_derived_symmetries = 8; }; // Templated test fixture @@ -673,25 +693,21 @@ TYPED_TEST(SymmetrizeCharacterizationTest, PerOpMapRealSpace) { // TEST 8 (a/b/c/d): rejection fixtures for the sign-consistency solver. // -// solveOrbitalOpSignsFromH0 has four throw branches. No shipped model exercises any of the -// four on a correct H0 and a complete symmetry table -- the spine cases all pass -- so we -// perturb a copy of the (exactly symmetric) H0 (8a-8c), or the table itself (8d), to break one -// operation and assert the solver throws for the intended reason. +// solveOrbitalOpSignsFromH0 derives the permutation from H0, throwing only when no band +// permutation reproduces it. No shipped model triggers that on a correct H0 -- the spine cases all +// pass -- so we perturb a copy of the (exactly symmetric) H0 (8a-8c) to break one op past every +// permutation, or corrupt the table (8d) to show the search recovers what geometry cannot place. // // These need a multi-orbital model. A single band has no off-diagonal coupling to corrupt, // and on the spine's square cluster every nonzero single-band entry sits at a k that is a // fixed point of every op (the BZ center and corner), so no single-entry perturbation can // break a symmetry there. Threeband (d, px, py) is the driver; square and singleband skip. -// 8a/8b -- the two ways a single broken off-diagonal coupling is rejected. Both perturb the -// strongest off-diagonal H0 entry (and its Hermitian partner) so its symmetry image no longer -// matches, differing only in how: -// * vanish it (-> magnitude mismatch: a coupling present on one side of the relation but gone on -// the other, so the band permutation is no longer a symmetry of H0); -// * scale it by 1.5 (-> non-unit ratio: the H0 ratio that would fix the sign product is no longer -// a real +/-1, the signature of an op needing a non-signed-permutation U_S -- genuine orbital -// mixing -- or simply not a symmetry). -// They share everything but the perturbation, so they live in one test with two assertion blocks. +// 8a/8b -- two ways a single broken off-diagonal coupling breaks the op past any permutation. Both +// perturb the strongest off-diagonal H0 entry (and its Hermitian partner), differing only in how: +// * vanish it (a coupling present on one side of the relation but gone on the other); +// * scale it by 1.5 (the H0 ratio that would fix a sign product is no longer a real +/-1). +// Either way the search exhausts every permutation and throws the unified message. TYPED_TEST(SymmetrizeCharacterizationTest, RejectsBrokenOffDiagonalCoupling) { using Fixture = SymmetrizeCharacterizationTest; using KCluster = typename Fixture::KCluster; @@ -737,7 +753,7 @@ TYPED_TEST(SymmetrizeCharacterizationTest, RejectsBrokenOffDiagonalCoupling) { const std::string msg = captureThrowMessage([&] { dca::phys::solveOrbitalOpSignsFromH0(H0p); }); ASSERT_FALSE(msg.empty()) << "sign solver did not reject a vanished coupling."; - EXPECT_NE(msg.find("magnitude mismatch"), std::string::npos) << "actual: " << msg; + EXPECT_NE(msg.find("for any band permutation"), std::string::npos) << "actual: " << msg; } // Scale by 1.5: the entry stays nonzero but its ratio to the image is not +/-1. @@ -748,7 +764,7 @@ TYPED_TEST(SymmetrizeCharacterizationTest, RejectsBrokenOffDiagonalCoupling) { const std::string msg = captureThrowMessage([&] { dca::phys::solveOrbitalOpSignsFromH0(H0p); }); ASSERT_FALSE(msg.empty()) << "sign solver did not reject a non-unit coupling ratio."; - EXPECT_NE(msg.find("not a real"), std::string::npos) << "actual: " << msg; + EXPECT_NE(msg.find("for any band permutation"), std::string::npos) << "actual: " << msg; } } @@ -756,7 +772,8 @@ TYPED_TEST(SymmetrizeCharacterizationTest, RejectsBrokenOffDiagonalCoupling) { // jointly satisfied by any assignment. At the zone corner all three bands couple (d-px, d-py, // px-py), so the d-px and d-py signs force the px-py sign product, and a px<->py swap op // reads the px-py sign off H0 directly. Flipping one direction of the px-py coupling -// makes the direct reading contradict the forced product: no sign vector works. +// makes the direct reading contradict the forced product: no sign vector works -- and, because the +// flip is asymmetric, no other band permutation rescues it either, so the search still throws. TYPED_TEST(SymmetrizeCharacterizationTest, RejectsInconsistentSigns) { using Fixture = SymmetrizeCharacterizationTest; using KCluster = typename Fixture::KCluster; @@ -798,7 +815,7 @@ TYPED_TEST(SymmetrizeCharacterizationTest, RejectsInconsistentSigns) { const std::string msg = captureThrowMessage([&] { dca::phys::solveOrbitalOpSignsFromH0(H0p); }); ASSERT_FALSE(msg.empty()) << "sign solver did not reject an inconsistent sign system."; - EXPECT_NE(msg.find("no consistent"), std::string::npos) << "actual: " << msg; + EXPECT_NE(msg.find("for any band permutation"), std::string::npos) << "actual: " << msg; } // TEST 9: the folding phase must not be absorbed into U_S (PR #368 review). At a zone-boundary @@ -847,19 +864,18 @@ TEST(SolveOrbitalOpSigns, BoundaryFoldDoesNotMaskPxParity) { }; USTable u_s; - dca::phys::detail::solveSignsForOp(0, 3, 1, Sym{}, Fold{}, H0{}, u_s); + dca::phys::detail::deriveOrbitalOpForOp(0, 3, 1, Sym{}, Fold{}, H0{}, u_s); EXPECT_DOUBLE_EQ(u_s(0, 0, 0), 1.); EXPECT_DOUBLE_EQ(u_s(1, 1, 0), -1.); // The intrinsic p_x parity, not the fold-dressed +1. EXPECT_DOUBLE_EQ(u_s(2, 2, 0), 1.); } -// 8d -- missing band image: the geometric position/flavor matching in set_symmetry_matrices -// records -1 when it finds no admissible image, so the solver cannot assume a complete table. -// Unlike 8a-8c this perturbs the symmetry table, not H0: the whole-group populator must throw, -// and the non-throwing classifier must omit the op instead of indexing H0 with -1 (an OOB). The -// table is shared static state, so it gets restored. -TYPED_TEST(SymmetrizeCharacterizationTest, RejectsMissingBandImage) { +// 8d -- recovery from a missing band image: set_symmetry_matrices records -1 when its position/ +// flavor matching finds no admissible image. Unlike 8a-8c this perturbs the symmetry table, not H0: +// the solver skips the -1 candidate (no OOB) and recovers the op from the search, so the populator +// succeeds and the classifier still includes it. The table is static state, so it gets restored. +TYPED_TEST(SymmetrizeCharacterizationTest, RecoversFromMissingBandImage) { using Fixture = SymmetrizeCharacterizationTest; using KCluster = typename Fixture::KCluster; @@ -871,19 +887,17 @@ TYPED_TEST(SymmetrizeCharacterizationTest, RejectsMissingBandImage) { const int saved_image = sym(0, 0, s_target).second; sym(0, 0, s_target).second = -1; + // The populator must NOT throw: the -1 makes the geometric candidate inadmissible, but the search + // should continue const std::string msg = captureThrowMessage([&] { dca::phys::solveOrbitalOpSignsFromH0(this->H0_); }); const std::vector verified = dca::phys::verifiedSymmetryOps(this->H0_); sym(0, 0, s_target).second = saved_image; - ASSERT_FALSE(msg.empty()) << "sign solver did not reject a -1 sentinel band image."; - EXPECT_NE(msg.find("no band image"), std::string::npos) << "actual: " << msg; - - std::vector expected = baseline; - expected.erase(std::find(expected.begin(), expected.end(), s_target)); - EXPECT_EQ(verified, expected) - << "classifier did not cleanly omit (only) the op with the sentinel image."; + EXPECT_TRUE(msg.empty()) << "solver failed to recover from a -1 valued band image: " << msg; + EXPECT_EQ(verified, baseline) + << "the H0 search did not recover the op whose geometric band image was corrupted."; } } // namespace