Skip to content

Commit

Permalink
Merge pull request #109 from Jami-ronkko/add-fake-Deneb
Browse files Browse the repository at this point in the history
Add fake backend for IQM Deneb.
  • Loading branch information
Aerylia authored Jul 31, 2024
2 parents a748714 + 547aa29 commit 81eb3d7
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 22 deletions.
2 changes: 1 addition & 1 deletion src/iqm/qiskit_iqm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.
"""Qiskit adapter for IQM's quantum computers.
"""
from iqm.qiskit_iqm.fake_backends import IQMErrorProfile, IQMFakeAdonis, IQMFakeApollo
from iqm.qiskit_iqm.fake_backends import IQMErrorProfile, IQMFakeAdonis, IQMFakeApollo, IQMFakeDeneb
from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMFakeBackend
from iqm.qiskit_iqm.iqm_circuit import IQMCircuit
from iqm.qiskit_iqm.iqm_job import IQMJob
Expand Down
1 change: 1 addition & 0 deletions src/iqm/qiskit_iqm/fake_backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@

from .fake_adonis import IQMFakeAdonis
from .fake_apollo import IQMFakeApollo
from .fake_deneb import IQMFakeDeneb
from .iqm_fake_backend import IQMErrorProfile, IQMFakeBackend
118 changes: 118 additions & 0 deletions src/iqm/qiskit_iqm/fake_backends/fake_deneb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright 2022-2023 Qiskit on IQM developers
#
# 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.
"""Fake backend for IQM's 6-qubit Deneb architecture.
"""
from iqm.iqm_client import QuantumArchitectureSpecification
from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMErrorProfile, IQMFakeBackend


def IQMFakeDeneb() -> IQMFakeBackend:
"""Return IQMFakeBackend instance representing IQM's Deneb architecture."""

architecture = QuantumArchitectureSpecification(
name="Deneb",
operations={
"prx": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"], ["QB6"]],
"cz": [
["QB1", "COMP_R"],
["QB2", "COMP_R"],
["QB3", "COMP_R"],
["QB4", "COMP_R"],
["QB5", "COMP_R"],
["QB6", "COMP_R"],
],
"move": [
["QB1", "COMP_R"],
["QB2", "COMP_R"],
["QB3", "COMP_R"],
["QB4", "COMP_R"],
["QB5", "COMP_R"],
["QB6", "COMP_R"],
],
"measure": [["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"], ["QB6"]],
"barrier": [],
},
qubits=["COMP_R", "QB1", "QB2", "QB3", "QB4", "QB5", "QB6"],
qubit_connectivity=[
["QB1", "COMP_R"],
["QB2", "COMP_R"],
["QB3", "COMP_R"],
["QB4", "COMP_R"],
["QB5", "COMP_R"],
["QB6", "COMP_R"],
],
)
error_profile = IQMErrorProfile(
t1s={
"COMP_R": 5400.0,
"QB1": 35000.0,
"QB2": 35000.0,
"QB3": 35000.0,
"QB4": 35000.0,
"QB5": 35000.0,
"QB6": 35000.0,
},
t2s={
"COMP_R": 10800.0,
"QB1": 33000.0,
"QB2": 33000.0,
"QB3": 33000.0,
"QB4": 33000.0,
"QB5": 33000.0,
"QB6": 33000.0,
},
single_qubit_gate_depolarizing_error_parameters={
"prx": {
"COMP_R": 0.0,
"QB1": 0.0002,
"QB2": 0.0002,
"QB3": 0.0002,
"QB4": 0.0002,
"QB5": 0.0002,
"QB6": 0.0002,
}
},
two_qubit_gate_depolarizing_error_parameters={
"cz": {
("QB1", "COMP_R"): 0.0128,
("QB2", "COMP_R"): 0.0128,
("QB3", "COMP_R"): 0.0128,
("QB4", "COMP_R"): 0.0128,
("QB5", "COMP_R"): 0.0128,
("QB6", "COMP_R"): 0.0128,
},
"move": {
("QB1", "COMP_R"): 0.0,
("QB2", "COMP_R"): 0.0,
("QB3", "COMP_R"): 0.0,
("QB4", "COMP_R"): 0.0,
("QB5", "COMP_R"): 0.0,
("QB6", "COMP_R"): 0.0,
},
},
single_qubit_gate_durations={"prx": 40.0},
two_qubit_gate_durations={"cz": 120.0, "move": 96.0},
readout_errors={
"COMP_R": {"0": 0.0, "1": 0.0},
"QB1": {"0": 0.977, "1": 0.977},
"QB2": {"0": 0.977, "1": 0.977},
"QB3": {"0": 0.977, "1": 0.977},
"QB4": {"0": 0.977, "1": 0.977},
"QB5": {"0": 0.977, "1": 0.977},
"QB6": {"0": 0.977, "1": 0.977},
},
name="sample-chip",
)

