From 30e1dc416473a4f109c2dd50b9539c51479d8b57 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Tue, 19 Mar 2024 09:59:46 -0700 Subject: [PATCH 1/7] add support to execut qutip circuits on ionq --- src/qutip_qip/ionq/__init__.py | 6 +++ src/qutip_qip/ionq/backend.py | 29 +++++++++++ src/qutip_qip/ionq/converter.py | 70 +++++++++++++++++++++++++ src/qutip_qip/ionq/job.py | 91 +++++++++++++++++++++++++++++++++ src/qutip_qip/ionq/provider.py | 63 +++++++++++++++++++++++ tests/test_ionq.py | 75 +++++++++++++++++++++++++++ 6 files changed, 334 insertions(+) create mode 100644 src/qutip_qip/ionq/__init__.py create mode 100644 src/qutip_qip/ionq/backend.py create mode 100644 src/qutip_qip/ionq/converter.py create mode 100644 src/qutip_qip/ionq/job.py create mode 100644 src/qutip_qip/ionq/provider.py create mode 100644 tests/test_ionq.py diff --git a/src/qutip_qip/ionq/__init__.py b/src/qutip_qip/ionq/__init__.py new file mode 100644 index 00000000..34755e47 --- /dev/null +++ b/src/qutip_qip/ionq/__init__.py @@ -0,0 +1,6 @@ +"""Simulation of IonQ circuits in ``qutip_qip``.""" + +from .backend import IonQSimulator, IonQQPU +from .converter import convert_qutip_circuit +from .job import Job +from .provider import Provider diff --git a/src/qutip_qip/ionq/backend.py b/src/qutip_qip/ionq/backend.py new file mode 100644 index 00000000..85732620 --- /dev/null +++ b/src/qutip_qip/ionq/backend.py @@ -0,0 +1,29 @@ +"""Backends for simulating circuits.""" + +from .provider import Provider + + +class IonQBackend: + def __init__(self, provider: Provider, backend: str, gateset: str): + self.provider = provider + self.provider.backend = backend + self.gateset = gateset + + def run(self, circuit: dict, shots: int = 1024): + return self.provider.run(circuit, shots=shots) + + +class IonQSimulator(IonQBackend): + def __init__(self, provider: Provider, gateset: str = "qis"): + super().__init__(provider, "simulator", gateset) + + +class IonQQPU(IonQBackend): + def __init__( + self, + provider: Provider, + qpu: str = "harmony", + gateset: str = "qis", + ): + qpu_name = ".".join(("qpu", qpu)).lower() + super().__init__(provider, qpu_name, gateset) diff --git a/src/qutip_qip/ionq/converter.py b/src/qutip_qip/ionq/converter.py new file mode 100644 index 00000000..af5c4cfa --- /dev/null +++ b/src/qutip_qip/ionq/converter.py @@ -0,0 +1,70 @@ +from qutip.qip.circuit import QubitCircuit, Gate + + +def convert_qutip_circuit(qc: QubitCircuit) -> dict: + """ + Convert a qutip_qip circuit to an IonQ circuit. + + Parameters + ---------- + qc: QubitCircuit + The qutip_qip circuit to be converted. + + Returns + ------- + dict + The IonQ circuit. + """ + ionq_circuit = [] + for gate in qc.gates: + if isinstance(gate, Gate): + ionq_circuit.append( + { + "gate": gate.name, + "targets": gate.targets, + "controls": gate.controls or [], + } + ) + return ionq_circuit + + +def create_job_body( + circuit: dict, + shots: int, + backend: str, + format: str = "ionq.circuit.v0", +) -> dict: + """ + Create the body of a job request. + + Parameters + ---------- + circuit: dict + The IonQ circuit. + shots: int + The number of shots. + backend: str + The simulator or QPU backend. + format: str + The format of the circuit. + + Returns + ------- + dict + The body of the job request. + """ + return { + "target": backend, + "input": { + "format": format, + "qubits": len( + { + q + for g in circuit + for q in g.get("targets", []) + g.get("controls", []) + } + ), + "circuit": circuit, + }, + "shots": shots, + } diff --git a/src/qutip_qip/ionq/job.py b/src/qutip_qip/ionq/job.py new file mode 100644 index 00000000..532d86be --- /dev/null +++ b/src/qutip_qip/ionq/job.py @@ -0,0 +1,91 @@ +"""Class for a running job.""" + +from .converter import create_job_body +import requests +import time + + +class Job: + """ + Class for a running job. + + Attributes + ---------- + body: dict + The body of the job request. + """ + + def __init__( + self, + circuit: dict, + shots: int, + backend: str, + headers: dict, + url: str, + ) -> None: + self.circuit = circuit + self.shots = shots + self.backend = backend + self.headers = headers + self.url = url + self.id = None + self.results = None + + def submit(self) -> None: + """ + Submit the job. + """ + json = create_job_body( + self.circuit, + self.shots, + self.backend, + ) + response = requests.post( + f"{self.url}/jobs", + json=json, + headers=self.headers, + ) + response.raise_for_status() + self.id = response.json()["id"] + + def get_status(self) -> dict: + """ + Get the status of the job. + + Returns + ------- + dict + The status of the job. + """ + response = requests.get( + f"{self.url}/jobs/{self.id}", + headers=self.headers, + ) + response.raise_for_status() + self.status = response.json() + return self.status + + def get_results(self, polling_rate: int = 1) -> dict: + """ + Get the results of the job. + + Returns + ------- + dict + The results of the job. + """ + while self.get_status()["status"] not in ( + "canceled", + "completed", + "failed", + ): + time.sleep(polling_rate) + response = requests.get( + f"{self.url}/jobs/{self.id}/results", + headers=self.headers, + ) + response.raise_for_status() + self.results = { + k: int(round(v * self.shots)) for k, v in response.json().items() + } + return self.results diff --git a/src/qutip_qip/ionq/provider.py b/src/qutip_qip/ionq/provider.py new file mode 100644 index 00000000..5de1f5f3 --- /dev/null +++ b/src/qutip_qip/ionq/provider.py @@ -0,0 +1,63 @@ +"""Provider for the simulator backends.""" + +from .converter import convert_qutip_circuit +from .job import Job +from ..version import version as __version__ +from os import getenv + + +class Provider: + """ + Provides access to qutip_qip based IonQ backends. + + Attributes + ---------- + name: str + Name of the provider + """ + + def __init__( + self, + token: str = None, + url: str = "https://api.ionq.co/v0.3", + ): + token = token or getenv("IONQ_API_KEY") + if not token: + raise ValueError("No token provided") + self.headers = self.create_headers(token) + self.url = url + self.backend = None + + def run(self, circuit, shots: int = 1024) -> Job: + """ + Run a circuit. + + Parameters + ---------- + circuit: QubitCircuit + The circuit to be run. + shots: int + The number of shots. + + Returns + ------- + Job + The running job. + """ + ionq_circuit = convert_qutip_circuit(circuit) + job = Job( + ionq_circuit, + shots, + self.backend, + self.headers, + self.url, + ) + job.submit() + return job + + def create_headers(self, token: str): + return { + "Authorization": f"apiKey {token}", + "Content-Type": "application/json", + "User-Agent": f"qutip-qip/{__version__}", + } diff --git a/tests/test_ionq.py b/tests/test_ionq.py new file mode 100644 index 00000000..997d8445 --- /dev/null +++ b/tests/test_ionq.py @@ -0,0 +1,75 @@ +import unittest +from unittest.mock import patch, MagicMock +from qutip.qip.circuit import QubitCircuit, Gate +from qutip.ionq import ( + Provider, + IonQSimulator, + IonQQPU, + convert_qutip_circuit, + Job, +) + + +class TestConverter(unittest.TestCase): + def test_convert_qutip_circuit(self): + # Create a simple QubitCircuit with one gate for testing + qc = QubitCircuit(N=1) + qc.add_gate("X", targets=[0]) + # Convert the qutip_qip circuit to IonQ format + ionq_circuit = convert_qutip_circuit(qc) + expected_output = [{"gate": "X", "targets": [0], "controls": []}] + self.assertEqual(ionq_circuit, expected_output) + + +class TestIonQBackend(unittest.TestCase): + def setUp(self): + self.provider = Provider(token="dummy_token") + + @patch("qutip.ionq.Provider") + def test_simulator_initialization(self, mock_provider): + simulator = IonQSimulator(provider=mock_provider) + self.assertEqual(simulator.provider, mock_provider) + self.assertEqual(simulator.gateset, "qis") + + @patch("qutip.ionq.Provider") + def test_qpu_initialization(self, mock_provider): + qpu = IonQQPU(provider=mock_provider, qpu="harmony") + self.assertEqual(qpu.provider, mock_provider) + self.assertTrue("qpu.harmony" in qpu.provider.backend) + + +class TestJob(unittest.TestCase): + @patch("requests.post") + def test_submit(self, mock_post): + mock_post.return_value.json.return_value = {"id": "test_job_id"} + mock_post.return_value.status_code = 200 + job = Job( + circuit={}, + shots=1024, + backend="simulator", + headers={}, + url="http://dummy_url", + ) + job.submit() + self.assertEqual(job.id, "test_job_id") + + @patch("requests.get") + def test_get_results(self, mock_get): + mock_get.return_value.json.return_value = {"0": 0.5, "1": 0.5} + mock_get.return_value.status_code = 200 + job = Job( + circuit={}, + shots=1024, + backend="simulator", + headers={}, + url="http://dummy_url", + ) + job.id = ( + "test_job_id" # Simulate a job that has already been submitted + ) + results = job.get_results(polling_rate=0) + self.assertEqual(results, {"0": 512, "1": 512}) + + +if __name__ == "__main__": + unittest.main() From 734724b1bc93b7fa29bd8fa7ab64d1f7147e85ee Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Tue, 19 Mar 2024 11:02:19 -0700 Subject: [PATCH 2/7] remove unnecessary call --- tests/test_ionq.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_ionq.py b/tests/test_ionq.py index 997d8445..31e17ce1 100644 --- a/tests/test_ionq.py +++ b/tests/test_ionq.py @@ -69,7 +69,3 @@ def test_get_results(self, mock_get): ) results = job.get_results(polling_rate=0) self.assertEqual(results, {"0": 512, "1": 512}) - - -if __name__ == "__main__": - unittest.main() From ee10b4d0bf677a7459ada75f4e112f86cec13701 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Wed, 20 Mar 2024 09:38:48 -0700 Subject: [PATCH 3/7] pass tests and parse results as quantumresult --- src/qutip_qip/ionq/converter.py | 50 +++++++++++++++++++++++++++++++-- src/qutip_qip/ionq/job.py | 10 +++---- tests/test_ionq.py | 27 ++++++++++++------ 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/qutip_qip/ionq/converter.py b/src/qutip_qip/ionq/converter.py index af5c4cfa..61b38bc2 100644 --- a/src/qutip_qip/ionq/converter.py +++ b/src/qutip_qip/ionq/converter.py @@ -1,4 +1,6 @@ -from qutip.qip.circuit import QubitCircuit, Gate +from qutip import Qobj +from qutip_qip.circuit import QubitCircuit, CircuitResult +import numpy as np def convert_qutip_circuit(qc: QubitCircuit) -> dict: @@ -17,7 +19,11 @@ def convert_qutip_circuit(qc: QubitCircuit) -> dict: """ ionq_circuit = [] for gate in qc.gates: - if isinstance(gate, Gate): + if ( + hasattr(gate, "name") + and hasattr(gate, "targets") + and hasattr(gate, "controls") + ): ionq_circuit.append( { "gate": gate.name, @@ -28,6 +34,46 @@ def convert_qutip_circuit(qc: QubitCircuit) -> dict: return ionq_circuit +def convert_ionq_response_to_circuitresult( + ionq_response: dict, +) -> CircuitResult: + """ + Convert an IonQ response to a CircuitResult. + + Parameters + ---------- + ionq_response: dict + The IonQ response. + + Returns + ------- + CircuitResult + The CircuitResult. + """ + states = list(ionq_response.keys()) + # probabilities = list(ionq_response.values()) + + max_state = max(int(state) for state in states) + num_qubits = int(np.ceil(np.log2(max_state + 1))) if max_state > 0 else 1 + + final_states = [] + final_probabilities = [] + + for state in states: + binary_state = format(int(state), "0{}b".format(num_qubits)) + state_vector = np.zeros((2**num_qubits,), dtype=complex) + index = int(binary_state, 2) + state_vector[index] = 1.0 + + qobj_state = Qobj( + state_vector, dims=[[2] * num_qubits, [1] * num_qubits] + ) + final_states.append(qobj_state) + final_probabilities.append(ionq_response[state]) + + return CircuitResult(final_states, final_probabilities) + + def create_job_body( circuit: dict, shots: int, diff --git a/src/qutip_qip/ionq/job.py b/src/qutip_qip/ionq/job.py index 532d86be..db8f2c9e 100644 --- a/src/qutip_qip/ionq/job.py +++ b/src/qutip_qip/ionq/job.py @@ -1,6 +1,7 @@ """Class for a running job.""" -from .converter import create_job_body +from .converter import create_job_body, convert_ionq_response_to_circuitresult +from qutip_qip.circuit import CircuitResult import requests import time @@ -65,7 +66,7 @@ def get_status(self) -> dict: self.status = response.json() return self.status - def get_results(self, polling_rate: int = 1) -> dict: + def get_results(self, polling_rate: int = 1) -> CircuitResult: """ Get the results of the job. @@ -85,7 +86,4 @@ def get_results(self, polling_rate: int = 1) -> dict: headers=self.headers, ) response.raise_for_status() - self.results = { - k: int(round(v * self.shots)) for k, v in response.json().items() - } - return self.results + return convert_ionq_response_to_circuitresult(response.json()) diff --git a/tests/test_ionq.py b/tests/test_ionq.py index 31e17ce1..5e8ff86f 100644 --- a/tests/test_ionq.py +++ b/tests/test_ionq.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import patch, MagicMock -from qutip.qip.circuit import QubitCircuit, Gate -from qutip.ionq import ( +from qutip_qip.circuit import QubitCircuit, Gate +from qutip_qip.ionq import ( Provider, IonQSimulator, IonQQPU, @@ -14,10 +14,14 @@ class TestConverter(unittest.TestCase): def test_convert_qutip_circuit(self): # Create a simple QubitCircuit with one gate for testing qc = QubitCircuit(N=1) - qc.add_gate("X", targets=[0]) + qc.add_gate("H", targets=[0]) + qc.add_gate("CNOT", targets=[0], controls=[1]) # Convert the qutip_qip circuit to IonQ format ionq_circuit = convert_qutip_circuit(qc) - expected_output = [{"gate": "X", "targets": [0], "controls": []}] + expected_output = [ + {"gate": "H", "targets": [0], "controls": []}, + {"gate": "CNOT", "targets": [0], "controls": [1]}, + ] self.assertEqual(ionq_circuit, expected_output) @@ -25,13 +29,13 @@ class TestIonQBackend(unittest.TestCase): def setUp(self): self.provider = Provider(token="dummy_token") - @patch("qutip.ionq.Provider") + @patch("qutip_qip.ionq.Provider") def test_simulator_initialization(self, mock_provider): simulator = IonQSimulator(provider=mock_provider) self.assertEqual(simulator.provider, mock_provider) self.assertEqual(simulator.gateset, "qis") - @patch("qutip.ionq.Provider") + @patch("qutip_qip.ionq.Provider") def test_qpu_initialization(self, mock_provider): qpu = IonQQPU(provider=mock_provider, qpu="harmony") self.assertEqual(qpu.provider, mock_provider) @@ -55,8 +59,15 @@ def test_submit(self, mock_post): @patch("requests.get") def test_get_results(self, mock_get): - mock_get.return_value.json.return_value = {"0": 0.5, "1": 0.5} - mock_get.return_value.status_code = 200 + # Simulate the status check response and the final result response + mock_get.side_effect = [ + MagicMock( + json=lambda: {"status": "completed"} + ), # Simulated status check response + MagicMock( + json=lambda: {"0": 0.5, "1": 0.5} + ), # Simulated final results response + ] job = Job( circuit={}, shots=1024, From a9d9c3e9a98b21b7c5481b1016729fa1969e058c Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Wed, 20 Mar 2024 09:44:52 -0700 Subject: [PATCH 4/7] pass tests --- src/qutip_qip/ionq/__init__.py | 5 ++++- tests/test_ionq.py | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/qutip_qip/ionq/__init__.py b/src/qutip_qip/ionq/__init__.py index 34755e47..8f45d54c 100644 --- a/src/qutip_qip/ionq/__init__.py +++ b/src/qutip_qip/ionq/__init__.py @@ -1,6 +1,9 @@ """Simulation of IonQ circuits in ``qutip_qip``.""" from .backend import IonQSimulator, IonQQPU -from .converter import convert_qutip_circuit +from .converter import ( + convert_qutip_circuit, + convert_ionq_response_to_circuitresult, +) from .job import Job from .provider import Provider diff --git a/tests/test_ionq.py b/tests/test_ionq.py index 5e8ff86f..ec91b4dd 100644 --- a/tests/test_ionq.py +++ b/tests/test_ionq.py @@ -6,6 +6,7 @@ IonQSimulator, IonQQPU, convert_qutip_circuit, + convert_ionq_response_to_circuitresult, Job, ) @@ -79,4 +80,9 @@ def test_get_results(self, mock_get): "test_job_id" # Simulate a job that has already been submitted ) results = job.get_results(polling_rate=0) - self.assertEqual(results, {"0": 512, "1": 512}) + self.assertEqual( + results.get_final_states(), + convert_ionq_response_to_circuitresult( + {"0": 0.5, "1": 0.5} + ).get_final_states(), + ) From f0af635ab3154e12b98d05d0ae385588f28de390 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 22 Mar 2024 13:33:13 -0700 Subject: [PATCH 5/7] fix gateset passing --- src/qutip_qip/ionq/backend.py | 8 +++++-- src/qutip_qip/ionq/converter.py | 39 ++++++++++++++++++++++++--------- src/qutip_qip/ionq/job.py | 4 ++++ src/qutip_qip/ionq/provider.py | 1 + tests/test_ionq.py | 8 ++++--- 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/qutip_qip/ionq/backend.py b/src/qutip_qip/ionq/backend.py index 85732620..cc3300d0 100644 --- a/src/qutip_qip/ionq/backend.py +++ b/src/qutip_qip/ionq/backend.py @@ -7,14 +7,18 @@ class IonQBackend: def __init__(self, provider: Provider, backend: str, gateset: str): self.provider = provider self.provider.backend = backend - self.gateset = gateset + self.provider.gateset = gateset def run(self, circuit: dict, shots: int = 1024): return self.provider.run(circuit, shots=shots) class IonQSimulator(IonQBackend): - def __init__(self, provider: Provider, gateset: str = "qis"): + def __init__( + self, + provider: Provider, + gateset: str = "qis", + ): super().__init__(provider, "simulator", gateset) diff --git a/src/qutip_qip/ionq/converter.py b/src/qutip_qip/ionq/converter.py index 61b38bc2..62c8c5bd 100644 --- a/src/qutip_qip/ionq/converter.py +++ b/src/qutip_qip/ionq/converter.py @@ -24,13 +24,24 @@ def convert_qutip_circuit(qc: QubitCircuit) -> dict: and hasattr(gate, "targets") and hasattr(gate, "controls") ): - ionq_circuit.append( - { - "gate": gate.name, - "targets": gate.targets, - "controls": gate.controls or [], - } - ) + g = { + "gate": gate.name, + } + if gate.targets is not None: + if len(gate.targets) == 1: + g["target"] = gate.targets[0] + else: + g["targets"] = gate.targets + if gate.controls is not None: + if len(gate.controls) == 1: + g["control"] = gate.controls[0] + else: + g["controls"] = gate.controls + if hasattr(gate, "arg_value") and gate.arg_value is not None: + g["angle"] = gate.arg_value + g["phase"] = gate.arg_value + g["rotation"] = gate.arg_value + ionq_circuit.append(g) return ionq_circuit @@ -78,6 +89,7 @@ def create_job_body( circuit: dict, shots: int, backend: str, + gateset: str, format: str = "ionq.circuit.v0", ) -> dict: """ @@ -91,6 +103,8 @@ def create_job_body( The number of shots. backend: str The simulator or QPU backend. + gateset: str + Either native or compiled gates. format: str The format of the circuit. @@ -101,16 +115,21 @@ def create_job_body( """ return { "target": backend, + "shots": shots, "input": { "format": format, + "gateset": gateset, + "circuit": circuit, "qubits": len( { q for g in circuit - for q in g.get("targets", []) + g.get("controls", []) + for q in g.get("targets", []) + + g.get("controls", []) + + [g.get("target")] + + [g.get("control")] + if q is not None } ), - "circuit": circuit, }, - "shots": shots, } diff --git a/src/qutip_qip/ionq/job.py b/src/qutip_qip/ionq/job.py index db8f2c9e..1c0cf150 100644 --- a/src/qutip_qip/ionq/job.py +++ b/src/qutip_qip/ionq/job.py @@ -21,12 +21,14 @@ def __init__( circuit: dict, shots: int, backend: str, + gateset: str, headers: dict, url: str, ) -> None: self.circuit = circuit self.shots = shots self.backend = backend + self.gateset = gateset self.headers = headers self.url = url self.id = None @@ -40,7 +42,9 @@ def submit(self) -> None: self.circuit, self.shots, self.backend, + self.gateset, ) + print(json) response = requests.post( f"{self.url}/jobs", json=json, diff --git a/src/qutip_qip/ionq/provider.py b/src/qutip_qip/ionq/provider.py index 5de1f5f3..17d1f974 100644 --- a/src/qutip_qip/ionq/provider.py +++ b/src/qutip_qip/ionq/provider.py @@ -49,6 +49,7 @@ def run(self, circuit, shots: int = 1024) -> Job: ionq_circuit, shots, self.backend, + self.gateset, self.headers, self.url, ) diff --git a/tests/test_ionq.py b/tests/test_ionq.py index ec91b4dd..02d261d8 100644 --- a/tests/test_ionq.py +++ b/tests/test_ionq.py @@ -20,8 +20,8 @@ def test_convert_qutip_circuit(self): # Convert the qutip_qip circuit to IonQ format ionq_circuit = convert_qutip_circuit(qc) expected_output = [ - {"gate": "H", "targets": [0], "controls": []}, - {"gate": "CNOT", "targets": [0], "controls": [1]}, + {"gate": "H", "target": 0}, + {"gate": "CNOT", "target": 0, "control": 1}, ] self.assertEqual(ionq_circuit, expected_output) @@ -34,7 +34,7 @@ def setUp(self): def test_simulator_initialization(self, mock_provider): simulator = IonQSimulator(provider=mock_provider) self.assertEqual(simulator.provider, mock_provider) - self.assertEqual(simulator.gateset, "qis") + self.assertEqual(mock_provider.gateset, "qis") @patch("qutip_qip.ionq.Provider") def test_qpu_initialization(self, mock_provider): @@ -52,6 +52,7 @@ def test_submit(self, mock_post): circuit={}, shots=1024, backend="simulator", + gateset="qis", headers={}, url="http://dummy_url", ) @@ -73,6 +74,7 @@ def test_get_results(self, mock_get): circuit={}, shots=1024, backend="simulator", + gateset="qis", headers={}, url="http://dummy_url", ) From 914c8015f582bea6f985c02f01cae6c5813f96e7 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Sun, 24 Mar 2024 02:19:31 -0700 Subject: [PATCH 6/7] rename provider + simplify circuit conversion --- src/qutip_qip/ionq/__init__.py | 2 +- src/qutip_qip/ionq/backend.py | 8 +-- src/qutip_qip/ionq/converter.py | 88 ++++++++++++++++----------------- src/qutip_qip/ionq/provider.py | 4 +- tests/test_ionq.py | 8 +-- 5 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/qutip_qip/ionq/__init__.py b/src/qutip_qip/ionq/__init__.py index 8f45d54c..6f954ef0 100644 --- a/src/qutip_qip/ionq/__init__.py +++ b/src/qutip_qip/ionq/__init__.py @@ -6,4 +6,4 @@ convert_ionq_response_to_circuitresult, ) from .job import Job -from .provider import Provider +from .provider import IonQProvider diff --git a/src/qutip_qip/ionq/backend.py b/src/qutip_qip/ionq/backend.py index cc3300d0..f898878f 100644 --- a/src/qutip_qip/ionq/backend.py +++ b/src/qutip_qip/ionq/backend.py @@ -1,10 +1,10 @@ """Backends for simulating circuits.""" -from .provider import Provider +from .provider import IonQProvider class IonQBackend: - def __init__(self, provider: Provider, backend: str, gateset: str): + def __init__(self, provider: IonQProvider, backend: str, gateset: str): self.provider = provider self.provider.backend = backend self.provider.gateset = gateset @@ -16,7 +16,7 @@ def run(self, circuit: dict, shots: int = 1024): class IonQSimulator(IonQBackend): def __init__( self, - provider: Provider, + provider: IonQProvider, gateset: str = "qis", ): super().__init__(provider, "simulator", gateset) @@ -25,7 +25,7 @@ def __init__( class IonQQPU(IonQBackend): def __init__( self, - provider: Provider, + provider: IonQProvider, qpu: str = "harmony", gateset: str = "qis", ): diff --git a/src/qutip_qip/ionq/converter.py b/src/qutip_qip/ionq/converter.py index 62c8c5bd..3549ad1d 100644 --- a/src/qutip_qip/ionq/converter.py +++ b/src/qutip_qip/ionq/converter.py @@ -19,70 +19,70 @@ def convert_qutip_circuit(qc: QubitCircuit) -> dict: """ ionq_circuit = [] for gate in qc.gates: - if ( - hasattr(gate, "name") - and hasattr(gate, "targets") - and hasattr(gate, "controls") - ): - g = { - "gate": gate.name, - } - if gate.targets is not None: - if len(gate.targets) == 1: - g["target"] = gate.targets[0] - else: - g["targets"] = gate.targets - if gate.controls is not None: - if len(gate.controls) == 1: - g["control"] = gate.controls[0] - else: - g["controls"] = gate.controls - if hasattr(gate, "arg_value") and gate.arg_value is not None: - g["angle"] = gate.arg_value - g["phase"] = gate.arg_value - g["rotation"] = gate.arg_value - ionq_circuit.append(g) + g = {"gate": gate.name} + # Map target(s) and control(s) depending on the number of qubits + for attr, key in (("targets", "target"), ("controls", "control")): + items = getattr(gate, attr, None) + if items: + g[key if len(items) == 1 else key + "s"] = ( + items[0] if len(items) == 1 else items + ) + # Include arg_value as angle, phase, and rotation if it exists + if getattr(gate, "arg_value", None) is not None: + g.update( + { + "angle": gate.arg_value, + "phase": gate.arg_value, + "rotation": gate.arg_value, + } + ) + ionq_circuit.append(g) return ionq_circuit -def convert_ionq_response_to_circuitresult( - ionq_response: dict, -) -> CircuitResult: +def convert_ionq_response_to_circuitresult(ionq_response: dict): """ Convert an IonQ response to a CircuitResult. Parameters ---------- ionq_response: dict - The IonQ response. + The IonQ response {state: probability, ...}. Returns ------- CircuitResult The CircuitResult. """ - states = list(ionq_response.keys()) - # probabilities = list(ionq_response.values()) + # Calculate the number of qubits based on the binary representation of the highest state + num_qubits = max(len(state) for state in ionq_response.keys()) - max_state = max(int(state) for state in states) - num_qubits = int(np.ceil(np.log2(max_state + 1))) if max_state > 0 else 1 + # Initialize an empty density matrix for the mixed state + density_matrix = np.zeros((2**num_qubits, 2**num_qubits), dtype=complex) - final_states = [] - final_probabilities = [] + # Iterate over the measurement outcomes and their probabilities + for state, probability in ionq_response.items(): + # Ensure state string is correctly padded for single-qubit cases + binary_state = format(int(state, base=10), f"0{num_qubits}b") + index = int(binary_state, 2) # Convert binary string back to integer - for state in states: - binary_state = format(int(state), "0{}b".format(num_qubits)) + # Update the density matrix to include this measurement outcome state_vector = np.zeros((2**num_qubits,), dtype=complex) - index = int(binary_state, 2) - state_vector[index] = 1.0 - - qobj_state = Qobj( - state_vector, dims=[[2] * num_qubits, [1] * num_qubits] + state_vector[index] = ( + 1.0 # Pure state corresponding to the measurement outcome ) - final_states.append(qobj_state) - final_probabilities.append(ionq_response[state]) - - return CircuitResult(final_states, final_probabilities) + density_matrix += probability * np.outer( + state_vector, state_vector.conj() + ) # Add weighted outer product + + # Convert the numpy array to a Qobj density matrix + qobj_density_matrix = Qobj( + density_matrix, dims=[[2] * num_qubits, [2] * num_qubits] + ) + + return CircuitResult( + [qobj_density_matrix], [1.0] + ) # Return the density matrix wrapped in CircuitResult def create_job_body( diff --git a/src/qutip_qip/ionq/provider.py b/src/qutip_qip/ionq/provider.py index 17d1f974..b8734ee6 100644 --- a/src/qutip_qip/ionq/provider.py +++ b/src/qutip_qip/ionq/provider.py @@ -1,4 +1,4 @@ -"""Provider for the simulator backends.""" +"""Provider for the IonQ backends.""" from .converter import convert_qutip_circuit from .job import Job @@ -6,7 +6,7 @@ from os import getenv -class Provider: +class IonQProvider: """ Provides access to qutip_qip based IonQ backends. diff --git a/tests/test_ionq.py b/tests/test_ionq.py index 02d261d8..5fac17c9 100644 --- a/tests/test_ionq.py +++ b/tests/test_ionq.py @@ -2,7 +2,7 @@ from unittest.mock import patch, MagicMock from qutip_qip.circuit import QubitCircuit, Gate from qutip_qip.ionq import ( - Provider, + IonQProvider, IonQSimulator, IonQQPU, convert_qutip_circuit, @@ -28,15 +28,15 @@ def test_convert_qutip_circuit(self): class TestIonQBackend(unittest.TestCase): def setUp(self): - self.provider = Provider(token="dummy_token") + self.provider = IonQProvider(token="dummy_token") - @patch("qutip_qip.ionq.Provider") + @patch("qutip_qip.ionq.IonQProvider") def test_simulator_initialization(self, mock_provider): simulator = IonQSimulator(provider=mock_provider) self.assertEqual(simulator.provider, mock_provider) self.assertEqual(mock_provider.gateset, "qis") - @patch("qutip_qip.ionq.Provider") + @patch("qutip_qip.ionq.IonQProvider") def test_qpu_initialization(self, mock_provider): qpu = IonQQPU(provider=mock_provider, qpu="harmony") self.assertEqual(qpu.provider, mock_provider) From cb6cdc0daf48e6f423aea5a4d8bc9864942d5ec5 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 25 Mar 2024 21:25:41 +0800 Subject: [PATCH 7/7] remove json print --- src/qutip_qip/ionq/job.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qutip_qip/ionq/job.py b/src/qutip_qip/ionq/job.py index 1c0cf150..c542d3cb 100644 --- a/src/qutip_qip/ionq/job.py +++ b/src/qutip_qip/ionq/job.py @@ -44,7 +44,6 @@ def submit(self) -> None: self.backend, self.gateset, ) - print(json) response = requests.post( f"{self.url}/jobs", json=json,