Skip to content

Commit 48cf3d9

Browse files
huaweil-nv1tnguyen
andauthored
Fix batched coefficient mismatch after sorting by degrees (#3725)
Use sorted batchedProductTerms instead of unsorted ops to get the correct coefficient for each term after sorting by degrees. Added unit tests for batched evolution with different coefficients. Signed-off-by: huaweil <huaweil@nvidia.com> Co-authored-by: Thien Nguyen <58006629+1tnguyen@users.noreply.github.com>
1 parent 5496873 commit 48cf3d9

File tree

2 files changed

+173
-2
lines changed

2 files changed

+173
-2
lines changed

runtime/nvqir/cudensitymat/CuDensityMatOpConverter.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,8 +521,10 @@ void cudaq::dynamics::CuDensityMatOpConverter::appendToCudensitymatOperator(
521521
m_deviceBuffers.emplace(staticCoefficients_d);
522522
std::vector<cudaq::scalar_operator> coeffs;
523523
coeffs.reserve(batchSize);
524-
for (const auto &hamiltonian : ops) {
525-
coeffs.emplace_back(hamiltonian[termIdx].get_coefficient());
524+
// Fix: Use sorted batchedProductTerms instead of unsorted ops to get
525+
// the correct coefficient for each term after sorting by degrees.
526+
for (const auto &productTerms : batchedProductTerms) {
527+
coeffs.emplace_back(productTerms[termIdx].get_coefficient());
526528
}
527529
cuDoubleComplex *totalCoefficients_d = static_cast<cuDoubleComplex *>(
528530
cudaq::dynamics::createArrayGpu(std::vector<std::complex<double>>(

unittests/dynamics/test_EvolveBatchedImpl.cpp

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,3 +896,172 @@ TEST(BatchedEvolveTester, checkIntermediateResultSaveNoneWithObservables) {
896896
1); // One observable (pauliZ)
897897
}
898898
}
899+
900+
// Test to reproduce coefficient mismatch bug when batched operators are sorted
901+
// by degrees but coefficients are taken from unsorted ops.
902+
// Bug: CuDensityMatOpConverter.cpp:528-529 uses ops[termIdx] instead of
903+
// batchedProductTerms[i][termIdx]
904+
TEST(BatchedEvolveTester, checkCoefficientMismatchAfterSorting) {
905+
// Use 2 qubits (degrees 0 and 1) to ensure sorting changes term order
906+
const cudaq::dimension_map dims = {{0, 2}, {1, 2}};
907+
908+
// We'll construct two Hamiltonians where terms are ordered such that sorting
909+
// by degrees will reorder them. Then we use X operators (non-diagonal) so
910+
// that different coefficients lead to measurably different evolution.
911+
//
912+
// Hamiltonian structure (before sorting):
913+
// term[0]: coeff1 * X(1) -> degrees = {1}
914+
// term[1]: coeff0 * X(0) -> degrees = {0}
915+
//
916+
// After stable_sort by degrees:
917+
// term[0]: coeff0 * X(0) -> degrees = {0}
918+
// term[1]: coeff1 * X(1) -> degrees = {1}
919+
//
920+
// Bug: coeffs[i] = ops[i][termIdx].get_coefficient() uses unsorted index
921+
// but prodTerms[i] = batchedProductTerms[i][termIdx] uses sorted index
922+
// This causes coefficient mismatch!
923+
924+
// Batch 1: H1 = f1 * X(1) + f0 * X(0) where f1=0.2, f0=0.1
925+
// After sorting: H1 = f0 * X(0) + f1 * X(1)
926+
// Batch 2: H2 = g1 * X(1) + g0 * X(0) where g1=0.4, g0=0.3
927+
// After sorting: H2 = g0 * X(0) + g1 * X(1)
928+
//
929+
// With the bug, when processing sorted term[0] (X(0)):
930+
// - prodTerms uses X(0) (correct)
931+
// - coeffs uses ops[0] (unsorted) which gives f1=0.2 for batch1, g1=0.4 for
932+
// batch2
933+
// instead of f0=0.1, g0=0.3
934+
935+
const double f0 = 0.1, f1 = 0.2;
936+
const double g0 = 0.3, g1 = 0.4;
937+
938+
// Use time-dependent callbacks to trigger the non-constant coefficient path
939+
auto make_td_coeff = [](double val) {
940+
return cudaq::scalar_operator(
941+
[val](const std::unordered_map<std::string, std::complex<double>>
942+
&parameters) { return std::complex<double>(val, 0.0); });
943+
};
944+
945+
// Hamiltonian 1: terms added in order degree1, degree0
946+
cudaq::sum_op<cudaq::matrix_handler> ham1 =
947+
make_td_coeff(2.0 * M_PI * f1) * cudaq::spin_op::x(1) +
948+
make_td_coeff(2.0 * M_PI * f0) * cudaq::spin_op::x(0);
949+
950+
// Hamiltonian 2: terms added in order degree1, degree0
951+
cudaq::sum_op<cudaq::matrix_handler> ham2 =
952+
make_td_coeff(2.0 * M_PI * g1) * cudaq::spin_op::x(1) +
953+
make_td_coeff(2.0 * M_PI * g0) * cudaq::spin_op::x(0);
954+
955+
constexpr int numSteps = 50;
956+
std::vector<double> steps = cudaq::linspace(0.0, 1.0, numSteps);
957+
cudaq::schedule schedule(steps, {"t"});
958+
959+
// Initial state: |00> = (1, 0, 0, 0) in computational basis
960+
// For 2 qubits: |00>, |01>, |10>, |11>
961+
auto initialState = cudaq::state::from_data(
962+
std::vector<std::complex<double>>{1.0, 0.0, 0.0, 0.0});
963+
964+
// Observables: Z(0) and Z(1) measured separately
965+
cudaq::sum_op<cudaq::matrix_handler> obsZ0(cudaq::spin_op::z(0));
966+
cudaq::sum_op<cudaq::matrix_handler> obsZ1(cudaq::spin_op::z(1));
967+
968+
cudaq::integrators::runge_kutta integrator(4, 0.01);
969+
auto results = cudaq::__internal__::evolveBatched(
970+
{ham1, ham2}, dims, schedule, {initialState, initialState}, integrator,
971+
{}, {obsZ0, obsZ1}, cudaq::IntermediateResultSave::ExpectationValue);
972+
973+
EXPECT_EQ(results.size(), 2);
974+
975+
// For independent X rotations on each qubit:
976+
// H = omega0 * X(0) + omega1 * X(1)
977+
// <Z(0)>(t) = cos(2 * omega0 * t)
978+
// <Z(1)>(t) = cos(2 * omega1 * t)
979+
//
980+
// Batch 1: omega0 = 2*pi*f0, omega1 = 2*pi*f1
981+
// <Z(0)>(t) = cos(4*pi*f0*t) = cos(4*pi*0.1*t)
982+
// <Z(1)>(t) = cos(4*pi*f1*t) = cos(4*pi*0.2*t)
983+
//
984+
// Batch 2: omega0 = 2*pi*g0, omega1 = 2*pi*g1
985+
// <Z(0)>(t) = cos(4*pi*g0*t) = cos(4*pi*0.3*t)
986+
// <Z(1)>(t) = cos(4*pi*g1*t) = cos(4*pi*0.4*t)
987+
//
988+
// With the bug (coefficients swapped):
989+
// Batch 1: omega0 = 2*pi*f1, omega1 = 2*pi*f0 (swapped!)
990+
// <Z(0)>(t) = cos(4*pi*0.2*t) <- wrong!
991+
// <Z(1)>(t) = cos(4*pi*0.1*t) <- wrong!
992+
993+
// Check batch 1 results
994+
{
995+
EXPECT_TRUE(results[0].expectation_values.has_value());
996+
const auto &expValsList = results[0].expectation_values.value();
997+
EXPECT_EQ(expValsList.size(), numSteps);
998+
999+
int count = 0;
1000+
for (auto expVals : expValsList) {
1001+
EXPECT_EQ(expVals.size(), 2); // Two observables
1002+
double t = steps[count];
1003+
1004+
// Expected values with CORRECT coefficients
1005+
double expectedZ0 = std::cos(4.0 * M_PI * f0 * t);
1006+
double expectedZ1 = std::cos(4.0 * M_PI * f1 * t);
1007+
1008+
// What we'd get with WRONG (swapped) coefficients
1009+
double wrongZ0 = std::cos(4.0 * M_PI * f1 * t);
1010+
double wrongZ1 = std::cos(4.0 * M_PI * f0 * t);
1011+
1012+
double actualZ0 = (double)expVals[0];
1013+
double actualZ1 = (double)expVals[1];
1014+
1015+
// If bug exists, actualZ0 would be close to wrongZ0 instead of expectedZ0
1016+
bool matchesCorrect = (std::abs(actualZ0 - expectedZ0) < 0.05) &&
1017+
(std::abs(actualZ1 - expectedZ1) < 0.05);
1018+
bool matchesWrong = (std::abs(actualZ0 - wrongZ0) < 0.05) &&
1019+
(std::abs(actualZ1 - wrongZ1) < 0.05);
1020+
1021+
if (t > 0.1) { // Skip early times where values might be similar
1022+
if (matchesWrong && !matchesCorrect) {
1023+
std::cout << "BUG DETECTED at t=" << t << ": Batch 1\n";
1024+
std::cout << " <Z(0)> actual=" << actualZ0
1025+
<< ", expected=" << expectedZ0 << ", wrong=" << wrongZ0
1026+
<< "\n";
1027+
std::cout << " <Z(1)> actual=" << actualZ1
1028+
<< ", expected=" << expectedZ1 << ", wrong=" << wrongZ1
1029+
<< "\n";
1030+
}
1031+
}
1032+
1033+
EXPECT_NEAR(actualZ0, expectedZ0, 0.05)
1034+
<< "Batch 1, t=" << t << ": <Z(0)> mismatch - coefficient bug?";
1035+
EXPECT_NEAR(actualZ1, expectedZ1, 0.05)
1036+
<< "Batch 1, t=" << t << ": <Z(1)> mismatch - coefficient bug?";
1037+
1038+
count++;
1039+
}
1040+
}
1041+
1042+
// Check batch 2 results
1043+
{
1044+
EXPECT_TRUE(results[1].expectation_values.has_value());
1045+
const auto &expValsList = results[1].expectation_values.value();
1046+
EXPECT_EQ(expValsList.size(), numSteps);
1047+
1048+
int count = 0;
1049+
for (auto expVals : expValsList) {
1050+
EXPECT_EQ(expVals.size(), 2);
1051+
double t = steps[count];
1052+
1053+
double expectedZ0 = std::cos(4.0 * M_PI * g0 * t);
1054+
double expectedZ1 = std::cos(4.0 * M_PI * g1 * t);
1055+
1056+
double actualZ0 = (double)expVals[0];
1057+
double actualZ1 = (double)expVals[1];
1058+
1059+
EXPECT_NEAR(actualZ0, expectedZ0, 0.05)
1060+
<< "Batch 2, t=" << t << ": <Z(0)> mismatch - coefficient bug?";
1061+
EXPECT_NEAR(actualZ1, expectedZ1, 0.05)
1062+
<< "Batch 2, t=" << t << ": <Z(1)> mismatch - coefficient bug?";
1063+
1064+
count++;
1065+
}
1066+
}
1067+
}

0 commit comments

Comments
 (0)