Skip to content

Commit 75ebdc5

Browse files
committed
Added a test for circuit conversion to two qubit gates layers and back. Found and fixed the issue with some circuits (containing classically controlled gates) being converted like that for mps initial qubits layout optimization in the network execution.
1 parent 376e14c commit 75ebdc5

4 files changed

Lines changed: 143 additions & 5 deletions

File tree

Circuit/Circuit.h

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,7 @@ class Circuit : public IOperation<Time> {
21922192
layers.emplace_back(std::make_shared<Circuits::Circuit<Time>>());
21932193

21942194
std::unordered_map<Types::qubit_t, Types::qubit_t> qubitsUsed;
2195+
std::unordered_map<size_t, size_t> classicalBitLayer;
21952196

21962197
for (const auto &op : GetOperations()) {
21972198
// check the instruction, see if a new layer is needed
@@ -2213,10 +2214,30 @@ class Circuit : public IOperation<Time> {
22132214
}
22142215
}
22152216

2217+
if (op->IsConditional()) {
2218+
const auto bits = op->AffectedBits();
2219+
for (const auto bit : bits)
2220+
maxLevel = std::max(maxLevel, classicalBitLayer[bit]);
2221+
}
2222+
22162223
// now set all the qubits in the instruction to the max level
22172224
for (Types::qubit_t qbit : qubits) qubitsUsed[qbit] = maxLevel;
22182225

2219-
layers[maxLevel > 0 ? maxLevel - 1 : 0]->AddOperation(op->Clone());
2226+
const size_t layerIdx = maxLevel > 0 ? maxLevel - 1 : 0;
2227+
2228+
// ensure enough layers exist for the computed level
2229+
while (layers.size() <= layerIdx)
2230+
layers.push_back(std::make_shared<Circuits::Circuit<Time>>());
2231+
2232+
layers[layerIdx]->AddOperation(op->Clone());
2233+
2234+
const auto writtenBits = op->AffectedBits();
2235+
if (!writtenBits.empty() && !op->IsConditional()) {
2236+
const size_t writtenLevel = maxLevel > 0 ? maxLevel : 1;
2237+
for (const auto bit : writtenBits)
2238+
classicalBitLayer[bit] =
2239+
std::max(classicalBitLayer[bit], writtenLevel);
2240+
}
22202241
} else
22212242
// add the instruction to the last layer
22222243
layers.back()->AddOperation(op->Clone());
@@ -2242,6 +2263,11 @@ class Circuit : public IOperation<Time> {
22422263

22432264
std::unordered_map<Types::qubit_t, Types::qubit_t> qubitsUsed;
22442265

2266+
// Track which layer (1-based, like qubitsUsed) each classical bit was last
2267+
// written to, so that conditional operations reading those bits are placed
2268+
// in the same layer or later.
2269+
std::unordered_map<size_t, size_t> classicalBitLayer;
2270+
22452271
for (const auto &op : GetOperations()) {
22462272
// check the instruction, see if a new layer is needed
22472273

@@ -2262,10 +2288,34 @@ class Circuit : public IOperation<Time> {
22622288
}
22632289
}
22642290

2291+
// For conditional operations, ensure this op is placed no earlier than
2292+
// the layer where the classical bits it depends on were written.
2293+
if (op->IsConditional()) {
2294+
const auto bits = op->AffectedBits();
2295+
for (const auto bit : bits)
2296+
maxLevel = std::max(maxLevel, classicalBitLayer[bit]);
2297+
}
2298+
22652299
// now set all the qubits in the instruction to the max level
22662300
for (Types::qubit_t qbit : qubits) qubitsUsed[qbit] = maxLevel;
22672301

2268-
layers[maxLevel > 0 ? maxLevel - 1 : 0]->AddOperation(op);
2302+
const size_t layerIdx = maxLevel > 0 ? maxLevel - 1 : 0;
2303+
2304+
// ensure enough layers exist for the computed level
2305+
while (layers.size() <= layerIdx)
2306+
layers.push_back(std::make_shared<Circuits::Circuit<Time>>());
2307+
2308+
layers[layerIdx]->AddOperation(op);
2309+
2310+
// Record the layer for classical bits written by measurements / resets
2311+
// so that later conditional ops respect the dependency.
2312+
const auto writtenBits = op->AffectedBits();
2313+
if (!writtenBits.empty() && !op->IsConditional()) {
2314+
const size_t writtenLevel = maxLevel > 0 ? maxLevel : 1;
2315+
for (const auto bit : writtenBits)
2316+
classicalBitLayer[bit] =
2317+
std::max(classicalBitLayer[bit], writtenLevel);
2318+
}
22692319
} else
22702320
// add the instruction to the last layer
22712321
layers.back()->AddOperation(op);

