Skip to content

Commit b995d07

Browse files
authored
Refactoring API structure (#19)
* refactor API structure * add docstring * add tests * edit setup.py * update tox.ini * update requirement.txt * add py3.12, remove py3.8 in test and support * edit ci.yml * black * update tox.ini * update tox.ini * remove py3.12 from support and test * remove py3.12 from support and test * delete abstract class in graphix * update test * update test * refactor according to the review * unpin dependencies * edit setup and ci * remove python version specification in ci.yml * remove python setup in ci.yml * recover ci.yml * refactor * black * Redesign IBMQBackend for improved type safety * reflect the review * Ensure type safety * black * add test fot job.py * black
1 parent f229e1c commit b995d07

25 files changed

+898
-552
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
fail-fast: false
2727
matrix:
2828
os: ['ubuntu-latest', 'windows-2022', 'macos-latest']
29-
python: ['3.8', '3.9', '3.10', '3.11']
29+
python: ['3.9', '3.10', '3.11', '3.12']
3030

3131
name: "Python ${{ matrix.python }} / ${{ matrix.os }}"
3232
runs-on: ${{ matrix.os }}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313
- Updated to support Qiskit 1.0.
14+
- Modified the APIs for Qiskit simulation and execution on IBMQ hardware.
1415

1516
### Fixed
1617

docs/source/tutorial.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ First, let us import relevant modules and define function we will use:
2929
.. code-block:: python
3030
3131
from graphix import Circuit
32-
from graphix_ibmq.runner import IBMQBackend
32+
from graphix_ibmq.backend import IBMQBackend
3333
import qiskit.quantum_info as qi
3434
from qiskit.visualization import plot_histogram
3535
import numpy as np

examples/gallery/aer_sim.py

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
import matplotlib.pyplot as plt
1313
import networkx as nx
1414
import random
15-
from graphix import Circuit
16-
from graphix_ibmq.runner import IBMQBackend
17-
from qiskit.tools.visualization import plot_histogram
18-
from qiskit_aer.noise import NoiseModel, depolarizing_error
15+
from graphix.transpiler import Circuit
16+
from graphix_ibmq.backend import IBMQBackend
17+
from qiskit.visualization import plot_histogram
1918

2019

2120
def cp(circuit, theta, control, target):
@@ -84,23 +83,27 @@ def swap(circuit, a, b):
8483
pattern.minimize_space()
8584

8685
# convert to qiskit circuit
87-
backend = IBMQBackend(pattern)
88-
print(type(backend.circ))
86+
backend = IBMQBackend.from_simulator()
87+
compiled = backend.compile(pattern)
8988

9089
#%%
9190
# We can now simulate the circuit with Aer.
9291

9392
# run and get counts
94-
result = backend.simulate()
93+
job = backend.submit_job(compiled, shots=1024)
94+
result = job.retrieve_result()
9595

9696
#%%
9797
# We can also simulate the circuit with noise model
9898

9999
# create an empty noise model
100+
from qiskit_aer.noise import NoiseModel, depolarizing_error
101+
102+
# add depolarizing error to all single qubit gates
100103
noise_model = NoiseModel()
101-
# add depolarizing error to all single qubit u1, u2, u3 gates
102104
error = depolarizing_error(0.01, 1)
103-
noise_model.add_all_qubit_quantum_error(error, ["u1", "u2", "u3"])
105+
noise_model.add_all_qubit_quantum_error(error, ["id", "rz", "sx", "x", "u1"])
106+
backend = IBMQBackend.from_simulator(noise_model=noise_model)
104107

105108
# print noise model info
106109
print(noise_model)
@@ -109,7 +112,8 @@ def swap(circuit, a, b):
109112
# Now we can run the simulation with noise model
110113

111114
# run and get counts
112-
result_noise = backend.simulate(noise_model=noise_model)
115+
job = backend.submit_job(compiled, shots=1024)
116+
result_noise = job.retrieve_result()
113117

114118

115119
#%%
@@ -137,31 +141,3 @@ def swap(circuit, a, b):
137141
legend = ax.legend(fontsize=18)
138142
legend = ax.legend(loc='upper left')
139143
# %%
140-
141-
142-
#%%
143-
# Example demonstrating how to run a pattern on an IBM Quantum device. All explanations are provided as comments.
144-
145-
# First, load the IBMQ account using an API token.
146-
"""
147-
from qiskit_ibm_runtime import QiskitRuntimeService
148-
service = QiskitRuntimeService(channel="ibm_quantum", token="your_ibm_token", instance="ibm-q/open/main")
149-
"""
150-
151-
# Then, select the quantum system on which to run the circuit.
152-
# If no system is specified, the least busy system will be automatically selected.
153-
"""
154-
backend.get_system(service, "ibm_kyoto")
155-
"""
156-
157-
# Finally, transpile the quantum circuit for the chosen system and execute it.
158-
"""
159-
backend.transpile()
160-
result = backend.run(shots=128)
161-
"""
162-
163-
# To retrieve the result at a later time, use the code below.
164-
"""
165-
result = backend.retrieve_result("your_job_id")
166-
"""
167-
# %%

examples/gallery/qiskit_to_graphix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
# %%
12-
from qiskit import QuantumCircuit, transpile
12+
from qiskit import transpile
1313
from qiskit.circuit.random.utils import random_circuit
1414

1515
qc = random_circuit(5, 2, seed=42)

examples/ibm_device.py

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@
1212
import matplotlib.pyplot as plt
1313
import networkx as nx
1414
import random
15-
from graphix import Circuit
16-
from graphix_ibmq.runner import IBMQBackend
17-
from qiskit_ibm_provider import IBMProvider
18-
from qiskit.tools.visualization import plot_histogram
15+
from graphix.transpiler import Circuit
16+
from graphix_ibmq.backend import IBMQBackend
17+
from qiskit.visualization import plot_histogram
1918
from qiskit.providers.fake_provider import FakeLagos
2019

2120

@@ -68,7 +67,7 @@ def swap(circuit, a, b):
6867
swap(circuit, 0, 2)
6968

7069
# transpile and plot the graph
71-
pattern = circuit.transpile()
70+
pattern = circuit.transpile().pattern
7271
nodes, edges = pattern.get_graph()
7372
g = nx.Graph()
7473
g.add_nodes_from(nodes)
@@ -84,45 +83,39 @@ def swap(circuit, a, b):
8483
pattern.minimize_space()
8584

8685
# convert to qiskit circuit
87-
backend = IBMQBackend(pattern)
88-
backend.to_qiskit()
89-
print(type(backend.circ))
86+
backend = IBMQBackend.from_simulator()
87+
compiled = backend.compile(pattern)
9088

9189
#%%
9290
# load the account with API token
93-
IBMProvider.save_account(token='MY API TOKEN')
91+
from qiskit_ibm_runtime import QiskitRuntimeService
92+
QiskitRuntimeService.save_account(channel="ibm_quantum", token="API TOKEN", overwrite=True)
9493

9594
# get the device backend
96-
instance_name = 'ibm-q/open/main'
97-
backend_name = "ibm_lagos"
98-
backend.get_backend(instance=instance_name,resource=backend_name)
99-
100-
#%%
101-
# Get provider and the backend.
102-
103-
instance_name = "ibm-q/open/main"
104-
backend_name = "ibm_lagos"
105-
106-
backend.get_backend(instance=instance_name, resource=backend_name)
95+
backend = IBMQBackend.from_hardware()
10796

10897
#%%
10998
# We can now execute the circuit on the device backend.
110-
111-
result = backend.run()
99+
compiled = backend.compile(pattern)
100+
job = backend.submit_job(compiled, shots=1024)
112101

113102
#%%
114-
# Retrieve the job if needed
103+
# Retrieve the job result
115104

116-
# result = backend.retrieve_result("Job ID")
105+
if job.is_done:
106+
result = job.retrieve_result()
117107

118108
#%%
119-
# We can simulate the circuit with noise model based on the device we used
109+
# We can simulate the circuit with device-based noise model.
120110

121111
# get the noise model of the device backend
122-
backend_noisemodel = FakeLagos()
112+
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
113+
backend = IBMQBackend.from_simulator(from_backend=FakeManilaV2())
123114

124115
# execute noisy simulation and get counts
125-
result_noise = backend.simulate(noise_model=backend_noisemodel)
116+
compiled = backend.compile(pattern)
117+
job = backend.submit_job(compiled, shots=1024)
118+
result_noise = job.retrieve_result()
126119

127120
#%%
128121
# Now let us compare the results with theoretical output

graphix_ibmq/backend.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
3+
import logging
4+
5+
from qiskit_aer import AerSimulator
6+
from qiskit_aer.noise import NoiseModel
7+
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
8+
from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService
9+
10+
from graphix_ibmq.compile_options import IBMQCompileOptions
11+
from graphix_ibmq.compiler import IBMQPatternCompiler, IBMQCompiledCircuit
12+
from graphix_ibmq.job import IBMQJob
13+
14+
if TYPE_CHECKING:
15+
from graphix.pattern import Pattern
16+
from qiskit.providers.backend import BackendV2
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class IBMQBackend:
22+
"""
23+
Manages compilation and execution on IBMQ simulators or hardware.
24+
25+
This class configures the execution target and provides methods to compile
26+
a graphix Pattern and submit it as a job. Instances should be created using
27+
the `from_simulator` or `from_hardware` classmethods.
28+
"""
29+
30+
def __init__(self, backend: BackendV2 | None = None, options: IBMQCompileOptions | None = None) -> None:
31+
if backend is None or options is None:
32+
raise TypeError(
33+
"IBMQBackend cannot be instantiated directly. "
34+
"Please use the classmethods `IBMQBackend.from_simulator()` "
35+
"or `IBMQBackend.from_hardware()`."
36+
)
37+
self._backend: BackendV2 = backend
38+
self._options: IBMQCompileOptions = options
39+
40+
@classmethod
41+
def from_simulator(
42+
cls,
43+
noise_model: NoiseModel | None = None,
44+
from_backend: BackendV2 | None = None,
45+
options: IBMQCompileOptions | None = None,
46+
) -> IBMQBackend:
47+
"""Creates an instance with a local Aer simulator as the backend.
48+
49+
Parameters
50+
----------
51+
noise_model : NoiseModel, optional
52+
A custom noise model for the simulation.
53+
from_backend : BackendV2, optional
54+
A hardware backend to base the noise model on.
55+
Ignored if `noise_model` is provided.
56+
options : IBMQCompileOptions, optional
57+
Compilation and execution options.
58+
"""
59+
if noise_model is None and from_backend is not None:
60+
noise_model = NoiseModel.from_backend(from_backend)
61+
62+
aer_backend = AerSimulator(noise_model=noise_model)
63+
compile_options = options if options is not None else IBMQCompileOptions()
64+
65+
logger.info("Backend set to local AerSimulator.")
66+
return cls(backend=aer_backend, options=compile_options)
67+
68+
@classmethod
69+
def from_hardware(
70+
cls,
71+
name: str | None = None,
72+
min_qubits: int = 1,
73+
options: IBMQCompileOptions | None = None,
74+
) -> IBMQBackend:
75+
"""Creates an instance with a real IBM Quantum hardware device as the backend.
76+
77+
Parameters
78+
----------
79+
name : str, optional
80+
The specific name of the device (e.g., 'ibm_brisbane'). If None,
81+
the least busy backend with at least `min_qubits` will be selected.
82+
min_qubits : int
83+
The minimum number of qubits required.
84+
options : IBMQCompileOptions, optional
85+
Compilation and execution options.
86+
"""
87+
service = QiskitRuntimeService()
88+
if name:
89+
hw_backend = service.backend(name)
90+
else:
91+
hw_backend = service.least_busy(min_num_qubits=min_qubits, operational=True)
92+
93+
compile_options = options if options is not None else IBMQCompileOptions()
94+
95+
logger.info("Selected hardware backend: %s", hw_backend.name)
96+
return cls(backend=hw_backend, options=compile_options)
97+
98+
@staticmethod
99+
def compile(pattern: Pattern, save_statevector: bool = False) -> IBMQCompiledCircuit:
100+
"""Compiles a graphix pattern into a Qiskit QuantumCircuit.
101+
102+
This method is provided as a staticmethod because it does not depend
103+
on the backend's state.
104+
105+
Parameters
106+
----------
107+
pattern : Pattern
108+
The graphix pattern to compile.
109+
save_statevector : bool
110+
If True, saves the statevector before the final measurement.
111+
112+
Returns
113+
-------
114+
IBMQCompiledCircuit
115+
An object containing the compiled circuit and related metadata.
116+
"""
117+
compiler = IBMQPatternCompiler(pattern)
118+
return compiler.compile(save_statevector=save_statevector)
119+
120+
def submit_job(self, compiled_circuit: IBMQCompiledCircuit, shots: int = 1024) -> IBMQJob:
121+
"""
122+
Submits the compiled circuit to the configured backend for execution.
123+
124+
Parameters
125+
----------
126+
compiled_circuit : IBMQCompiledCircuit
127+
The compiled circuit object from the `compile` method.
128+
shots : int, optional
129+
The number of execution shots. Defaults to 1024.
130+
131+
Returns
132+
-------
133+
IBMQJob
134+
A job object to monitor execution and retrieve results.
135+
"""
136+
pass_manager = generate_preset_pass_manager(
137+
backend=self._backend,
138+
optimization_level=self._options.optimization_level,
139+
)
140+
transpiled_circuit = pass_manager.run(compiled_circuit.circuit)
141+
142+
sampler = Sampler(mode=self._backend)
143+
job = sampler.run([transpiled_circuit], shots=shots)
144+
145+
return IBMQJob(job, compiled_circuit)

graphix_ibmq/compile_options.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass
7+
class IBMQCompileOptions:
8+
"""Compilation options specific to IBMQ backends.
9+
10+
Attributes
11+
----------
12+
optimization_level : int
13+
Optimization level for Qiskit transpiler (0 to 3).
14+
save_statevector : bool
15+
Whether to save the statevector before measurement (for debugging/testing).
16+
layout_method : str
17+
Qubit layout method used by the transpiler (for future use).
18+
"""
19+
20+
optimization_level: int = 3
21+
save_statevector: bool = False
22+
layout_method: str = "trivial"
23+
24+
def __repr__(self) -> str:
25+
"""Return a string representation of the compilation options."""
26+
return (
27+
f"IBMQCompileOptions(optimization_level={self.optimization_level}, "
28+
f"save_statevector={self.save_statevector}, "
29+
f"layout_method='{self.layout_method}')"
30+
)

0 commit comments

Comments
 (0)