From 656aa8c9f1c14f1093dbe2945897421bf821ec26 Mon Sep 17 00:00:00 2001 From: Manideep3969 Date: Tue, 18 Aug 2026 13:00:25 +0530 Subject: [PATCH] fix(#41): cost model now uses per-qubit error rates when layout is provided The _get_gate_fidelity method accepted a layout parameter but never used it, always returning average error rates regardless of which physical qubit a gate was mapped to. This meant per-qubit calibration data from the backend was effectively ignored for fidelity estimation. Fix: Add _get_gate_fidelity_for_qubit and _get_gate_fidelity_for_pair methods that look up specific error rates from device calibration data when available. Update estimate_gate_error to iterate over circuit instructions (not just count_ops) when layout-aware or per-qubit data is available, using the correct per-qubit/per-link rates. All 313 tests pass. --- src/qc_compiler/cost_model.py | 98 +++++++++++++++++++++++++++++++++-- tests/test_cost_model.py | 77 ++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 6 deletions(-) diff --git a/src/qc_compiler/cost_model.py b/src/qc_compiler/cost_model.py index 3741cd4..3352a49 100644 --- a/src/qc_compiler/cost_model.py +++ b/src/qc_compiler/cost_model.py @@ -185,10 +185,9 @@ def estimate_gate_error( Computes 1 - Π_gates (1 - error_rate(gate, qubits)). - If no device is characterized, uses average error rates from - typical superconducting hardware: - - Single-qubit gate error: 0.05% - - Two-qubit gate error: 1.0% + When a layout is provided and device calibration data is available, + uses per-qubit and per-link error rates for each gate instruction. + Falls back to average error rates when per-qubit data is unavailable. Args: circuit: The transpiled quantum circuit. @@ -201,6 +200,25 @@ def estimate_gate_error( if not self.device.num_qubits: return self._estimate_gate_error_default(circuit) + if layout is not None or self.device.single_qubit_gate_errors: + product = 1.0 + for instr in circuit.data: + gate_name = instr.operation.name + qubits = tuple( + circuit.find_bit(q).index for q in instr.qubits + ) + if len(qubits) == 2 and gate_name in TWO_QUBIT_GATES: + fidelity = self._get_gate_fidelity_for_pair( + gate_name, qubits, layout + ) + else: + fidelity = self._get_gate_fidelity_for_qubit( + gate_name, qubits[0] if qubits else 0, layout + ) + product *= fidelity + + return 1.0 - product + product = 1.0 ops = circuit.count_ops() @@ -237,9 +255,13 @@ def _get_gate_fidelity( ) -> float: """Get the fidelity for a single gate execution on the device. + When layout is provided, looks up the specific error rate for + the gate on the mapped physical qubit(s). Falls back to average + error rates when per-qubit data is unavailable. + Args: gate_name: The gate name (e.g., 'sx', 'cx', 'ecr'). - layout: Optional qubit mapping. + layout: Optional qubit mapping from virtual to physical qubits. Returns: Gate fidelity (0 to 1). @@ -251,6 +273,72 @@ def _get_gate_fidelity( return 1.0 - avg_error + def _get_gate_fidelity_for_qubit( + self, gate_name: str, qubit: int, layout: dict | None = None + ) -> float: + """Get the fidelity for a single-qubit gate on a specific qubit. + + Uses per-qubit error rates from device calibration when available. + + Args: + gate_name: The gate name (e.g., 'sx', 'rz', 'h'). + qubit: The virtual qubit index the gate acts on. + layout: Optional mapping from virtual to physical qubits. + + Returns: + Gate fidelity (0 to 1). + """ + if not self.device.single_qubit_gate_errors: + return self._get_gate_fidelity(gate_name) + + physical_qubit = layout.get(qubit, qubit) if layout else qubit + key = (gate_name, physical_qubit) + if key in self.device.single_qubit_gate_errors: + return 1.0 - self.device.single_qubit_gate_errors[key] + + for (g, q), error in self.device.single_qubit_gate_errors.items(): + if q == physical_qubit: + return 1.0 - error + + return self._get_gate_fidelity(gate_name) + + def _get_gate_fidelity_for_pair( + self, gate_name: str, qubits: tuple[int, ...], layout: dict | None = None + ) -> float: + """Get the fidelity for a two-qubit gate on a specific qubit pair. + + Uses per-link error rates from device calibration when available. + + Args: + gate_name: The gate name (e.g., 'cx', 'ecr', 'swap'). + qubits: The virtual qubit indices the gate acts on. + layout: Optional mapping from virtual to physical qubits. + + Returns: + Gate fidelity (0 to 1). + """ + if not self.device.two_qubit_gate_errors: + return self._get_gate_fidelity(gate_name) + + if layout: + physical_qubits = tuple(layout.get(q, q) for q in qubits) + else: + physical_qubits = qubits + + key = (gate_name, physical_qubits) + if key in self.device.two_qubit_gate_errors: + return 1.0 - self.device.two_qubit_gate_errors[key] + + reversed_key = (gate_name, physical_qubits[::-1]) + if reversed_key in self.device.two_qubit_gate_errors: + return 1.0 - self.device.two_qubit_gate_errors[reversed_key] + + for (g, pair), error in self.device.two_qubit_gate_errors.items(): + if set(pair) == set(physical_qubits): + return 1.0 - error + + return self._get_gate_fidelity(gate_name) + def _avg_single_qubit_error(self) -> float: """Compute average single-qubit gate error across all qubits.""" if not self.device.single_qubit_gate_errors: diff --git a/tests/test_cost_model.py b/tests/test_cost_model.py index 9a2740c..1b85e70 100644 --- a/tests/test_cost_model.py +++ b/tests/test_cost_model.py @@ -293,4 +293,79 @@ def test_default_values(self): assert breakdown.gate_fidelity == 1.0 assert breakdown.decoherence_fidelity == 1.0 assert breakdown.measurement_fidelity == 1.0 - assert breakdown.total_fidelity == 1.0 \ No newline at end of file + assert breakdown.total_fidelity == 1.0 + + +class TestLayoutAwareFidelity: + """Regression tests for layout-aware per-qubit error rates (issue #41).""" + + def test_per_qubit_gate_error_with_layout(self): + device = DeviceCharacterization( + backend_name="test_device", + num_qubits=3, + single_qubit_gate_errors={ + ("sx", 0): 0.001, + ("sx", 1): 0.005, + ("sx", 2): 0.002, + }, + two_qubit_gate_errors={ + ("cx", (0, 1)): 0.01, + ("cx", (1, 2)): 0.03, + }, + ) + model = CostModel() + model.device = device + + qc = QuantumCircuit(2) + qc.sx(0) + qc.sx(1) + qc.cx(0, 1) + + error_no_layout = model.estimate_gate_error(qc) + error_with_layout = model.estimate_gate_error(qc, layout={0: 1, 1: 2}) + + assert error_no_layout > 0 + assert error_with_layout > 0 + assert error_no_layout != error_with_layout + + def test_per_qubit_fidelity_uses_specific_qubit_rates(self): + device = DeviceCharacterization( + backend_name="test_device", + num_qubits=2, + single_qubit_gate_errors={ + ("sx", 0): 0.001, + ("sx", 1): 0.01, + }, + two_qubit_gate_errors={ + ("cx", (0, 1)): 0.02, + }, + ) + model = CostModel() + model.device = device + + qc = QuantumCircuit(2) + qc.sx(0) + + fidelity_q0 = model._get_gate_fidelity_for_qubit("sx", 0) + fidelity_q1 = model._get_gate_fidelity_for_qubit("sx", 1) + + assert abs(fidelity_q0 - 0.999) < 1e-6 + assert abs(fidelity_q1 - 0.990) < 1e-6 + + def test_per_pair_fidelity_uses_specific_link_rates(self): + device = DeviceCharacterization( + backend_name="test_device", + num_qubits=3, + two_qubit_gate_errors={ + ("cx", (0, 1)): 0.01, + ("cx", (1, 2)): 0.05, + }, + ) + model = CostModel() + model.device = device + + fidelity_01 = model._get_gate_fidelity_for_pair("cx", (0, 1)) + fidelity_12 = model._get_gate_fidelity_for_pair("cx", (1, 2)) + + assert abs(fidelity_01 - 0.99) < 1e-6 + assert abs(fidelity_12 - 0.95) < 1e-6 \ No newline at end of file