diff --git a/pennylane_ionq/_version.py b/pennylane_ionq/_version.py index 649c3c9..09de5c8 100644 --- a/pennylane_ionq/_version.py +++ b/pennylane_ionq/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.37.0-dev" +__version__ = "0.38.0-dev" diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 6a7f9b3..926c6a7 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -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", @@ -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 = ( + "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) + + class IonQDevice(QubitDevice): r"""IonQ device for PennyLane. @@ -119,26 +142,30 @@ def __init__( 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, } @@ -152,6 +179,108 @@ def reset(self): 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) + 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"] + + resources = Resources( # temporary until shots get updated on tape ! + 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( + executions=1, + shots=self._shots, + results=results, + resources=resources, + ) + + self.tracker.update(batches=1, batch_len=len(circuits)) + self.tracker.record() + + 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.") + + for i, operation in enumerate(operations): + self._apply_operation(operation, circuit_index) + + # diagonalize observables + for operation in rotations: + self._apply_operation(operation, circuit_index) + @property def operations(self): """Get the supported set of operations. @@ -162,6 +291,8 @@ def operations(self): return set(self._operation_map.keys()) def apply(self, operations, **kwargs): + """Implementation of QubitDevice abstract method apply.""" + self.reset() rotations = kwargs.pop("rotations", []) @@ -169,14 +300,6 @@ def apply(self, operations, **kwargs): warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.") for i, operation in enumerate(operations): - 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 @@ -185,7 +308,13 @@ def apply(self, operations, **kwargs): 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]} @@ -211,9 +340,10 @@ def _apply_operation(self, operation): 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 @@ -235,34 +365,46 @@ def _submit_job(self): # 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]) + 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() - # 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 diff --git a/tests/test_device.py b/tests/test_device.py index 286b3e5..26fa8c4 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -13,16 +13,23 @@ # limitations under the License. """Tests that plugin devices are accessible and integrate with PennyLane""" import json +import logging import numpy as np import pennylane as qml import pytest import requests -from unittest.mock import PropertyMock, patch from conftest import shortnames -from pennylane_ionq.api_client import JobExecutionError, ResourceManager, Job, Field -from pennylane_ionq.device import QPUDevice, IonQDevice +from pennylane_ionq.api_client import JobExecutionError, ResourceManager, Job +from pennylane_ionq.device import ( + QPUDevice, + IonQDevice, + SimulatorDevice, + CircuitIndexNotSetException, +) from pennylane_ionq.ops import GPI, GPI2, MS +from pennylane.measurements import SampleMeasurement, ShotCopies +from unittest import mock FAKE_API_KEY = "ABC123" @@ -43,10 +50,10 @@ def test_generate_samples_qpu_device(self, wires, histogram): """Test that the generate_samples method for QPUDevices shuffles the samples between calls.""" dev = QPUDevice(wires, shots=1024, api_key=FAKE_API_KEY) - dev.histogram = histogram + dev.histograms = [histogram] sample1 = dev.generate_samples() - assert dev.histogram == histogram # make sure histogram is still the same + assert dev.histograms[0] == histogram # make sure histogram is still the same sample2 = dev.generate_samples() assert not np.all(sample1 == sample2) # some rows are different @@ -211,19 +218,6 @@ def test_prob_no_results(self, d): dev = qml.device(d, wires=1, shots=1) assert dev.prob is None - def test_probability(self): - """Test that device.probability works.""" - dev = qml.device("ionq.simulator", wires=2) - dev.target_device._samples = np.array([[1, 1], [1, 1], [0, 0], [0, 0]]) - assert np.array_equal(dev.probability(shot_range=(0, 2)), [0, 0, 0, 1]) - - uniform_prob = [0.25] * 4 - with patch( - "pennylane_ionq.device.SimulatorDevice.prob", new_callable=PropertyMock - ) as mock_prob: - mock_prob.return_value = uniform_prob - assert np.array_equal(dev.probability(), uniform_prob) - @pytest.mark.parametrize("backend", ["harmony", "aria-1", "aria-2", "forte-1", None]) def test_backend_initialization(self, backend): """Test that the device initializes with the correct backend.""" @@ -235,6 +229,242 @@ def test_backend_initialization(self, backend): ) assert dev.backend == backend + def test_recording_when_pennylane_tracker_active(self, requires_api): + """Test recording device execution history via pennnylane tracker class.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + dev.tracker = qml.Tracker() + dev.tracker.active = True + dev.tracker.reset() + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.probs(wires=[0]) + dev.batch_execute([tape1, tape1]) + assert dev.tracker.history["executions"] == [1, 1] + assert dev.tracker.history["shots"] == [1024, 1024] + assert dev.tracker.history["batches"] == [1] + assert dev.tracker.history["batch_len"] == [2] + assert len(dev.tracker.history["resources"]) == 2 + assert dev.tracker.history["resources"][0].num_wires == 1 + assert dev.tracker.history["resources"][0].num_gates == 1 + assert dev.tracker.history["resources"][0].depth == 1 + assert dev.tracker.history["resources"][0].gate_types == {"GPI": 1} + assert dev.tracker.history["resources"][0].gate_sizes == {1: 1} + assert dev.tracker.history["resources"][0].shots.total_shots == 1024 + assert len(dev.tracker.history["results"]) == 2 + + def test_not_recording_when_pennylane_tracker_not_active(self, requires_api): + """Test recording device not executed when tracker is inactive.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + dev.tracker = qml.Tracker() + dev.tracker.active = False + dev.tracker.reset() + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.probs(wires=[0]) + dev.batch_execute([tape1]) + assert dev.tracker.history == {} + + def test_warning_on_empty_circuit(self, requires_api): + """Test warning are shown when circuit is empty.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + qml.probs(wires=[0]) + with pytest.warns( + UserWarning, + match="Circuit is empty. Empty circuits return failures. Submitting anyway.", + ): + dev.batch_execute([tape1]) + + @mock.patch("logging.Logger.isEnabledFor", return_value=True) + @mock.patch("logging.Logger.debug") + def test_batch_execute_logging_when_enabled( + self, mock_logging_debug_method, mock_logging_is_enabled_for_method, requires_api + ): + """Test logging invoked in batch_execute method.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.probs(wires=[0]) + dev.batch_execute([tape1]) + assert mock_logging_is_enabled_for_method.called + assert mock_logging_is_enabled_for_method.call_args[0][0] == logging.DEBUG + mock_logging_debug_method.assert_called() + + def test_batch_execute_probabilities_raises(self, requires_api): + """Test invoking probability() method raises exception if circuit index not + previously set when multiple circuits are submitted in one job. + """ + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0.5, wires=[0]) + qml.probs(wires=[0]) + dev.batch_execute([tape1, tape1]) + with pytest.raises( + CircuitIndexNotSetException, + match="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.", + ): + dev.probability() + + def test_batch_execute_probabilities(self, requires_api): + """Test batch_execute method when computing circuit probabilities.""" + dev = SimulatorDevice(wires=(0, 1, 2), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0.5, wires=[0]) + GPI2(0, wires=[1]) + MS(0, 0.5, wires=[1, 2]) + qml.probs(wires=[0, 1, 2]) + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + GPI(0.5, wires=[1]) + MS(0, 0.5, wires=[1, 2]) + qml.probs(wires=[0, 1, 2]) + results = dev.batch_execute([tape1, tape2]) + assert np.array_equal(results[0], [0.0, 0.0, 0.0, 0.0, 0.25, 0.25, 0.25, 0.25]) + assert np.array_equal(results[1], [0.0, 0.25, 0.25, 0.0, 0.0, 0.25, 0.25, 0.0]) + dev.set_current_circuit_index(0) + assert np.array_equal( + dev.probability(), + [0.0, 0.0, 0.0, 0.0, 0.25, 0.25, 0.25, 0.25], + ) + dev.set_current_circuit_index(1) + assert np.array_equal( + dev.probability(), + [0.0, 0.25, 0.25, 0.0, 0.0, 0.25, 0.25, 0.0], + ) + + def test_batch_execute_probabilities_with_shot_vector(self, requires_api): + """Test batch_execute method with shot vector.""" + dev = SimulatorDevice(wires=(0, 1, 2), gateset="native", shots=1024) + dev._shot_vector = (ShotCopies(1, 3),) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.probs(wires=[0]) + results = dev.batch_execute([tape1]) + assert len(results[0]) == 3 + + def test_batch_execute_variance(self, requires_api): + """Test batch_execute method when computing variance of an observable.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.var(qml.PauliZ(0)) + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + qml.var(qml.PauliZ(0)) + results = dev.batch_execute([tape1, tape2]) + assert results[0] == pytest.approx(0, abs=0.01) + assert results[1] == pytest.approx(1, abs=0.01) + + def test_batch_execute_expectation_value(self, requires_api): + """Test batch_execute method when computing expectation value of an observable.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.expval(qml.PauliZ(0)) + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + qml.expval(qml.PauliZ(0)) + results = dev.batch_execute([tape1, tape2]) + assert results[0] == pytest.approx(-1, abs=0.1) + assert results[1] == pytest.approx(0, abs=0.1) + + def test_batch_execute_expectation_value_with_diagonalization_rotations(self, requires_api): + """Test batch_execute method when computing expectation value of an + observable that requires rotations for diagonalization.""" + dev = SimulatorDevice(wires=(0,), gateset="qis", shots=1024) + with qml.tape.QuantumTape() as tape1: + qml.Hadamard(0) + qml.expval(qml.PauliX(0)) + with qml.tape.QuantumTape() as tape2: + qml.expval(qml.PauliX(0)) + results = dev.batch_execute([tape1, tape2]) + assert results[0] == pytest.approx(1, abs=0.1) + assert results[1] == pytest.approx(0, abs=0.1) + + def test_batch_execute_invoking_prob_property_raises(self, requires_api): + """Test invoking prob device property raises exception if circuit index not + previously set when multiple circuits are submitted in one job. + """ + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.probs(wires=[0]) + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + qml.probs(wires=[0]) + dev.batch_execute([tape1, tape2]) + with pytest.raises( + CircuitIndexNotSetException, + match="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.", + ): + dev.prob + + def test_batch_execute_prob_property(self, requires_api): + """Test batch_execute method with invoking invoking prob device property.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.sample(qml.PauliZ(0)) + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + qml.sample(qml.PauliZ(0)) + dev.batch_execute([tape1, tape2]) + dev.set_current_circuit_index(0) + prob0 = dev.prob + dev.set_current_circuit_index(1) + prob1 = dev.prob + np.testing.assert_array_almost_equal(prob0, [0.0, 1.0], decimal=1) + np.testing.assert_array_almost_equal(prob1, [0.5, 0.5], decimal=1) + + def test_batch_execute_counts(self, requires_api): + """Test batch_execute method when computing counts.""" + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + qml.counts(qml.PauliZ(0)) + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + qml.counts(qml.PauliZ(0)) + results = dev.batch_execute([tape1, tape2]) + assert results[0][-1] == 1024 + assert results[1][-1] == pytest.approx(512, abs=100) + + def test_sample_measurements(self, requires_api): + """Test branch of code activated by using SampleMeasurement.""" + + class CountState(SampleMeasurement): + def __init__(self, state: str): + self.state = state # string identifying the state e.g. "0101" + wires = list(range(len(state))) + super().__init__(wires=wires) + + def process_samples(self, samples, wire_order, shot_range=None, bin_size=None): + counts_mp = qml.counts(wires=self._wires) + counts = counts_mp.process_samples(samples, wire_order, shot_range, bin_size) + return float(counts.get(self.state, 0)) + + def process_counts(self, counts, wire_order): + return float(counts.get(self.state, 0)) + + def __copy__(self): + return CountState(state=self.state) + + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + + with qml.tape.QuantumTape() as tape1: + GPI(0, wires=[0]) + CountState(state="1") + + with qml.tape.QuantumTape() as tape2: + GPI2(0, wires=[0]) + CountState(state="1") + + results = dev.batch_execute([tape1, tape2]) + assert results[0] == 1024 + assert results[1] == pytest.approx(512, abs=100) + class TestJobAttribute: """Tests job creation with mocked submission.""" @@ -258,8 +488,37 @@ def mock_submit_job(*args): assert dev.job["target"] == "foo" assert dev.job["input"]["qubits"] == 1 - assert len(dev.job["input"]["circuit"]) == 1 - assert dev.job["input"]["circuit"][0] == {"gate": "x", "target": 0} + assert len(dev.job["input"]["circuits"][0]) == 1 + assert dev.job["input"]["circuits"][0]["circuit"][0] == { + "gate": "x", + "target": 0, + } + + def test_nonparametrized_tape_batch_submit(self, mocker): + """Tests job attribute after single paulix tape, on batch submit.""" + + def mock_submit_job(*args): + pass + + mocker.patch("pennylane_ionq.device.IonQDevice._submit_job", mock_submit_job) + dev = IonQDevice(wires=(0,), target="foo") + + with qml.tape.QuantumTape() as tape: + qml.PauliX(0) + + dev.reset(circuits_array_length=1) + dev.batch_apply(tape.operations, circuit_index=0) + + assert dev.job["input"]["format"] == "ionq.circuit.v0" + assert dev.job["input"]["gateset"] == "qis" + assert dev.job["target"] == "foo" + assert dev.job["input"]["qubits"] == 1 + + assert len(dev.job["input"]["circuits"]) == 1 + assert dev.job["input"]["circuits"][0]["circuit"][0] == { + "gate": "x", + "target": 0, + } def test_parameterized_op(self, mocker): """Tests job attribute several parameterized operations.""" @@ -280,13 +539,45 @@ def mock_submit_job(*args): assert dev.job["input"]["gateset"] == "qis" assert dev.job["input"]["qubits"] == 1 - assert len(dev.job["input"]["circuit"]) == 2 - assert dev.job["input"]["circuit"][0] == { + assert len(dev.job["input"]["circuits"][0]["circuit"]) == 2 + assert dev.job["input"]["circuits"][0]["circuit"][0] == { + "gate": "rx", + "target": 0, + "rotation": 1.2345, + } + assert dev.job["input"]["circuits"][0]["circuit"][1] == { + "gate": "ry", + "target": 0, + "rotation": 2.3456, + } + + def test_parameterized_op_batch_submit(self, mocker): + """Tests job attribute several parameterized operations.""" + + def mock_submit_job(*args): + pass + + mocker.patch("pennylane_ionq.device.IonQDevice._submit_job", mock_submit_job) + dev = IonQDevice(wires=(0,)) + + with qml.tape.QuantumTape() as tape: + qml.RX(1.2345, wires=0) + qml.RY(qml.numpy.array(2.3456), wires=0) + + dev.reset(circuits_array_length=1) + dev.batch_apply(tape.operations, circuit_index=0) + + assert dev.job["input"]["format"] == "ionq.circuit.v0" + assert dev.job["input"]["gateset"] == "qis" + assert dev.job["input"]["qubits"] == 1 + + assert len(dev.job["input"]["circuits"][0]["circuit"]) == 2 + assert dev.job["input"]["circuits"][0]["circuit"][0] == { "gate": "rx", "target": 0, "rotation": 1.2345, } - assert dev.job["input"]["circuit"][1] == { + assert dev.job["input"]["circuits"][0]["circuit"][1] == { "gate": "ry", "target": 0, "rotation": 2.3456, @@ -313,30 +604,76 @@ def mock_submit_job(*args): assert dev.job["input"]["gateset"] == "native" assert dev.job["input"]["qubits"] == 3 - assert len(dev.job["input"]["circuit"]) == 4 - assert dev.job["input"]["circuit"][0] == { + assert len(dev.job["input"]["circuits"][0]["circuit"]) == 4 + assert dev.job["input"]["circuits"][0]["circuit"][0] == { "gate": "gpi", "target": 0, "phase": 0.1, } - assert dev.job["input"]["circuit"][1] == { + assert dev.job["input"]["circuits"][0]["circuit"][1] == { "gate": "gpi2", "target": 1, "phase": 0.2, } - assert dev.job["input"]["circuit"][2] == { + assert dev.job["input"]["circuits"][0]["circuit"][2] == { "gate": "ms", "targets": [1, 2], "phases": [0.2, 0.3], "angle": 0.25, } - assert dev.job["input"]["circuit"][3] == { + assert dev.job["input"]["circuits"][0]["circuit"][3] == { "gate": "ms", "targets": [1, 2], "phases": [0.4, 0.5], "angle": 0.1, } + def test_parameterized_native_op_batch_submit(self, mocker): + """Tests job attribute for several parameterized native operations with batch_execute.""" + + class StopExecute(Exception): + pass + + def mock_submit_job(*args): + raise StopExecute() + + mocker.patch("pennylane_ionq.device.SimulatorDevice._submit_job", mock_submit_job) + dev = SimulatorDevice(wires=(0,), gateset="native", shots=1024) + + with qml.tape.QuantumTape() as tape1: + GPI(0.7, wires=[0]) + GPI2(0.8, wires=[0]) + qml.expval(qml.PauliZ(0)) + with qml.tape.QuantumTape() as tape2: + GPI2(0.9, wires=[0]) + qml.expval(qml.PauliZ(0)) + + try: + dev.batch_execute([tape1, tape2]) + except StopExecute: + pass + + assert dev.job["input"]["format"] == "ionq.circuit.v0" + assert dev.job["input"]["gateset"] == "native" + assert dev.job["target"] == "simulator" + assert dev.job["input"]["qubits"] == 1 + assert len(dev.job["input"]["circuits"]) == 2 + assert dev.job["input"]["circuits"][0]["circuit"][0] == { + "gate": "gpi", + "target": 0, + "phase": 0.7, + } + assert dev.job["input"]["circuits"][0]["circuit"][1] == { + "gate": "gpi2", + "target": 0, + "phase": 0.8, + } + assert dev.job["input"]["circuits"][1]["circuit"][0] == { + "gate": "gpi2", + "target": 0, + "phase": 0.9, + } + @pytest.mark.parametrize( "phi0, phi1, theta", [