Skip to content

Commit 30e1dc4

Browse files
committed
add support to execut qutip circuits on ionq
1 parent a5e9702 commit 30e1dc4

File tree

6 files changed

+334
-0
lines changed

6 files changed

+334
-0
lines changed

src/qutip_qip/ionq/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Simulation of IonQ circuits in ``qutip_qip``."""
2+
3+
from .backend import IonQSimulator, IonQQPU
4+
from .converter import convert_qutip_circuit
5+
from .job import Job
6+
from .provider import Provider

src/qutip_qip/ionq/backend.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Backends for simulating circuits."""
2+
3+
from .provider import Provider
4+
5+
6+
class IonQBackend:
7+
def __init__(self, provider: Provider, backend: str, gateset: str):
8+
self.provider = provider
9+
self.provider.backend = backend
10+
self.gateset = gateset
11+
12+
def run(self, circuit: dict, shots: int = 1024):
13+
return self.provider.run(circuit, shots=shots)
14+
15+
16+
class IonQSimulator(IonQBackend):
17+
def __init__(self, provider: Provider, gateset: str = "qis"):
18+
super().__init__(provider, "simulator", gateset)
19+
20+
21+
class IonQQPU(IonQBackend):
22+
def __init__(
23+
self,
24+
provider: Provider,
25+
qpu: str = "harmony",
26+
gateset: str = "qis",
27+
):
28+
qpu_name = ".".join(("qpu", qpu)).lower()
29+
super().__init__(provider, qpu_name, gateset)

src/qutip_qip/ionq/converter.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from qutip.qip.circuit import QubitCircuit, Gate
2+
3+
4+
def convert_qutip_circuit(qc: QubitCircuit) -> dict:
5+
"""
6+
Convert a qutip_qip circuit to an IonQ circuit.
7+
8+
Parameters
9+
----------
10+
qc: QubitCircuit
11+
The qutip_qip circuit to be converted.
12+
13+
Returns
14+
-------
15+
dict
16+
The IonQ circuit.
17+
"""
18+
ionq_circuit = []
19+
for gate in qc.gates:
20+
if isinstance(gate, Gate):
21+
ionq_circuit.append(
22+
{
23+
"gate": gate.name,
24+
"targets": gate.targets,
25+
"controls": gate.controls or [],
26+
}
27+
)
28+
return ionq_circuit
29+
30+
31+
def create_job_body(
32+
circuit: dict,
33+
shots: int,
34+
backend: str,
35+
format: str = "ionq.circuit.v0",
36+
) -> dict:
37+
"""
38+
Create the body of a job request.
39+
40+
Parameters
41+
----------
42+
circuit: dict
43+
The IonQ circuit.
44+
shots: int
45+
The number of shots.
46+
backend: str
47+
The simulator or QPU backend.
48+
format: str
49+
The format of the circuit.
50+
51+
Returns
52+
-------
53+
dict
54+
The body of the job request.
55+
"""
56+
return {
57+
"target": backend,
58+
"input": {
59+
"format": format,
60+
"qubits": len(
61+
{
62+
q
63+
for g in circuit
64+
for q in g.get("targets", []) + g.get("controls", [])
65+
}
66+
),
67+
"circuit": circuit,
68+
},
69+
"shots": shots,
70+
}

