Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable multi circuit submission in PennyLane IonQ, second attempt. #121

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a5ad41c
Adding code for batch circuit submit.
radumarg Jul 31, 2024
24289d1
Merge branch 'master' of https://github.com/PennyLaneAI/PennyLane-Ion…
radumarg Jul 31, 2024
311c09f
Remove unused imports.
radumarg Jul 31, 2024
bad9168
Update version number.
radumarg Jul 31, 2024
b22e88f
Initialize self._samples in overloaded methods.
radumarg Jul 31, 2024
d7e16a9
Various fixes.
radumarg Jul 31, 2024
dede94e
Running existing unit tests from previous implementation and fixing b…
radumarg Aug 6, 2024
50ea3e2
Merge latest changes from master.
radumarg Sep 11, 2024
0755787
Correct unit test after updating pennylane baseline code to latest ve…
radumarg Sep 11, 2024
8c28fee
Merge branch 'master' into multi-circuit-submission-2
Alex-Preciado Sep 17, 2024
77a53a0
Merge branch 'master' of https://github.com/radumarg/PennyLane-IonQ i…
radumarg Sep 19, 2024
1b244fd
Merge branch 'multi-circuit-submission-2' of https://github.com/radum…
radumarg Sep 19, 2024
34be427
Fix codefactor issues.
radumarg Sep 19, 2024
2832900
Run black code formatter.
radumarg Sep 19, 2024
c19af9a
Shots cannot be none in an IonQDevice. Remove check on shots.
radumarg Sep 19, 2024
db1f366
Correct docstring comment.
radumarg Sep 19, 2024
4755b9d
Remove exeception handling code.
radumarg Sep 24, 2024
287c552
Remove self.histogram, replace with self.histograms.
radumarg Sep 24, 2024
d922016
Uniformize treatment of one vs multiple circuits.
radumarg Sep 24, 2024
ec02077
Improve current_circuit_index handling with raising exceptions, add t…
radumarg Sep 25, 2024
8db4dbc
Reset samples in reset function.
radumarg Sep 25, 2024
12d4020
Run black code formatter.
radumarg Sep 25, 2024
ea65129
Remove unused includes.
radumarg Sep 25, 2024
80f221e
Fix codefactor reported issues.
radumarg Sep 25, 2024
9a7ac79
Adding doc string to method.
radumarg Sep 25, 2024
c7102f2
Fix docstring.
radumarg Sep 25, 2024
e8cd6e3
Add test with shot vector.
radumarg Sep 26, 2024
1c20c3e
Add test with an observable that requires rotations for diagonalization.
radumarg Sep 26, 2024
54809ad
Adding unit tests for using pennylane tracker in batch_execute method…
radumarg Sep 26, 2024
17f6f52
Add tests for logging in batch_execute.
radumarg Sep 26, 2024
0c30e00
Run black.
radumarg Sep 26, 2024
87516ab
Remove method override.
radumarg Oct 2, 2024
75d3a3c
Implement review comments.'
radumarg Oct 3, 2024
0b92fa4
Implement more review comments.'
radumarg Oct 3, 2024
142c4cf
Remove unused imports.
radumarg Oct 3, 2024
0aafd79
Remove unused imports.
radumarg Oct 3, 2024
28babc1
Remove checks for BasisState, QubitStateVector and StatePrep because …
radumarg Oct 9, 2024
eae3bcb
Fix code formatting.
radumarg Oct 9, 2024
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 pennylane_ionq/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
Version number (major.minor.patch[-label])
"""

__version__ = "0.37.0-dev"
__version__ = "0.38.0-dev"
218 changes: 180 additions & 38 deletions pennylane_ionq/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,26 @@
"""
This module contains the device class for constructing IonQ devices for PennyLane.
"""
import inspect
import logging
import warnings
from time import sleep

import numpy as np

from pennylane import DeviceError
from pennylane.devices import QubitDevice

from pennylane.measurements import (
Shots,
)
from pennylane.resource import Resources

from .api_client import Job, JobExecutionError
from ._version import __version__

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

_qis_operation_map = {
# native PennyLane operations also native to IonQ
"PauliX": "x",
Expand Down Expand Up @@ -60,6 +69,20 @@
}


class CircuitIndexNotSetException(Exception):
"""Raised when after submitting multiple circuits circuit index is not set
before the user want to access implementation methods of IonQDevice
like probability(), estimate_probability(), sample() or the prob property.
"""

def __init__(self):
self.message = (

Check warning on line 79 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L79

Added line #L79 was not covered by tests
"Because multiple circuits have been submitted in this job, the index of the circuit "
"you want to access must be first set via the set_current_circuit_index device method."
)
super().__init__(self.message)

Check warning on line 83 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L83

Added line #L83 was not covered by tests


class IonQDevice(QubitDevice):
r"""IonQ device for PennyLane.

