Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'windows-2022', 'macos-latest']
python: ['3.8', '3.9', '3.10', '3.11']
python: ['3.9', '3.10', '3.11', '3.12']

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These versions of Python are not really used: regardless of the version of Python we test here, tox installs and tests Python 3.9, 3.10, and 3.11 in its own environments. That means we run the exact same set of tests four times, and each time we test the three versions of Python listed in tox.ini. We probably want to test only one version of Python at this level (and probably even without explicitly specifying a particular version), and let tox run the tests in its multiple environments.

Copy link
Contributor Author

@d1ssk d1ssk Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment, but actually I don't think the current setup runs every tox env four times. Each github actions job only invokes its corresponding tox environment (e.g. the 3.10 runner only runs py310, the 3.11 runner only runs py311, etc.), although running tox in local environment indeed execute tests in all the versions.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, sorry: I was mistaken, you're right. That is the purpose of the [gh-actions] section in tox.ini. Then everything is fine!


name: "Python ${{ matrix.python }} / ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Updated to support Qiskit 1.0.
- Modified the APIs for Qiskit simulation and execution on IBMQ hardware.

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ First, let us import relevant modules and define function we will use:
.. code-block:: python

from graphix import Circuit
from graphix_ibmq.runner import IBMQBackend
from graphix_ibmq.backend import IBMQBackend
import qiskit.quantum_info as qi
from qiskit.visualization import plot_histogram
import numpy as np
Expand Down
52 changes: 14 additions & 38 deletions examples/gallery/aer_sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
import matplotlib.pyplot as plt
import networkx as nx
import random
from graphix import Circuit
from graphix_ibmq.runner import IBMQBackend
from qiskit.tools.visualization import plot_histogram
from qiskit_aer.noise import NoiseModel, depolarizing_error
from graphix.transpiler import Circuit
from graphix_ibmq.backend import IBMQBackend
from qiskit.visualization import plot_histogram


def cp(circuit, theta, control, target):
Expand Down Expand Up @@ -84,23 +83,27 @@ def swap(circuit, a, b):
pattern.minimize_space()

# convert to qiskit circuit
backend = IBMQBackend(pattern)
print(type(backend.circ))
backend = IBMQBackend.from_simulator()
compiled = backend.compile(pattern)

#%%
# We can now simulate the circuit with Aer.

# run and get counts
result = backend.simulate()
job = backend.submit_job(compiled, shots=1024)
result = job.retrieve_result()

#%%
# We can also simulate the circuit with noise model

# create an empty noise model
from qiskit_aer.noise import NoiseModel, depolarizing_error

# add depolarizing error to all single qubit gates
noise_model = NoiseModel()
# add depolarizing error to all single qubit u1, u2, u3 gates
error = depolarizing_error(0.01, 1)
noise_model.add_all_qubit_quantum_error(error, ["u1", "u2", "u3"])
noise_model.add_all_qubit_quantum_error(error, ["id", "rz", "sx", "x", "u1"])
backend = IBMQBackend.from_simulator(noise_model=noise_model)

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

# run and get counts
result_noise = backend.simulate(noise_model=noise_model)
job = backend.submit_job(compiled, shots=1024)
result_noise = job.retrieve_result()


#%%
Expand Down Expand Up @@ -137,31 +141,3 @@ def swap(circuit, a, b):
legend = ax.legend(fontsize=18)
legend = ax.legend(loc='upper left')
# %%


#%%
# Example demonstrating how to run a pattern on an IBM Quantum device. All explanations are provided as comments.

# First, load the IBMQ account using an API token.
"""
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService(channel="ibm_quantum", token="your_ibm_token", instance="ibm-q/open/main")
"""

# Then, select the quantum system on which to run the circuit.
# If no system is specified, the least busy system will be automatically selected.
"""
backend.get_system(service, "ibm_kyoto")
"""

# Finally, transpile the quantum circuit for the chosen system and execute it.
"""
backend.transpile()
result = backend.run(shots=128)
"""