return IQMFakeBackend(architecture, error_profile, name="IQMFakeDenebBackend")
91 changes: 82 additions & 9 deletions src/iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@

from qiskit import QuantumCircuit
from qiskit.providers import JobV1, Options
from qiskit.transpiler import TransformationPass
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit_aer.noise.errors import depolarizing_error, thermal_relaxation_error

from iqm.iqm_client import QuantumArchitectureSpecification
from iqm.qiskit_iqm.iqm_backend import IQM_TO_QISKIT_GATE_NAME, IQMBackendBase
from iqm.qiskit_iqm.iqm_circuit import IQMCircuit


# pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -221,8 +223,12 @@ def _create_noise_model(
"""
Builds a noise model from the attributes.
"""
noise_model = NoiseModel(basis_gates=["r", "cz"])

iqm_to_qiskit_gates = dict(IQM_TO_QISKIT_GATE_NAME)
for iqm_gate in architecture.operations:
if iqm_gate not in ["measure", "barrier"] + list(iqm_to_qiskit_gates):
iqm_to_qiskit_gates[iqm_gate] = iqm_gate
noise_model = NoiseModel(basis_gates=list(iqm_to_qiskit_gates.values()))
# Add single-qubit gate errors to noise model
for gate in error_profile.single_qubit_gate_depolarizing_error_parameters.keys():
for qb in architecture.qubits:
Expand All @@ -234,7 +240,7 @@ def _create_noise_model(
)
full_error_channel = thermal_relaxation_channel.compose(depolarizing_channel)
noise_model.add_quantum_error(
full_error_channel, IQM_TO_QISKIT_GATE_NAME[gate], [self.qubit_name_to_index(qb)]
full_error_channel, iqm_to_qiskit_gates[gate], [self.qubit_name_to_index(qb)]
)

# Add two-qubit gate errors to noise model
Expand All @@ -256,7 +262,7 @@ def _create_noise_model(
full_error_channel = thermal_relaxation_channel.compose(depolarizing_channel)
noise_model.add_quantum_error(
full_error_channel,
IQM_TO_QISKIT_GATE_NAME[gate],
iqm_to_qiskit_gates[gate],
[self.qubit_name_to_index(qb_order[0]), self.qubit_name_to_index(qb_order[1])],
)

Expand All @@ -275,14 +281,18 @@ def _default_options(cls) -> Options:
def max_circuits(self) -> Optional[int]:
return None

def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options) -> JobV1:
def run(
self, run_input: Union[QuantumCircuit, list[QuantumCircuit], IQMCircuit, list[IQMCircuit]], **options
) -> JobV1:
"""
Run `run_input` on the fake backend using a simulator.
This method runs circuit jobs (an individual or a list of QuantumCircuit
) and returns a :class:`~qiskit.providers.JobV1` object.
This method runs circuit jobs (an individual or a list of QuantumCircuit or IQMCircuit )
and returns a :class:`~qiskit.providers.JobV1` object.
It will run the simulation with a noise model of the fake backend (e.g. Adonis).
It will run the simulation with a noise model of the fake backend (e.g. Adonis, Deneb).
Validity of move gates is also checked. The method also transpiles circuit
to the native gates so that moves are implemented as unitaries.
Args:
run_input: One or more quantum circuits to simulate on the backend.
Expand All @@ -292,15 +302,78 @@ def run(self, run_input: Union[QuantumCircuit, list[QuantumCircuit]], **options)
Raises:
ValueError: If empty list of circuits is provided.
"""
circuits = [run_input] if isinstance(run_input, QuantumCircuit) else run_input
circuits_aux = [run_input] if isinstance(run_input, (QuantumCircuit, IQMCircuit)) else run_input

if len(circuits) == 0:
if len(circuits_aux) == 0:
raise ValueError("Empty list of circuits submitted for execution.")

this = self

class check_move_validity(TransformationPass):
"""Checks that the placement of move gates is valid in the circuit."""