Expand Down Expand Up @@ -119,26 +142,30 @@
raise ValueError("The ionq device does not support analytic expectation values.")

super().__init__(wires=wires, shots=shots)
self._current_circuit_index = None
self.target = target
self.api_key = api_key
self.gateset = gateset
self.error_mitigation = error_mitigation
self.sharpen = sharpen
self._operation_map = _GATESET_OPS[gateset]
self.histograms = []
self._samples = None
self.reset()

def reset(self):
def reset(self, circuits_array_length=1):
"""Reset the device"""
self._prob_array = None
self.histogram = None
self.circuit = {
self._current_circuit_index = None
self._samples = None
self.histograms = []
self.input = {
"format": "ionq.circuit.v0",
"qubits": self.num_wires,
"circuit": [],
"circuits": [{"circuit": []} for _ in range(circuits_array_length)],
"gateset": self.gateset,
}
self.job = {
"input": self.circuit,
"input": self.input,
"target": self.target,
"shots": self.shots,
}
Expand All @@ -152,6 +179,108 @@
stacklevel=2,
)

def set_current_circuit_index(self, circuit_index):
"""Sets the index of the current circuit for which operations are applied upon.
In case of multiple circuits being submitted via batch_execute method
self._current_circuit_index tracks the index of the current circuit.
"""
self._current_circuit_index = circuit_index

def batch_execute(self, circuits):
"""Execute a batch of quantum circuits on the device.

The circuits are represented by tapes, and they are executed one-by-one using the
device's ``execute`` method. The results are collected in a list.

Args:
circuits (list[~.tape.QuantumTape]): circuits to execute on the device

Returns:
list[array[float]]: list of measured value(s)
"""
if logger.isEnabledFor(logging.DEBUG):
logger.debug( # pragma: no cover
"""Entry with args=(circuits=%s) called by=%s""",
circuits,
"::L".join(
str(i) for i in inspect.getouterframes(inspect.currentframe(), 2)[1][1:3]
),
)

self.reset(circuits_array_length=len(circuits))

for circuit_index, circuit in enumerate(circuits):
self.check_validity(circuit.operations, circuit.observables)
self.batch_apply(
circuit.operations,
rotations=self._get_diagonalizing_gates(circuit),
circuit_index=circuit_index,
)

self._submit_job()

results = []
for circuit_index, circuit in enumerate(circuits):
self.set_current_circuit_index(circuit_index)
self._samples = self.generate_samples()

# compute the required statistics
if self._shot_vector is not None:
result = self.shot_vec_statistics(circuit)

Check warning on line 229 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L229

Added line #L229 was not covered by tests
radumarg marked this conversation as resolved.
Show resolved Hide resolved
else:
result = self.statistics(circuit)
single_measurement = len(circuit.measurements) == 1

result = result[0] if single_measurement else tuple(result)

self.set_current_circuit_index(None)
self._samples = None
results.append(result)

# increment counter for number of executions of qubit device
self._num_executions += 1

if self.tracker.active:
for circuit in circuits:
shots_from_dev = self._shots if not self.shot_vector else self._raw_shot_sequence
tape_resources = circuit.specs["resources"]

Check warning on line 246 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L244-L246

Added lines #L244 - L246 were not covered by tests

resources = Resources( # temporary until shots get updated on tape !

Check warning on line 248 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L248

Added line #L248 was not covered by tests
tape_resources.num_wires,
tape_resources.num_gates,
tape_resources.gate_types,
tape_resources.gate_sizes,
tape_resources.depth,
Shots(shots_from_dev),
)
self.tracker.update(

Check warning on line 256 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L256

Added line #L256 was not covered by tests
executions=1,
shots=self._shots,
results=results,
resources=resources,
)

self.tracker.update(batches=1, batch_len=len(circuits))
self.tracker.record()

Check warning on line 264 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L263-L264

Added lines #L263 - L264 were not covered by tests

return results

def batch_apply(self, operations, circuit_index, **kwargs):

"Apply circuit operations when submitting for execution a batch of circuits."

rotations = kwargs.pop("rotations", [])

if len(operations) == 0 and len(rotations) == 0:
warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.")

Check warning on line 275 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L275