# To retrieve the result at a later time, use the code below.
"""
result = backend.retrieve_result("your_job_id")
"""
# %%
2 changes: 1 addition & 1 deletion examples/gallery/qiskit_to_graphix.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


# %%
from qiskit import QuantumCircuit, transpile
from qiskit import transpile
from qiskit.circuit.random.utils import random_circuit

qc = random_circuit(5, 2, seed=42)
Expand Down
47 changes: 20 additions & 27 deletions examples/ibm_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
import matplotlib.pyplot as plt
import networkx as nx
import random
from graphix import Circuit
from graphix_ibmq.runner import IBMQBackend
from qiskit_ibm_provider import IBMProvider
from qiskit.tools.visualization import plot_histogram
from graphix.transpiler import Circuit
from graphix_ibmq.backend import IBMQBackend
from qiskit.visualization import plot_histogram
from qiskit.providers.fake_provider import FakeLagos


Expand Down Expand Up @@ -68,7 +67,7 @@ def swap(circuit, a, b):
swap(circuit, 0, 2)

# transpile and plot the graph
pattern = circuit.transpile()
pattern = circuit.transpile().pattern
nodes, edges = pattern.get_graph()
g = nx.Graph()
g.add_nodes_from(nodes)
Expand All @@ -84,45 +83,39 @@ def swap(circuit, a, b):
pattern.minimize_space()

# convert to qiskit circuit
backend = IBMQBackend(pattern)
backend.to_qiskit()
print(type(backend.circ))
backend = IBMQBackend.from_simulator()
compiled = backend.compile(pattern)

#%%
# load the account with API token
IBMProvider.save_account(token='MY API TOKEN')
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(channel="ibm_quantum", token="API TOKEN", overwrite=True)

# get the device backend
instance_name = 'ibm-q/open/main'
backend_name = "ibm_lagos"
backend.get_backend(instance=instance_name,resource=backend_name)

#%%
# Get provider and the backend.

instance_name = "ibm-q/open/main"
backend_name = "ibm_lagos"

backend.get_backend(instance=instance_name, resource=backend_name)
backend = IBMQBackend.from_hardware()

#%%
# We can now execute the circuit on the device backend.

result = backend.run()
compiled = backend.compile(pattern)
job = backend.submit_job(compiled, shots=1024)

#%%
# Retrieve the job if needed
# Retrieve the job result

# result = backend.retrieve_result("Job ID")
if job.is_done:
result = job.retrieve_result()

#%%
# We can simulate the circuit with noise model based on the device we used
# We can simulate the circuit with device-based noise model.

# get the noise model of the device backend
backend_noisemodel = FakeLagos()
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
backend = IBMQBackend.from_simulator(from_backend=FakeManilaV2())

# execute noisy simulation and get counts
result_noise = backend.simulate(noise_model=backend_noisemodel)
compiled = backend.compile(pattern)
job = backend.submit_job(compiled, shots=1024)
result_noise = job.retrieve_result()

#%%
# Now let us compare the results with theoretical output
Expand Down
147 changes: 147 additions & 0 deletions graphix_ibmq/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import logging

from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService

from graphix_ibmq.compile_options import IBMQCompileOptions
from graphix_ibmq.compiler import IBMQPatternCompiler, IBMQCompiledCircuit
from graphix_ibmq.job import IBMQJob

if TYPE_CHECKING:
from graphix.pattern import Pattern
from qiskit.providers.backend import BackendV2, Backend

logger = logging.getLogger(__name__)


class IBMQBackend:
"""
Manages compilation and execution on IBMQ simulators or hardware.
This class configures the execution target and provides methods to compile
a graphix Pattern and submit it as a job. Instances should be created using
the `from_simulator` or `from_hardware` classmethods.
"""

def __init__(self, backend: Backend | None = None, options: IBMQCompileOptions | None = None) -> None:
if backend is None or options is None:
raise TypeError(
"IBMQBackend cannot be instantiated directly. "
"Please use the classmethods `IBMQBackend.from_simulator()` "
"or `IBMQBackend.from_hardware()`."
)
self._backend: Backend = backend
self._options: IBMQCompileOptions = options

