Skip to content

Commit c5ce585

Browse files
authored
🐛 Preserve bijective heuristic layouts (#1153)
Replace direct physical-to-logical layout assignments with the existing swap-based update so moving a logical qubit cannot duplicate it. Add regression coverage for deferred, static, and fidelity-aware placement. Assisted-by: GPT-5 via Codex
1 parent 7b2fc33 commit c5ce585

3 files changed

Lines changed: 79 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._
2222

2323
### Fixed
2424

25+
- 🐛 Preserve bijective layouts when the heuristic mapper moves logical qubits
26+
([#1153]) ([**@burgholzer**])
2527
- 🐛 Preserve the input circuit name during mapping ([#1154])
2628
([**@burgholzer**])
2729
- 🐛 Handle Qiskit targets without calibration properties ([#1152])
@@ -307,6 +309,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._
307309
<!-- PR links -->
308310

309311
[#1154]: https://github.com/munich-quantum-toolkit/qmap/pull/1154
312+
[#1153]: https://github.com/munich-quantum-toolkit/qmap/pull/1153
310313
[#1152]: https://github.com/munich-quantum-toolkit/qmap/pull/1152
311314
[#1126]: https://github.com/munich-quantum-toolkit/qmap/pull/1126
312315
[#1116]: https://github.com/munich-quantum-toolkit/qmap/pull/1116

src/sc/heuristic/HeuristicMapper.cpp

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "sc/heuristic/HeuristicMapper.hpp"
1212

1313
#include "ir/Definitions.hpp"
14+
#include "ir/Permutation.hpp"
1415
#include "ir/operations/CompoundOperation.hpp"
1516
#include "ir/operations/OpType.hpp"
1617
#include "ir/operations/StandardOperation.hpp"
@@ -106,6 +107,20 @@ void HeuristicMapper::map(const Configuration& configuration) {
106107
}
107108
}
108109

110+
namespace {
111+
// search for current position of target value in map and afterward exchange
112+
// it with the value at new position
113+
void findAndSWAP(const qc::Qubit targetValue, const qc::Qubit newPosition,
114+
qc::Permutation& map) {
115+
for (const auto& q : map) {
116+
if (q.second == targetValue) {
117+
std::swap(map.at(newPosition), map.at(q.first));
118+
break;
119+
}
120+
}
121+
}
122+
} // namespace
123+
109124
void HeuristicMapper::staticInitialMapping() {
110125
for (const auto& gate : layers.at(0U)) {
111126
if (gate.singleQubit()) {
@@ -120,11 +135,12 @@ void HeuristicMapper::staticInitialMapping() {
120135
locations.at(static_cast<std::uint16_t>(gate.control)) =
121136
static_cast<std::int16_t>(q0);
122137
locations.at(gate.target) = static_cast<std::int16_t>(q1);
123-
qcMapped.initialLayout.at(q0) = static_cast<qc::Qubit>(gate.control);
124-
qcMapped.initialLayout.at(q1) = static_cast<qc::Qubit>(gate.target);
125-
qcMapped.outputPermutation.at(q0) =
126-
static_cast<qc::Qubit>(gate.control);
127-
qcMapped.outputPermutation.at(q1) = static_cast<qc::Qubit>(gate.target);
138+
findAndSWAP(static_cast<qc::Qubit>(gate.control), q0,
139+
qcMapped.initialLayout);
140+
findAndSWAP(gate.target, q1, qcMapped.initialLayout);
141+
findAndSWAP(static_cast<qc::Qubit>(gate.control), q0,
142+
qcMapped.outputPermutation);
143+
findAndSWAP(gate.target, q1, qcMapped.outputPermutation);
128144
break;
129145
}
130146
}
@@ -137,8 +153,8 @@ void HeuristicMapper::staticInitialMapping() {
137153
if (qubits.at(j) == DEFAULT_POSITION) {
138154
locations.at(i) = static_cast<std::int16_t>(j);
139155
qubits.at(j) = static_cast<std::int16_t>(i);
140-
qcMapped.initialLayout.at(j) = i;
141-
qcMapped.outputPermutation.at(j) = i;
156+
findAndSWAP(i, j, qcMapped.initialLayout);
157+
findAndSWAP(i, j, qcMapped.outputPermutation);
142158
break;
143159
}
144160
}
@@ -194,20 +210,6 @@ void HeuristicMapper::createInitialMapping() {
194210
}
195211
}
196212

197-
namespace {
198-
// search for current position of target value in map and afterward exchange
199-
// it with the value at new position
200-
void findAndSWAP(const qc::Qubit targetValue, const qc::Qubit newPosition,
201-
qc::Permutation& map) {
202-
for (const auto& q : map) {
203-
if (q.second == targetValue) {
204-
std::swap(map.at(newPosition), map.at(q.first));
205-
break;
206-
}
207-
}
208-
}
209-
} // namespace
210-
211213
void HeuristicMapper::mapUnmappedGates(std::size_t layer) {
212214
if (fidelityAwareHeur) {
213215
for (std::size_t q = 0; q < singleQubitMultiplicities.at(layer).size();
@@ -223,6 +225,10 @@ void HeuristicMapper::mapUnmappedGates(std::size_t layer) {
223225
if (qubits.at(physQbit) == -1) {
224226
locations.at(q) = static_cast<std::int16_t>(physQbit);
225227
qubits.at(physQbit) = static_cast<std::int16_t>(q);
228+
findAndSWAP(static_cast<qc::Qubit>(q), physQbit,
229+
qcMapped.initialLayout);
230+
findAndSWAP(static_cast<qc::Qubit>(q), physQbit,
231+
qcMapped.outputPermutation);
226232
break;
227233
}
228234
}
@@ -498,7 +504,7 @@ void HeuristicMapper::routeCircuit() {
498504
locations.at(target) = static_cast<std::int16_t>(loc);
499505
qubits.at(loc) = static_cast<std::int16_t>(target);
500506
op->setTargets({static_cast<qc::Qubit>(loc)});
501-
qcMapped.initialLayout.at(loc) = target;
507+
findAndSWAP(target, loc, qcMapped.initialLayout);
502508
qcMapped.outputPermutation[static_cast<qc::Qubit>(loc)] = target;
503509
} else {
504510
op->setTargets({static_cast<qc::Qubit>(targetLocation)});

test/python/sc/test_heuristic_mapper.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from qiskit.providers.fake_provider import GenericBackendV2
1717

1818
from mqt.qmap.plugins.qiskit.sc import compile_
19+
from mqt.qmap.sc import Heuristic, InitialLayout, Layering
1920

2021

2122
@pytest.fixture
@@ -79,3 +80,50 @@ def test_heuristic_non_trivial_swaps(backend: GenericBackendV2) -> None:
7980
print(result)
8081

8182
assert result.considered_equivalent() is True
83+
84+
85+
def test_heuristic_layout_with_deferred_single_qubit() -> None:
86+
"""Verify that placing a single-only qubit preserves a bijective layout."""
87+
qc = QuantumCircuit(4)
88+
qc.h(3)
89+
qc.cx(0, 1)
90+
qc.cx(1, 2)
91+
qc.measure_all()
92+
backend = GenericBackendV2(num_qubits=5, coupling_map=[[0, 1], [1, 4], [4, 3], [3, 2]])
93+
94+
qc_mapped, _ = compile_(qc, arch=backend)
95+
96+
assert verify(qc, qc_mapped).considered_equivalent() is True
97+
98+
99+
def test_heuristic_static_layout_is_bijective() -> None:
100+
"""Verify that static placement preserves a bijective layout."""
101+
qc = QuantumCircuit(4)
102+
qc.h(0)
103+
qc.h(1)
104+
qc.cx(2, 3)
105+
qc.measure_all()
106+
backend = GenericBackendV2(num_qubits=5, coupling_map=[[0, 4], [4, 3], [3, 2], [2, 1]])
107+
108+
qc_mapped, _ = compile_(qc, arch=backend, initial_layout=InitialLayout.static, layering=Layering.disjoint_qubits)
109+
110+
assert verify(qc, qc_mapped).considered_equivalent() is True
111+
112+
113+
def test_fidelity_aware_layout_is_bijective() -> None:
114+
"""Verify that fidelity-aware single-qubit placement preserves a bijective layout."""
115+
qc = QuantumCircuit(4)
116+
qc.h(3)
117+
qc.cx(0, 1)
118+
qc.cx(1, 2)
119+
qc.measure_all()
120+
backend = GenericBackendV2(num_qubits=5, coupling_map=[[0, 1], [1, 4], [4, 3], [3, 2]], seed=1)
121+
122+
qc_mapped, _ = compile_(
123+
qc,
124+
arch=backend,
125+
heuristic=Heuristic.fidelity_best_location,
126+
lookahead_heuristic=None,
127+
)
128+
129+
assert verify(qc, qc_mapped).considered_equivalent() is True

0 commit comments

Comments
 (0)