Added line #L275 was not covered by tests

for i, operation in enumerate(operations):

Check notice on line 277 in pennylane_ionq/device.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane_ionq/device.py#L277

Unused variable 'i' (unused-variable)
self._apply_operation(operation, circuit_index)

# diagonalize observables
for operation in rotations:
self._apply_operation(operation, circuit_index)

Check warning on line 282 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L282

Added line #L282 was not covered by tests
radumarg marked this conversation as resolved.
Show resolved Hide resolved

@property
def operations(self):
"""Get the supported set of operations.
Expand All @@ -162,21 +291,15 @@
return set(self._operation_map.keys())

def apply(self, operations, **kwargs):
"""Implementation of QubitDevice abstract method apply."""

self.reset()
rotations = kwargs.pop("rotations", [])

if len(operations) == 0 and len(rotations) == 0:
warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.")

for i, operation in enumerate(operations):

Check notice on line 302 in pennylane_ionq/device.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane_ionq/device.py#L302

Unused variable 'i' (unused-variable)
if i > 0 and operation.name in {
"BasisState",
"QubitStateVector",
"StatePrep",
}:
raise DeviceError(
f"The operation {operation.name} is only supported at the beginning of a circuit."
)
self._apply_operation(operation)

# diagonalize observables
Expand All @@ -185,7 +308,13 @@

self._submit_job()

def _apply_operation(self, operation):
def _apply_operation(self, operation, circuit_index=0):
"""Applies operations to the internal device state.

Args:
operation (.Operation): operation to apply on the device
circuit_index: index of the circuit to apply operation to
"""
name = operation.name
wires = self.map_wires(operation.wires).tolist()
gate = {"gate": self._operation_map[name]}
Expand All @@ -211,9 +340,10 @@
elif par:
gate["rotation"] = float(par[0])

self.circuit["circuit"].append(gate)
self.input["circuits"][circuit_index]["circuit"].append(gate)

def _submit_job(self):

job = Job(api_key=self.api_key)

# send job for exection
Expand All @@ -235,34 +365,46 @@
# state (as a base-10 integer string) to the probability
# as a floating point value between 0 and 1.
# e.g., {"0": 0.413, "9": 0.111, "17": 0.476}
self.histogram = job.data.value
some_inner_value = next(iter(job.data.value.values()))
if isinstance(some_inner_value, dict):
self.histograms = []
for key in job.data.value.keys():
self.histograms.append(job.data.value[key])

Check warning on line 372 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L370-L372

Added lines #L370 - L372 were not covered by tests
radumarg marked this conversation as resolved.
Show resolved Hide resolved
else:
self.histograms = []
self.histograms.append(job.data.value)

@property
def prob(self):
"""None or array[float]: Array of computational basis state probabilities. If
no job has been submitted, returns ``None``.
"""
if self.histogram is None:
return None

if self._prob_array is None:
# The IonQ API returns basis states using little-endian ordering.
# Here, we rearrange the states to match the big-endian ordering
# expected by PennyLane.
basis_states = (
int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) for k in self.histogram
)
idx = np.fromiter(basis_states, dtype=int)
if self._current_circuit_index is None and len(self.histograms) > 1:
raise CircuitIndexNotSetException()

Check warning on line 383 in pennylane_ionq/device.py

View check run for this annotation

Codecov / codecov/patch

pennylane_ionq/device.py#L383

Added line #L383 was not covered by tests

# convert the sparse probs into a probability array
self._prob_array = np.zeros([2**self.num_wires])

# histogram values don't always perfectly sum to exactly one
histogram_values = self.histogram.values()
norm = sum(histogram_values)
self._prob_array[idx] = np.fromiter(histogram_values, float) / norm

return self._prob_array
if self._current_circuit_index is not None:
histogram = self.histograms[self._current_circuit_index]
else:
try:
histogram = self.histograms[0]
except IndexError:
return None

# The IonQ API returns basis states using little-endian ordering.
# Here, we rearrange the states to match the big-endian ordering
# expected by PennyLane.
basis_states = (int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) for k in histogram)
idx = np.fromiter(basis_states, dtype=int)

# convert the sparse probs into a probability array
prob_array = np.zeros([2**self.num_wires])

# histogram values don't always perfectly sum to exactly one
histogram_values = histogram.values()
norm = sum(histogram_values)
prob_array[idx] = np.fromiter(histogram_values, float) / norm

return prob_array

def probability(self, wires=None, shot_range=None, bin_size=None):
wires = wires or self.wires
Expand Down
Loading
Loading