From 464f2e2bcb670aa2b7b4cb3ac2938d552b245844 Mon Sep 17 00:00:00 2001 From: dsdsdshe Date: Wed, 2 Jul 2025 10:31:31 +0800 Subject: [PATCH] Fix(device): Handle non-contiguous physical qubit IDs in SABRE mappers The C++ implementations of the SABRE and MQSABRE algorithms previously assumed that the physical qubit IDs from the `QubitsTopology` were a contiguous range starting from 0. This caused indexing errors and crashes when provided with a topology using non-contiguous IDs (e.g., `[12, 13, 14, 15]`). This commit resolves the issue by introducing an internal ID-compression mechanism. On initialization, the mappers now create a bidirectional mapping between the original, arbitrary qubit IDs and a temporary, contiguous set of IDs `[0..n-1]`. The core SABRE logic operates on this compressed ID space. The final results, including the mapped circuit and the initial/final qubit layouts, are remapped back to the original physical qubit IDs before being returned. Additionally, this commit: - Adds a validation check to ensure the number of logical qubits does not exceed the number of available physical qubits. - Introduces a new test file `tests/st/test_device/test_mapping.py` to verify the fix for non-contiguous IDs and cover other edge cases like disconnected topologies. - Fixes a minor typo in a `MQSABRE` error message. --- ccsrc/include/device/mapping.h | 9 ++ ccsrc/lib/device/mapping.cpp | 109 ++++++++++--- mindquantum/algorithm/mapping/mq_sabre.py | 2 +- tests/st/test_device/test_mapping.py | 180 ++++++++++++++++++++++ 4 files changed, 276 insertions(+), 24 deletions(-) create mode 100644 tests/st/test_device/test_mapping.py diff --git a/ccsrc/include/device/mapping.h b/ccsrc/include/device/mapping.h index 6ea56405f..4c41faa72 100644 --- a/ccsrc/include/device/mapping.h +++ b/ccsrc/include/device/mapping.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -168,6 +169,10 @@ class MQ_SABRE { VT> T; // The length of a CNOT between the physical qubits Qi and Qj VT> DM; // distance matrix + // Internal ID-compression: map original physical qubit IDs to [0..num_physical-1] + std::unordered_map old_to_new_; + VT new_to_old_; + public: VT> CNOT_error_rate; // The error rate of the cnot gates VT> CNOT_gate_length; // The length of the cnot gates @@ -296,6 +301,10 @@ class SABRE { double H(const std::list& F, const std::list& E, const VT& pi, const std::pair& SWAP, const VT& decay) const; + // Internal ID-compression: map original physical qubit IDs to [0..num_physical-1] + std::unordered_map old_to_new_; + VT new_to_old_; + public: /** * @brief Construct a new SABRE object diff --git a/ccsrc/lib/device/mapping.cpp b/ccsrc/lib/device/mapping.cpp index a389923ce..09afa4d91 100644 --- a/ccsrc/lib/device/mapping.cpp +++ b/ccsrc/lib/device/mapping.cpp @@ -17,6 +17,7 @@ #include "device/mapping.h" #include +#include #include #include #include @@ -447,15 +448,29 @@ MQ_SABRE::MQ_SABRE(const VT>& circ, const std::shared auto tmp = GateToAbstractGate(circ); this->num_logical = tmp.first; this->gates = tmp.second; - this->num_physical = coupling_graph->size(); - this->CNOT_error_rate = VT>(this->num_physical, VT(num_physical)); - this->CNOT_gate_length = VT>(this->num_physical, VT(num_physical)); + // Compress physical-qubit IDs to contiguous [0..num_physical-1] + auto ids_set = coupling_graph->AllQubitID(); + VT sorted_ids(ids_set.begin(), ids_set.end()); + std::sort(sorted_ids.begin(), sorted_ids.end()); + for (int i = 0; i < static_cast(sorted_ids.size()); ++i) { + old_to_new_[sorted_ids[i]] = i; + new_to_old_.push_back(sorted_ids[i]); + } + this->num_physical = static_cast(sorted_ids.size()); + if (this->num_logical > this->num_physical) { + throw std::runtime_error("The number of logical qubits (" + std::to_string(this->num_logical) + + ") cannot be greater than the number of physical qubits (" + + std::to_string(this->num_physical) + ")."); + } + + this->CNOT_error_rate = VT>(this->num_physical, VT(this->num_physical, 0.0)); + this->CNOT_gate_length = VT>(this->num_physical, VT(this->num_physical, 0.0)); // get cnot error rate matrix and cnot length matrix - for (int i = 0; i < CnotErrrorRateAndGateLength.size(); i++) { - CNOT_error_rate[CnotErrrorRateAndGateLength[i].first.first][CnotErrrorRateAndGateLength[i].first.second] - = CnotErrrorRateAndGateLength[i].second[0]; - CNOT_gate_length[CnotErrrorRateAndGateLength[i].first.first][CnotErrrorRateAndGateLength[i].first.second] - = CnotErrrorRateAndGateLength[i].second[1]; + for (const auto& cnot_data : CnotErrrorRateAndGateLength) { + int n_q1 = old_to_new_.at(cnot_data.first.first); + int n_q2 = old_to_new_.at(cnot_data.first.second); + CNOT_error_rate[n_q1][n_q2] = cnot_data.second[0]; + CNOT_gate_length[n_q1][n_q2] = cnot_data.second[1]; } this->SWAP_success_rate = VT>(this->num_physical, VT(this->num_physical)); this->SWAP_gate_length = VT>(this->num_physical, VT(this->num_physical)); @@ -473,9 +488,11 @@ MQ_SABRE::MQ_SABRE(const VT>& circ, const std::shared } } this->G = VT>(this->num_physical, VT(0)); - for (auto id : coupling_graph->AllQubitID()) { - auto nearby = (*coupling_graph)[id]->neighbour; - this->G[id].insert(this->G[id].begin(), nearby.begin(), nearby.end()); + for (auto old_id : sorted_ids) { + int nid = old_to_new_[old_id]; + for (auto old_nb : (*coupling_graph)[old_id]->neighbour) { + this->G[nid].push_back(old_to_new_[old_nb]); + } } // get DAG of logical circuit this->DAG = GetCircuitDAG(num_logical, gates); @@ -572,16 +589,35 @@ std::pair>, std::pair, VT>> MQ_SABRE::Solve(double W, do auto initial_mapping = this->layout; // first mapping auto gates = HeuristicSearch(this->layout, this->DAG); // final mapping - // return std::pair>, std::pair, VT>>(); VT> gate_info; for (auto& g : gates) { + // translate back to original physical-qubit IDs + qbit_t old_q1 = new_to_old_.at(g.q1); + qbit_t old_q2 = new_to_old_.at(g.q2); if (g.type == "SWAP") { - gate_info.push_back({-1, g.q1, g.q2}); + gate_info.push_back({-1, static_cast(old_q1), static_cast(old_q2)}); } else { - gate_info.push_back({std::stoi(g.tag), g.q1, g.q2}); + gate_info.push_back({std::stoi(g.tag), static_cast(old_q1), static_cast(old_q2)}); } } - return {gate_info, {initial_mapping, this->layout}}; + // remap initial and final logical->physical mappings back to original IDs + VT init_map_remap(initial_mapping.size()); + for (size_t i = 0; i < initial_mapping.size(); i++) { + if (initial_mapping[i] != -1) { + init_map_remap[i] = static_cast(new_to_old_.at(initial_mapping[i])); + } else { + init_map_remap[i] = -1; + } + } + VT final_map_remap(this->layout.size()); + for (size_t i = 0; i < this->layout.size(); i++) { + if (this->layout[i] != -1) { + final_map_remap[i] = static_cast(new_to_old_.at(this->layout[i])); + } else { + final_map_remap[i] = -1; + } + } + return {gate_info, {init_map_remap, final_map_remap}}; } inline void MQ_SABRE::SetParameters(double W, double alpha1, double alpha2, double alpha3) { @@ -675,11 +711,26 @@ SABRE::SABRE(const VT>& circ, const std::shared_ptrnum_logical = tmp.first; this->gates = tmp.second; - this->num_physical = coupling_graph->size(); - this->G = VT>(this->num_physical, VT(0)); - for (auto id : coupling_graph->AllQubitID()) { - auto nearby = (*coupling_graph)[id]->neighbour; - this->G[id].insert(this->G[id].begin(), nearby.begin(), nearby.end()); + // Compress physical-qubit IDs to contiguous [0..num_physical-1] + auto ids_set = coupling_graph->AllQubitID(); + VT sorted_ids(ids_set.begin(), ids_set.end()); + std::sort(sorted_ids.begin(), sorted_ids.end()); + for (int i = 0; i < static_cast(sorted_ids.size()); ++i) { + old_to_new_[sorted_ids[i]] = i; + new_to_old_.push_back(sorted_ids[i]); + } + this->num_physical = static_cast(sorted_ids.size()); + if (this->num_logical > this->num_physical) { + throw std::runtime_error("The number of logical qubits (" + std::to_string(this->num_logical) + + ") cannot be greater than the number of physical qubits (" + + std::to_string(this->num_physical) + ")."); + } + this->G = VT>(num_physical, VT(0)); + for (auto old_id : sorted_ids) { + int nid = old_to_new_[old_id]; + for (auto old_nb : (*coupling_graph)[old_id]->neighbour) { + this->G[nid].push_back(old_to_new_[old_nb]); + } } // ----------------------------------------------------------------------------- @@ -817,13 +868,25 @@ std::pair>, std::pair, VT>> SABRE::Solve(int iter_num, d auto gs = HeuristicSearch(pi, this->DAG); VT> gate_info; for (auto& g : gs) { + // translate back to original physical-qubit IDs + qbit_t old_q1 = new_to_old_[g.q1]; + qbit_t old_q2 = new_to_old_[g.q2]; if (g.type == "SWAP") { - gate_info.push_back({-1, g.q1, g.q2}); + gate_info.push_back({-1, static_cast(old_q1), static_cast(old_q2)}); } else { - gate_info.push_back({std::stoi(g.tag), g.q1, g.q2}); + gate_info.push_back({std::stoi(g.tag), static_cast(old_q1), static_cast(old_q2)}); } } - return {gate_info, {initial_mapping, pi}}; + // remap initial and final logical->physical mappings back to original IDs + VT init_map_remap; + VT final_map_remap; + init_map_remap.reserve(initial_mapping.size()); + std::transform(initial_mapping.cbegin(), initial_mapping.cend(), std::back_inserter(init_map_remap), + [this](int v) { return static_cast(new_to_old_[v]); }); + final_map_remap.reserve(pi.size()); + std::transform(pi.cbegin(), pi.cend(), std::back_inserter(final_map_remap), + [this](int v) { return static_cast(new_to_old_[v]); }); + return {gate_info, {init_map_remap, final_map_remap}}; } inline void SABRE::SetParameters(double W, double delta1, double delta2) { diff --git a/mindquantum/algorithm/mapping/mq_sabre.py b/mindquantum/algorithm/mapping/mq_sabre.py index 2253f7a85..67ed70a65 100644 --- a/mindquantum/algorithm/mapping/mq_sabre.py +++ b/mindquantum/algorithm/mapping/mq_sabre.py @@ -116,7 +116,7 @@ class MQSABRE: if not check_connected(topology): raise ValueError( - 'The current mapping algorithm SABRE only supports connected graphs, ' + 'The current mapping algorithm MQSABRE only supports connected graphs, ' 'please manually assign some lines to connected subgraphs.' ) diff --git a/tests/st/test_device/test_mapping.py b/tests/st/test_device/test_mapping.py new file mode 100644 index 000000000..bd25be292 --- /dev/null +++ b/tests/st/test_device/test_mapping.py @@ -0,0 +1,180 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test for SABRE and MQSABRE mapping algorithms.""" + +import pytest + +from mindquantum.algorithm.mapping import MQSABRE, SABRE +from mindquantum.core.circuit import Circuit +from mindquantum.core.gates import H, X +from mindquantum.device import GridQubits, LinearQubits, QubitNode, QubitsTopology + + +def test_sabre_basic(): + """ + Description: Test basic SABRE functionality. + Expectation: Runs without error, returns correct types. + """ + circ = Circuit([H(0), X(1, 0), X(2, 1)]) + topo = LinearQubits(3) + solver = SABRE(circ, topo) + new_circ, init_map, final_map = solver.solve() + + assert isinstance(new_circ, Circuit) + assert isinstance(init_map, list) + assert isinstance(final_map, list) + assert len(init_map) == 3 + assert len(final_map) == 3 + assert len(new_circ) >= len(circ) + + +def test_sabre_non_contiguous_qubits(): + """ + Description: Test SABRE with non-contiguous qubit IDs. This should pass due to the fix. + Expectation: Runs without crashing and maps to correct physical qubits. + """ + q12 = QubitNode(12, poi_x=0, poi_y=0) + q13 = QubitNode(13, poi_x=1, poi_y=0) + q14 = QubitNode(14, poi_x=0, poi_y=1) + q15 = QubitNode(15, poi_x=1, poi_y=1) + + topology = QubitsTopology([q12, q13, q14, q15]) + couplers = [[12, 13], [12, 14], [14, 15], [13, 15]] + for pair in couplers: + topology[pair[0]] >> topology[pair[1]] + + circ = Circuit().rx(1.23, 0).rx(2.13, 1).rx(3.12, 2).x(1, 0).x(2, 1).x(0, 2) + solver = SABRE(circ, topology) + new_circ, init_mapping, final_mapping = solver.solve(5, 0.5, 0.3, 0.2) + + assert isinstance(new_circ, Circuit) + physical_qubits = {12, 13, 14, 15} + for gate in new_circ: + all_gate_qubits = set(gate.obj_qubits) | set(gate.ctrl_qubits) + assert all_gate_qubits.issubset(physical_qubits) + + +def test_sabre_disconnected_topology(): + """ + Description: Test SABRE with a disconnected topology. + Expectation: Raises ValueError. + """ + q0 = QubitNode(0) + q1 = QubitNode(1) + q2 = QubitNode(2) + q3 = QubitNode(3) + q0 >> q1 # component 1 + q2 >> q3 # component 2 + topology = QubitsTopology([q0, q1, q2, q3]) + circ = Circuit().x(1, 0).x(3, 2) + with pytest.raises(ValueError, match="SABRE only supports connected graphs"): + SABRE(circ, topology) + + +def test_sabre_insufficient_physical_qubits(): + """ + Description: Test SABRE when logical qubits are more than physical qubits. + Expectation: Raises RuntimeError because the C++ core will throw an exception. + """ + circ = Circuit().x(1, 0).x(3, 2) # Needs 4 logical qubits (0, 1, 2, 3) + topology = LinearQubits(3) # Only 3 physical qubits + with pytest.raises( + RuntimeError, match="The number of logical qubits .* cannot be greater than the number of physical qubits" + ): + SABRE(circ, topology) + + +def test_mqsabre_basic(): + """ + Description: Test basic MQSABRE functionality. + Expectation: Runs without error and returns correct types. + """ + circ = Circuit([H(0), X(1, 0), X(2, 1), X(3, 2)]) + topology = GridQubits(2, 2) + cnot_data = [ + ((0, 1), [0.001, 250.0]), + ((1, 0), [0.001, 250.0]), + ((0, 2), [0.002, 300.0]), + ((2, 0), [0.002, 300.0]), + ((1, 3), [0.001, 250.0]), + ((3, 1), [0.001, 250.0]), + ((2, 3), [0.002, 300.0]), + ((3, 2), [0.002, 300.0]), + ] + solver = MQSABRE(circ, topology, cnot_data) + new_circ, init_map, final_map = solver.solve() + + assert isinstance(new_circ, Circuit) + assert isinstance(init_map, list) + assert isinstance(final_map, list) + assert len(init_map) == 4 + assert len(final_map) >= 4 # Final map can be larger if idle qubits are mapped + assert len(new_circ) >= len(circ) + + +def test_mqsabre_disconnected_topology(): + """ + Description: Test MQSABRE with a disconnected topology. + Expectation: Raises ValueError. + """ + q0 = QubitNode(0) + q1 = QubitNode(1) + q2 = QubitNode(2) + q3 = QubitNode(3) + q0 >> q1 + q2 >> q3 + topology = QubitsTopology([q0, q1, q2, q3]) + circ = Circuit().x(1, 0).x(3, 2) + cnot_data = [((0, 1), [0.01, 100]), ((1, 0), [0.01, 100]), ((2, 3), [0.01, 100]), ((3, 2), [0.01, 100])] + with pytest.raises(ValueError, match="MQSABRE only supports connected graphs"): + MQSABRE(circ, topology, cnot_data) + + +def test_mqsabre_non_contiguous_qubits_fixed(): + """ + Description: Test MQSABRE with non-contiguous qubit IDs. This should pass after the fix. + Expectation: Runs without crashing and maps to correct physical qubits. + """ + q_map = {0: 10, 1: 12, 2: 14, 3: 16} + + nodes = [QubitNode(qid) for qid in q_map.values()] + topology = QubitsTopology(nodes) + topology[q_map[0]] >> topology[q_map[1]] + topology[q_map[1]] >> topology[q_map[3]] + topology[q_map[0]] >> topology[q_map[2]] + topology[q_map[2]] >> topology[q_map[3]] + + circ = Circuit().x(1, 0).x(2, 1).x(3, 2) + + cnot_data = [ + ((q_map[0], q_map[1]), [0.001, 250.0]), + ((q_map[1], q_map[0]), [0.001, 250.0]), + ((q_map[1], q_map[3]), [0.002, 300.0]), + ((q_map[3], q_map[1]), [0.002, 300.0]), + ((q_map[0], q_map[2]), [0.003, 350.0]), + ((q_map[2], q_map[0]), [0.003, 350.0]), + ((q_map[2], q_map[3]), [0.004, 400.0]), + ((q_map[3], q_map[2]), [0.004, 400.0]), + ] + + # With the ID compression fix, this should now pass without raising an error. + solver = MQSABRE(circ, topology, cnot_data) + new_circ, init_map, final_map = solver.solve() + + assert isinstance(new_circ, Circuit) + physical_qubits = set(q_map.values()) + for gate in new_circ: + all_gate_qubits = set(gate.obj_qubits) | set(gate.ctrl_qubits) + assert all_gate_qubits.issubset(physical_qubits) -- Gitee