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

Add job_time #160

Merged
merged 19 commits into from
Nov 24, 2021
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* Added support for the `qml.SX` operation to the Qiskit devices.
[(#158)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/158)

* Added support for returning job execution times. [(#160)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/160)

### Documentation

### Bug fixes
Expand All @@ -17,7 +19,7 @@

This release contains contributions from (in alphabetical order):

Antal Száva
Guillermo Alonso-Linaje, Antal Száva

---

Expand Down
21 changes: 21 additions & 0 deletions pennylane_qiskit/ibmq.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,24 @@ def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=10
p = provider or IBMQ.get_provider(hub=hub, group=group, project=project)

super().__init__(wires=wires, provider=p, backend=backend, shots=shots, **kwargs)

def batch_execute(self, circuits):
res = super().batch_execute(circuits)
if self.tracker.active:
self._track_run()
return res

def _track_run(self):
"""Provide runtime information."""

time_per_step = self._current_job.time_per_step()
job_time = {
"creating": (time_per_step["CREATED"] - time_per_step["CREATING"]).total_seconds(),
"validating": (
time_per_step["VALIDATED"] - time_per_step["VALIDATING"]
).total_seconds(),
"queued": (time_per_step["RUNNING"] - time_per_step["QUEUED"]).total_seconds(),
"running": (time_per_step["COMPLETED"] - time_per_step["RUNNING"]).total_seconds(),
}
self.tracker.update(job_time=job_time)
self.tracker.record()
5 changes: 4 additions & 1 deletion pennylane_qiskit/qiskit_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,6 @@ def generate_samples(self, circuit=None):

# hardware or hardware simulator
samples = self._current_job.result().get_memory(circuit)

# reverse qubit order to match PennyLane convention
return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples])

Expand Down Expand Up @@ -411,4 +410,8 @@ def batch_execute(self, circuits):
res = self.statistics(circuit.observables)
results.append(res)

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

return results
17 changes: 12 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
A = np.array([[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]])


state_backends = ["statevector_simulator", "unitary_simulator", "aer_simulator_statevector", "aer_simulator_unitary"]
state_backends = [
"statevector_simulator",
"unitary_simulator",
"aer_simulator_statevector",
"aer_simulator_unitary",
]
hw_backends = ["qasm_simulator", "aer_simulator"]


Expand Down Expand Up @@ -75,8 +80,9 @@ def device(request, backend, shots):
if backend not in state_backends and shots is None:
pytest.skip("Hardware simulators do not support analytic mode")

if (issubclass(request.param, AerDevice) and "aer" not in backend) \
or (issubclass(request.param, BasicAerDevice) and "aer" in backend):
if (issubclass(request.param, AerDevice) and "aer" not in backend) or (
issubclass(request.param, BasicAerDevice) and "aer" in backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

def _device(n, device_options=None):
Expand All @@ -90,8 +96,9 @@ def _device(n, device_options=None):
@pytest.fixture(params=[AerDevice, BasicAerDevice])
def state_vector_device(request, statevector_backend, shots):

if (issubclass(request.param, AerDevice) and "aer" not in statevector_backend) \
or (issubclass(request.param, BasicAerDevice) and "aer" in statevector_backend):
if (issubclass(request.param, AerDevice) and "aer" not in statevector_backend) or (
issubclass(request.param, BasicAerDevice) and "aer" in statevector_backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

def _device(n):
Expand Down
26 changes: 25 additions & 1 deletion tests/test_ibmq.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def circuit(x, y):
x = qml.numpy.array(0.543, requires_grad=True)
y = qml.numpy.array(0.123, requires_grad=True)

res = qml.grad(circuit)(x,y)
res = qml.grad(circuit)(x, y)
expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]])
assert np.allclose(res, expected, **tol)

Expand All @@ -235,6 +235,7 @@ def circuit(x, y):
# running the circuit
assert spy2.call_count == 2


@pytest.mark.parametrize("shots", [1000])
def test_probability(token, tol, shots):
"""Test that the probs function works."""
Expand All @@ -259,3 +260,26 @@ def circuit(x):
assert np.isclose(hw_prob.sum(), 1, **tol)
assert np.allclose(prob_analytic(x), hw_prob, **tol)
assert not np.array_equal(prob_analytic(x), hw_prob)


def test_track(token):
"""Test that the tracker works."""

IBMQ.enable_account(token)
dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1)
dev.tracker.active = True

@qml.qnode(dev)
def circuit():
qml.PauliX(wires=0)
return qml.probs(wires=0)

circuit()

assert "job_time" in dev.tracker.history
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we check that the element job_time in dev.tracker.history have the expected form? (Dictionary containing creating, validating, queued and running)