@classmethod
def from_simulator(
cls,
noise_model: NoiseModel | None = None,
from_backend: BackendV2 | None = None,
options: IBMQCompileOptions | None = None,
) -> IBMQBackend:
"""Creates an instance with a local Aer simulator as the backend.
Parameters
----------
noise_model : NoiseModel, optional
A custom noise model for the simulation.
from_backend : BackendV2, optional
A hardware backend to base the noise model on.
Ignored if `noise_model` is provided.
options : IBMQCompileOptions, optional
Compilation and execution options.
"""
if noise_model is None and from_backend is not None:
noise_model = NoiseModel.from_backend(from_backend)

aer_backend = AerSimulator(noise_model=noise_model)
compile_options = options if options is not None else IBMQCompileOptions()

logger.info("Backend set to local AerSimulator.")
return cls(backend=aer_backend, options=compile_options)

@classmethod
def from_hardware(
cls,
name: str | None = None,
least_busy: bool = False,
min_qubits: int = 1,
options: IBMQCompileOptions | None = None,
) -> IBMQBackend:
"""Creates an instance with a real IBM Quantum hardware device as the backend.
Parameters
----------
name : str, optional
The specific name of the device (e.g., 'ibm_brisbane').
least_busy : bool
If True, selects the least busy device meeting the criteria.
min_qubits : int
The minimum number of qubits required.
options : IBMQCompileOptions, optional
Compilation and execution options.
"""
service = QiskitRuntimeService()
if name:
hw_backend = service.backend(name)
else:
hw_backend = service.least_busy(min_num_qubits=min_qubits, operational=True)

compile_options = options if options is not None else IBMQCompileOptions()

logger.info("Selected hardware backend: %s", hw_backend.name)
return cls(backend=hw_backend, options=compile_options)

@staticmethod
def compile(pattern: Pattern, save_statevector: bool = False) -> IBMQCompiledCircuit:
"""Compiles a graphix pattern into a Qiskit QuantumCircuit.
This method is provided as a staticmethod because it does not depend
on the backend's state.
Parameters
----------
pattern : Pattern
The graphix pattern to compile.
save_statevector : bool
If True, saves the statevector before the final measurement.
Returns
-------
IBMQCompiledCircuit
An object containing the compiled circuit and related metadata.
"""
compiler = IBMQPatternCompiler(pattern)
return compiler.compile(save_statevector=save_statevector)

def submit_job(self, compiled_circuit: IBMQCompiledCircuit, shots: int = 1024) -> IBMQJob:
"""
Submits the compiled circuit to the configured backend for execution.
Parameters
----------
compiled_circuit : IBMQCompiledCircuit
The compiled circuit object from the `compile` method.
shots : int, optional
The number of execution shots. Defaults to 1024.
Returns
-------
IBMQJob
A job object to monitor execution and retrieve results.
"""
pass_manager = generate_preset_pass_manager(
backend=self._backend,
optimization_level=self._options.optimization_level,
)
transpiled_circuit = pass_manager.run(compiled_circuit.circuit)

sampler = Sampler(mode=self._backend)
job = sampler.run([transpiled_circuit], shots=shots)

return IBMQJob(job, compiled_circuit)
30 changes: 30 additions & 0 deletions graphix_ibmq/compile_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class IBMQCompileOptions:
"""Compilation options specific to IBMQ backends.

Attributes
----------
optimization_level : int
Optimization level for Qiskit transpiler (0 to 3).
save_statevector : bool
Whether to save the statevector before measurement (for debugging/testing).
layout_method : str
Qubit layout method used by the transpiler (for future use).
"""

optimization_level: int = 3
save_statevector: bool = False
layout_method: str = "trivial"

def __repr__(self) -> str:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is automatically implemented by dataclass.

"""Return a string representation of the compilation options."""
return (
f"IBMQCompileOptions(optimization_level={self.optimization_level}, "
f"save_statevector={self.save_statevector}, "
f"layout_method='{self.layout_method}')"
)
Loading
Loading