Network/NetworkJob.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#include "../Simulators/MPSDummySimulator.h"
2020

21+
#include "Network.h"
22+
2123
namespace Network {
2224

2325
template <typename Time = Types::time_type>
@@ -330,6 +332,8 @@ class ExecuteJob {
330332
std::string maxBondDim;
331333
std::string singularValueThreshold;
332334
std::string mpsSample;
335+
336+
std::shared_ptr<Network::INetwork<Time>> network;
333337
};
334338

335339
} // namespace Network

Network/SimpleDisconnectedNetwork.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,8 @@ class SimpleDisconnectedNetwork : public INetwork<Time> {
647647
job->mpsSample = mpsSample;
648648
job->singularValueThreshold = singularValueThreshold;
649649

650+
job->network = BaseClass::getptr();
651+
650652
if (optSim) {
651653
job->optSim = optSim->Clone();
652654
job->executedGates = executed;
@@ -669,6 +671,8 @@ class SimpleDisconnectedNetwork : public INetwork<Time> {
669671
job->mpsSample = mpsSample;
670672
job->singularValueThreshold = singularValueThreshold;
671673

674+
job->network = BaseClass::getptr();
675+
672676
if (optSim) {
673677
optSim->SetMultithreading(true);
674678
job->optSim = optSim;
@@ -829,6 +833,8 @@ class SimpleDisconnectedNetwork : public INetwork<Time> {
829833
job->mpsSample = mpsSample;
830834
job->singularValueThreshold = singularValueThreshold;
831835

836+
job->network = BaseClass::getptr();
837+
832838
if (optSim) {
833839
job->optSim = optSim->Clone();
834840
job->executedGates = executed;
@@ -851,6 +857,8 @@ class SimpleDisconnectedNetwork : public INetwork<Time> {
851857
job->mpsSample = mpsSample;
852858
job->singularValueThreshold = singularValueThreshold;
853859

860+
job->network = BaseClass::getptr();
861+
854862
if (optSim) {
855863
optSim->SetMultithreading(true);
856864
job->optSim = optSim;
@@ -2142,9 +2150,8 @@ class SimpleDisconnectedNetwork : public INetwork<Time> {
21422150

21432151
if (optimizeInitialQubitsMap) sim->SetInitialQubitsMap(optimalMap);
21442152

2145-
// TODO: this breaks scheduling, needs to be done before scheduling, but at that point we don't know it mps swaps optimization is going to be used or not
2146-
//auto optCirc = Circuits::Circuit<Time>::LayersToCircuit(layers);
2147-
//dcirc->SetOperations(optCirc->GetOperations());
2153+
auto optCirc = Circuits::Circuit<Time>::LayersToCircuit(layers);
2154+
dcirc->SetOperations(optCirc->GetOperations());
21482155
}
21492156
}
21502157
}

tests/mpsswapsopttests.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,83 @@ constexpr std::array numGates{10, 20, 30, 50, 80, 100, 150,
3939
200, 270, 340, 500, 800, 1000};
4040

4141

42+
43+
BOOST_DATA_TEST_CASE(ConvertForCuttingLayersRoundtripMatchesStatevector,
44+
numGates, nrGates) {
45+
constexpr int nrQubits = 10;
46+
const size_t nrStates = 1ULL << nrQubits;
47+
48+
// build a random circuit with 1- and 2-qubit gates
49+
auto randomCirc = std::make_shared<Circuits::Circuit<>>();
50+
51+
std::mt19937 g(std::random_device{}());
52+
std::uniform_real_distribution<double> dblDist(-2. * M_PI, 2. * M_PI);
53+
std::uniform_int_distribution<int> gateDist(
54+
0, static_cast<int>(Circuits::QuantumGateType::kCUGateType));
55+
56+
for (int gateNr = 0; gateNr < nrGates; ++gateNr) {
57+
Types::qubits_vector qubits(nrQubits);
58+
std::iota(qubits.begin(), qubits.end(), 0);
59+
std::shuffle(qubits.begin(), qubits.end(), g);
60+
auto q1 = qubits[0];
61+
auto q2 = qubits[1];
62+
63+
const double param1 = dblDist(g);
64+
const double param2 = dblDist(g);
65+
const double param3 = dblDist(g);
66+
const double param4 = dblDist(g);
67+
68+
Circuits::QuantumGateType gateType =
69+
static_cast<Circuits::QuantumGateType>(gateDist(g));
70+
71+
auto theGate = Circuits::CircuitFactory<>::CreateGate(
72+
gateType, q1, q2, 0, param1, param2, param3, param4);
73+
randomCirc->AddOperation(theGate);
74+
}
75+
76+
// --- Reference: execute the original circuit on a statevector simulator ---
77+
auto svSim = Simulators::SimulatorsFactory::CreateSimulator(
78+
Simulators::SimulatorType::kQCSim,
79+
Simulators::SimulationType::kStatevector);
80+
svSim->AllocateQubits(nrQubits);
81+
svSim->Initialize();
82+
83+
Circuits::OperationState stateSV;
84+
stateSV.AllocateBits(nrQubits);
85+
randomCirc->Execute(svSim, stateSV);
86+
87+
// --- Convert the circuit the same way OptimizeMPSInitialQubitsMap does ---
88+
auto convertedCirc = std::make_shared<Circuits::Circuit<>>(*randomCirc);
89+
convertedCirc->ConvertForCutting();
90+
auto layers = convertedCirc->ToMultipleQubitsLayersNoClone();
91+
auto finalCirc = Circuits::Circuit<>::LayersToCircuit(layers);
92+
93+
// BOOST_TEST_MESSAGE("Original gate count: " << randomCirc->size());
94+
// BOOST_TEST_MESSAGE("Converted gate count: " << finalCirc->size());
95+
// BOOST_TEST_MESSAGE("Number of layers: " << layers.size());
96+
97+
// --- Execute the converted circuit on an MPS simulator (no bond dim cap) ---
98+
auto mpsSim = Simulators::SimulatorsFactory::CreateSimulator(
99+
Simulators::SimulatorType::kQCSim,
100+
Simulators::SimulationType::kMatrixProductState);
101+
mpsSim->AllocateQubits(nrQubits);
102+
mpsSim->Initialize();
103+
104+
Circuits::OperationState stateMPS;
105+
stateMPS.AllocateBits(nrQubits);
106+
finalCirc->Execute(mpsSim, stateMPS);
107+
108+
// --- Compare amplitudes ---
109+
for (size_t s = 0; s < nrStates; ++s) {
110+
const auto aSV = svSim->Amplitude(s);
111+
const auto aMPS = mpsSim->Amplitude(s);
112+
BOOST_CHECK_PREDICATE(
113+
checkClose,
114+
(aSV)(aMPS)(1e-6)); // no bond dim limit, expect close match
115+
}
116+
}
117+
118+
42119
BOOST_DATA_TEST_CASE(OptimalQubitsMapZeroSwapCost, numGates, nrGates) {
43120
constexpr int nrQubits = 10;
44121

0 commit comments

Comments
 (0)