if "job_time" in dev.tracker.history:
assert "creating" in dev.tracker.history["job_time"][0]
assert "validating" in dev.tracker.history["job_time"][0]
assert "queued" in dev.tracker.history["job_time"][0]
assert "running" in dev.tracker.history["job_time"][0]
assert len(dev.tracker.history["job_time"][0]) == 4
36 changes: 21 additions & 15 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ class TestDeviceIntegration:
@pytest.mark.parametrize("d", pldevices)
def test_load_device(self, d, backend):
"""Test that the qiskit device loads correctly"""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
if (d[0] == "qiskit.aer" and "aer" not in backend) or (
d[0] == "qiskit.basicaer" and "aer" in backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

dev = qml.device(d[0], wires=2, backend=backend, shots=1024)
Expand All @@ -38,7 +39,9 @@ def test_incorrect_backend(self):

def test_incorrect_backend_wires(self):
"""Test that exception is raised if number of wires is too large"""
with pytest.raises(ValueError, match=r"Backend 'aer_simulator\_statevector' supports maximum"):
with pytest.raises(
ValueError, match=r"Backend 'aer_simulator\_statevector' supports maximum"
):
qml.device("qiskit.aer", wires=100, method="statevector")

def test_args(self):
Expand All @@ -55,8 +58,9 @@ def test_args(self):
@pytest.mark.parametrize("shots", [None, 8192])
def test_one_qubit_circuit(self, shots, d, backend, tol):
"""Test that devices provide correct result for a simple circuit"""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
if (d[0] == "qiskit.aer" and "aer" not in backend) or (
d[0] == "qiskit.basicaer" and "aer" in backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

if backend not in state_backends and shots is None:
Expand All @@ -83,8 +87,9 @@ def circuit(x, y, z):
def test_basis_state_and_rot(self, shots, d, backend, tol):
"""Integration test for the BasisState and Rot operations for non-analytic mode."""

if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
if (d[0] == "qiskit.aer" and "aer" not in backend) or (
d[0] == "qiskit.basicaer" and "aer" in backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

dev = qml.device(d[0], wires=1, backend=backend, shots=shots)
Expand Down Expand Up @@ -149,9 +154,7 @@ def test_noise_model_qasm_simulator(self, monkeypatch):

cache = []
with monkeypatch.context() as m:
m.setattr(
aer.AerSimulator, "set_options", lambda *args, **kwargs: cache.append(kwargs)
)
m.setattr(aer.AerSimulator, "set_options", lambda *args, **kwargs: cache.append(kwargs))
dev = qml.device("qiskit.aer", wires=2, noise_model="test value")
assert cache[-1] == {"noise_model": "test value"}

Expand Down Expand Up @@ -485,6 +488,7 @@ def circuit():

assert circuit() == -1


class TestBatchExecution:
"""Test the devices work correctly with the batch execution pipeline."""

Expand All @@ -493,8 +497,9 @@ class TestBatchExecution:
def test_one_qubit_circuit_batch_params(self, shots, d, backend, tol, mocker):
"""Test that devices provide correct result for a simple circuit using
the batch_params transform."""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
if (d[0] == "qiskit.aer" and "aer" not in backend) or (
d[0] == "qiskit.basicaer" and "aer" in backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

if backend not in state_backends and shots is None:
Expand Down Expand Up @@ -531,8 +536,9 @@ def circuit(x, y, z):
def test_batch_execute_parameter_shift(self, shots, d, backend, tol, mocker):
"""Test that devices provide correct result computing the gradient of a
circuit using the parameter-shift rule and the batch execution pipeline."""
if (d[0] == "qiskit.aer" and "aer" not in backend) \
or (d[0] == "qiskit.basicaer" and "aer" in backend):
if (d[0] == "qiskit.aer" and "aer" not in backend) or (
d[0] == "qiskit.basicaer" and "aer" in backend
):
pytest.skip("Only the AerSimulator is supported on AerDevice")

if backend not in state_backends and shots is None:
Expand All @@ -553,7 +559,7 @@ def circuit(x, y):
x = qml.numpy.array(0.543, requires_grad=True)
y = qml.numpy.array(0.123, requires_grad=True)

res = qml.grad(circuit)(x,y)
res = qml.grad(circuit)(x, y)
expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]])
assert np.allclose(res, expected, **tol)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_inverses.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class TestInverses:
("Hadamard", 0),
("S", 1),
("T", 1),
("SX", 0)
("SX", 0),
],
)
def test_supported_gate_inverse_single_wire_no_parameters(self, name, expected_output):
Expand Down
13 changes: 4 additions & 9 deletions tests/test_qiskit_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def test_backend_options_cleaned(self):
dev2 = qml.device("qiskit.aer", wires=2)
assert dev2.backend.options.get("noise_model") is None


@pytest.mark.parametrize("shots", [None])
class TestBatchExecution:
"""Tests for the batch_execute method."""
Expand Down Expand Up @@ -154,13 +155,9 @@ def test_result(self, device, tol):
tape2_expected = dev.execute(self.tape2)

assert len(res) == 2
assert np.allclose(
res[0], tape1_expected, atol=0
)
assert np.allclose(res[0], tape1_expected, atol=0)

assert np.allclose(
res[1], tape2_expected, atol=0
)
assert np.allclose(res[1], tape2_expected, atol=0)

def test_result_empty_tape(self, device, tol):
"""Tests that the result has the correct shape and entry types for empty tapes."""
Expand All @@ -174,6 +171,4 @@ def test_result_empty_tape(self, device, tol):
# execution
dev.reset()
assert len(res) == 3
assert np.allclose(
res[0], dev.execute(empty_tape), atol=0
)
assert np.allclose(res[0], dev.execute(empty_tape), atol=0)