src/qutip_qip/ionq/job.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Class for a running job."""
2+
3+
from .converter import create_job_body
4+
import requests
5+
import time
6+
7+
8+
class Job:
9+
"""
10+
Class for a running job.
11+
12+
Attributes
13+
----------
14+
body: dict
15+
The body of the job request.
16+
"""
17+
18+
def __init__(
19+
self,
20+
circuit: dict,
21+
shots: int,
22+
backend: str,
23+
headers: dict,
24+
url: str,
25+
) -> None:
26+
self.circuit = circuit
27+
self.shots = shots
28+
self.backend = backend
29+
self.headers = headers
30+
self.url = url
31+
self.id = None
32+
self.results = None
33+
34+
def submit(self) -> None:
35+
"""
36+
Submit the job.
37+
"""
38+
json = create_job_body(
39+
self.circuit,
40+
self.shots,
41+
self.backend,
42+
)
43+
response = requests.post(
44+
f"{self.url}/jobs",
45+
json=json,
46+
headers=self.headers,
47+
)
48+
response.raise_for_status()
49+
self.id = response.json()["id"]
50+
51+
def get_status(self) -> dict:
52+
"""
53+
Get the status of the job.
54+
55+
Returns
56+
-------
57+
dict
58+
The status of the job.
59+
"""
60+
response = requests.get(
61+
f"{self.url}/jobs/{self.id}",
62+
headers=self.headers,
63+
)
64+
response.raise_for_status()
65+
self.status = response.json()
66+
return self.status
67+
68+
def get_results(self, polling_rate: int = 1) -> dict:
69+
"""
70+
Get the results of the job.
71+
72+
Returns
73+
-------
74+
dict
75+
The results of the job.
76+
"""
77+
while self.get_status()["status"] not in (
78+
"canceled",
79+
"completed",
80+
"failed",
81+
):
82+
time.sleep(polling_rate)
83+
response = requests.get(
84+
f"{self.url}/jobs/{self.id}/results",
85+
headers=self.headers,
86+
)
87+
response.raise_for_status()
88+
self.results = {
89+
k: int(round(v * self.shots)) for k, v in response.json().items()
90+
}
91+
return self.results

src/qutip_qip/ionq/provider.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Provider for the simulator backends."""
2+
3+
from .converter import convert_qutip_circuit
4+
from .job import Job
5+
from ..version import version as __version__
6+
from os import getenv
7+
8+
9+
class Provider:
10+
"""
11+
Provides access to qutip_qip based IonQ backends.
12+
13+
Attributes
14+
----------
15+
name: str
16+
Name of the provider
17+
"""
18+
19+
def __init__(
20+
self,
21+
token: str = None,
22+
url: str = "https://api.ionq.co/v0.3",
23+
):
24+
token = token or getenv("IONQ_API_KEY")
25+
if not token:
26+
raise ValueError("No token provided")
27+
self.headers = self.create_headers(token)
28+
self.url = url
29+
self.backend = None
30+
31+
def run(self, circuit, shots: int = 1024) -> Job:
32+
"""
33+
Run a circuit.
34+
35+
Parameters
36+
----------
37+
circuit: QubitCircuit
38+
The circuit to be run.
39+
shots: int
40+
The number of shots.
41+
42+
Returns
43+
-------
44+
Job
45+
The running job.
46+
"""
47+
ionq_circuit = convert_qutip_circuit(circuit)
48+
job = Job(
49+
ionq_circuit,
50+
shots,
51+
self.backend,
52+
self.headers,
53+
self.url,
54+
)
55+
job.submit()
56+
return job
57+
58+
def create_headers(self, token: str):
59+
return {
60+
"Authorization": f"apiKey {token}",
61+
"Content-Type": "application/json",
62+
"User-Agent": f"qutip-qip/{__version__}",
63+
}

tests/test_ionq.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import unittest
2+
from unittest.mock import patch, MagicMock
3+
from qutip.qip.circuit import QubitCircuit, Gate
4+
from qutip.ionq import (
5+
Provider,
6+
IonQSimulator,
7+
IonQQPU,
8+
convert_qutip_circuit,
9+
Job,
10+
)
11+
12+
13+
class TestConverter(unittest.TestCase):
14+
def test_convert_qutip_circuit(self):
15+
# Create a simple QubitCircuit with one gate for testing
16+
qc = QubitCircuit(N=1)
17+
qc.add_gate("X", targets=[0])
18+
# Convert the qutip_qip circuit to IonQ format
19+
ionq_circuit = convert_qutip_circuit(qc)
20+
expected_output = [{"gate": "X", "targets": [0], "controls": []}]
21+
self.assertEqual(ionq_circuit, expected_output)
22+
23+
24+
class TestIonQBackend(unittest.TestCase):
25+
def setUp(self):
26+
self.provider = Provider(token="dummy_token")
27+
28+
@patch("qutip.ionq.Provider")
29+
def test_simulator_initialization(self, mock_provider):
30+
simulator = IonQSimulator(provider=mock_provider)
31+
self.assertEqual(simulator.provider, mock_provider)
32+
self.assertEqual(simulator.gateset, "qis")
33+
34+
@patch("qutip.ionq.Provider")
35+
def test_qpu_initialization(self, mock_provider):
36+
qpu = IonQQPU(provider=mock_provider, qpu="harmony")
37+
self.assertEqual(qpu.provider, mock_provider)
38+
self.assertTrue("qpu.harmony" in qpu.provider.backend)
39+
40+
41+
class TestJob(unittest.TestCase):
42+
@patch("requests.post")
43+
def test_submit(self, mock_post):
44+
mock_post.return_value.json.return_value = {"id": "test_job_id"}
45+
mock_post.return_value.status_code = 200
46+
job = Job(
47+
circuit={},
48+
shots=1024,
49+
backend="simulator",
50+
headers={},
51+
url="http://dummy_url",
52+
)
53+
job.submit()
54+
self.assertEqual(job.id, "test_job_id")
55+
56+
@patch("requests.get")
57+
def test_get_results(self, mock_get):
58+
mock_get.return_value.json.return_value = {"0": 0.5, "1": 0.5}
59+
mock_get.return_value.status_code = 200
60+
job = Job(
61+
circuit={},
62+
shots=1024,
63+
backend="simulator",
64+
headers={},
65+
url="http://dummy_url",
66+
)
67+
job.id = (
68+
"test_job_id" # Simulate a job that has already been submitted
69+
)
70+
results = job.get_results(polling_rate=0)
71+
self.assertEqual(results, {"0": 512, "1": 512})
72+
73+
74+
if __name__ == "__main__":
75+
unittest.main()

0 commit comments

Comments
 (0)