def run(self, dag):
qubits_involved_in_last_move = None # Store which qubit was last used for MOVE IN
for node in dag.op_nodes():
if node.op.name not in this.noise_model.basis_gates + ["id", "barrier", "measure", "measurement"]:
raise ValueError("Operation '" + node.op.name + "' is not supported by the backend.")
if qubits_involved_in_last_move is not None:
# Verify that no single qubit gate is performed on the qubit between MOVE IN and MOVE OUT
if (
node.op.name not in ["move", "barrier", "measure"]
and len(node.qargs) == 1
and node.qargs[0] == qubits_involved_in_last_move[0]
):
raise ValueError(
"Operations to qubits '{'QB"
+ str(qubits_involved_in_last_move[0].index + 1)
+ "'}' while their states are moved to a resonator."
)
if node.op.name == "move":
if qubits_involved_in_last_move is None:
# MOVE IN was performed
qubits_involved_in_last_move = node.qargs
elif qubits_involved_in_last_move != node.qargs:
raise ValueError(
"Cannot apply MOVE on 'QB"
+ str(node.qargs[0].index + 1)
+ "' because COMP_R already holds the state of 'QB"
+ str(qubits_involved_in_last_move[0].index + 1)
+ "'."
)
else:
# MOVE OUT was performed
qubits_involved_in_last_move = None

if qubits_involved_in_last_move is not None:
raise ValueError(
"The following resonators are still holding qubit states "
+ "at the end of the circuit: {'COMP_R': 'QB"
+ str(qubits_involved_in_last_move[0].index + 1)
+ "'}."
)

return dag

circuits = []

for circ in circuits_aux:
circ_to_add = circ

if "move" in self.noise_model.basis_gates:
check_move_validity()(circ)

for iqm_gate in [
g for g in self.noise_model.basis_gates if g not in list(IQM_TO_QISKIT_GATE_NAME.values())
]:
circ_to_add = circ.decompose(gates_to_decompose=iqm_gate)
circuits.append(circ_to_add)

shots = options.get("shots", self.options.shots)

# Create noisy simulator backend and run circuits
sim_noise = AerSimulator(noise_model=self.noise_model)

job = sim_noise.run(circuits, shots=shots)

return job
20 changes: 9 additions & 11 deletions src/iqm/qiskit_iqm/move_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"""Move gate to be used with Qiskit Quantum Circuits."""

from qiskit.circuit import Gate
from qiskit.circuit.library import CXGate
from qiskit.circuit.quantumcircuit import QuantumCircuit, QuantumRegister
import qiskit.quantum_info as qi


class MoveGate(Gate):
Expand All @@ -40,23 +40,21 @@ class MoveGate(Gate):
def __init__(self, label=None):
"""Initializes the move gate"""
super().__init__("move", 2, [], label=label)
self.unitary = qi.Operator(
[[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]]
)

def _define(self):
"""Pretend that this gate is an SWAP for the purpose of matrix checking.
"""Pretend that this gate is a SWAP for the purpose of matrix checking.
The |0> needs to be traced out for the resonator 'qubits'.
gate swap a,b {
cx q[0],q[1];
cx q[1],q[0];
cx q[0],q[1];
}
gate swap a,b
"""

q = QuantumRegister(2, "q")
qc = QuantumCircuit(q, name=self.name)
rules = [(CXGate(), [q[0], q[1]], []), (CXGate(), [q[1], q[0]], []), (CXGate(), [q[0], q[1]], [])]
for instr, qargs, cargs in rules:
qc._append(instr, qargs, cargs)
qc = QuantumCircuit(q, name=self.label if self.label else self.name)

qc.unitary(self.unitary, [q[0], q[1]], label=self.name)

self.definition = qc
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def adonis_coupling_map():
return {(0, 2), (2, 0), (1, 2), (2, 1), (2, 3), (3, 2), (2, 4), (4, 2)}


@pytest.fixture
def deneb_coupling_map():
return {(1, 0), (0, 1), (2, 0), (0, 2), (3, 0), (0, 3), (4, 0), (0, 4), (5, 0), (0, 5), (6, 0), (0, 6)}


@pytest.fixture
def ndonis_architecture() -> QuantumArchitectureSpecification:
return QuantumArchitectureSpecification(**ndonis_architecture_specification)
Expand Down
Loading

0 comments on commit 81eb3d7

Please sign in to comment.