Skip to content

Commit

Permalink
Bump orjson from 3.9.14 to 3.9.15 (#464)
Browse files Browse the repository at this point in the history
* Add support for differentiable parameter vectors (#458)

* Allow parameter vectors to be differentiable

* Fix unintended `if` statement fall-through

* Add note to changelog

* Add missing tests for `_check_parameter_bound()`

* Add tests for parameter vector gradients

* Add missing edge case to parameter expression test

* Update note in changelog

* Replace "take" with "compute" in changelog

Co-authored-by: Utkarsh <utkarshazad98@gmail.com>

---------

Co-authored-by: Utkarsh <utkarshazad98@gmail.com>

* Bump orjson from 3.9.14 to 3.9.15

Bumps [orjson](https://github.com/ijl/orjson) from 3.9.14 to 3.9.15.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](ijl/orjson@3.9.14...3.9.15)

---
updated-dependencies:
- dependency-name: orjson
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Mikhail Andrenkov <mikhail@xanadu.ai>
Co-authored-by: Utkarsh <utkarshazad98@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 29, 2024
1 parent fb30897 commit 90418f0
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 32 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@

### Bug fixes 🐛

* It is now possible to compute the gradient of a circuit with `ParameterVector` elements.
[(#458)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/458)

### Contributors ✍️

This release contains contributions from (in alphabetical order):
Expand Down
50 changes: 40 additions & 10 deletions pennylane_qiskit/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterExpression, ParameterVector
from qiskit.circuit import Measure, Barrier, ControlFlowOp, Clbit
from qiskit.circuit.classical import expr
from qiskit.circuit.controlflow.switch_case import _DefaultCaseType
from qiskit.circuit.library import GlobalPhaseGate
from qiskit.circuit.classical import expr
from qiskit.circuit.parametervector import ParameterVectorElement
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import SparsePauliOp
from sympy import lambdify
Expand All @@ -51,17 +52,25 @@
)


def _check_parameter_bound(param: Parameter, unbound_params: Dict[Parameter, Any]):
def _check_parameter_bound(
param: Parameter,
unbound_params: Dict[Union[Parameter, ParameterVector], Any],
):
"""Utility function determining if a certain parameter in a QuantumCircuit has
been bound.
Args:
param (qiskit.circuit.Parameter): the parameter to be checked
unbound_params (dict[qiskit.circuit.Parameter, Any]):
a dictionary mapping qiskit parameters to trainable parameter values
unbound_params (dict[qiskit.circuit.Parameter | qiskit.circuit.ParameterVector, Any]):
a dictionary mapping qiskit parameters (or vectors) to trainable parameter values
"""
if isinstance(param, Parameter) and param not in unbound_params:
raise ValueError(f"The parameter {param} was not bound correctly.".format(param))
if isinstance(param, ParameterVectorElement):
if param.vector not in unbound_params:
raise ValueError(f"The vector of parameter {param} was not bound correctly.")

elif isinstance(param, Parameter):
if param not in unbound_params:
raise ValueError(f"The parameter {param} was not bound correctly.")


def _process_basic_param_args(params, *args, **kwargs):
Expand Down Expand Up @@ -274,11 +283,32 @@ def _get_operation_params(instruction, unbound_params) -> list:

if isinstance(p, ParameterExpression):
if p.parameters: # non-empty set = has unbound parameters
ordered_params = tuple(p.parameters)
f = lambdify(ordered_params, getattr(p, "_symbol_expr"), modules=qml.numpy)
f_args = []
for i_ordered_params in ordered_params:
f_args.append(unbound_params.get(i_ordered_params))
f_params = []

# Ensure duplicate subparameters are only appended once.
f_param_names = set()

for subparam in p.parameters:
if isinstance(subparam, ParameterVectorElement):
# Unfortunately, parameter vector elements are named using square brackets.
# As a result, element names are not a valid Python identifier which causes
# issues with SymPy. To get around this, we create a temporary parameter
# representing the entire vector and pass that into the SymPy function.
parameter = Parameter(subparam.vector.name)
argument = unbound_params.get(subparam.vector)
else:
parameter = subparam
argument = unbound_params.get(subparam)

if parameter.name not in f_param_names:
f_param_names.add(parameter.name)
f_params.append(parameter)
f_args.append(argument)

f_expr = getattr(p, "_symbol_expr")
f = lambdify(f_params, f_expr, modules=qml.numpy)

operation_params.append(f(*f_args))
else: # needed for qiskit<0.43.1
operation_params.append(float(p)) # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ networkx==3.2.1
ninja==1.11.1.1
ntlm-auth==1.5.0
numpy==1.26.4
orjson==3.9.14
orjson==3.9.15
pbr==6.0.0
pennylane==0.34
PennyLane-Lightning==0.34
Expand Down
126 changes: 105 additions & 21 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import sys
from typing import cast

import pytest
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qiskit.circuit import library as lib
from qiskit.circuit import Parameter, ParameterVector
from qiskit.circuit.classical import expr
from qiskit.circuit.library import DraperQFTAdder
from qiskit.circuit.parametervector import ParameterVectorElement
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import SparsePauliOp

Expand Down Expand Up @@ -243,17 +245,6 @@ def test_pass_parameters_to_bind(self, recorder):
assert recorder.queue[0].parameters == [0.5]
assert recorder.queue[0].wires == Wires([0])

def test_parameter_was_not_bound(self, recorder):
"""Tests that an error is raised when parameters were not bound."""

theta = Parameter("θ")
unbound_params = {}

with pytest.raises(
ValueError, match="The parameter {} was not bound correctly.".format(theta)
):
_check_parameter_bound(theta, unbound_params)

def test_unused_parameters_are_ignored(self, recorder):
"""Tests that unused parameters are ignored during assignment."""
a, b, c = [Parameter(var) for var in "abc"]
Expand Down Expand Up @@ -741,6 +732,44 @@ def test_operation_transformed_into_qubit_unitary(self, recorder):
assert recorder.queue[0].wires == Wires([0, 1])


class TestCheckParameterBound:
"""Tests for the :func:`_check_parameter_bound()` function."""

def test_parameter_vector_element_is_unbound(self):
"""Tests that no exception is raised if the vector associated with a parameter vector
element exists in the dictionary of unbound parameters.
"""
param_vec = ParameterVector("θ", 2)
param = cast(ParameterVectorElement, param_vec[1])
_check_parameter_bound(param=param, unbound_params={param_vec: [0.1, 0.2]})

def test_parameter_vector_element_is_not_unbound(self):
"""Tests that a ValueError is raised if the vector associated with a parameter vector
element is missing from the dictionary of unbound parameters.
"""
param_vec = ParameterVector("θ", 2)
param = cast(ParameterVectorElement, param_vec[1])

match = r"The vector of parameter θ\[1\] was not bound correctly\."
with pytest.raises(ValueError, match=match):
_check_parameter_bound(param=param, unbound_params={})

def test_parameter_is_unbound(self):
"""Tests that no exception is raised if the checked parameter exists in the dictionary of
unbound parameters.
"""
param = Parameter("θ")
_check_parameter_bound(param=param, unbound_params={param: 0.1})

def test_parameter_is_not_unbound(self):
"""Tests that a ValueError is raised if the checked parameter is missing in the dictionary
of unbound parameters.
"""
param = Parameter("θ")
with pytest.raises(ValueError, match=r"The parameter θ was not bound correctly\."):
_check_parameter_bound(param=param, unbound_params={})


class TestConverterUtils:
"""Tests the utility functions used by the converter function."""

Expand Down Expand Up @@ -1134,7 +1163,6 @@ def test_qasm_(self, recorder):


class TestConverterIntegration:

def test_use_loaded_circuit_in_qnode(self, qubit_device_2_wires):
"""Tests loading a converted template in a QNode."""

Expand Down Expand Up @@ -1274,7 +1302,7 @@ def circuit_native_pennylane():
@pytest.mark.parametrize("shots", [None])
@pytest.mark.parametrize("theta,phi,varphi", list(zip(THETA, PHI, VARPHI)))
def test_gradient(self, theta, phi, varphi, shots, tol):
"""Test that the gradient works correctly"""
"""Tests that the gradient of a circuit is calculated correctly."""
qc = QuantumCircuit(3)
qiskit_params = [Parameter("param_{}".format(i)) for i in range(3)]

Expand Down Expand Up @@ -1305,6 +1333,64 @@ def circuit(params):

assert np.allclose(res, expected, **tol)

@pytest.mark.parametrize("shots", [None])
def test_gradient_with_parameter_vector(self, shots, tol):
"""Tests that the gradient of a circuit with a parameter vector is calculated correctly."""
qiskit_circuit = QuantumCircuit(1)

theta_param = ParameterVector("θ", 2)
theta_val = np.array([np.pi / 4, np.pi / 16])

qiskit_circuit.rx(theta_param[0], 0)
qiskit_circuit.rx(theta_param[1] * 4, 0)

pl_circuit_loader = qml.from_qiskit(qiskit_circuit)

dev = qml.device("default.qubit", wires=1, shots=shots)

@qml.qnode(dev)
def circuit(theta):
pl_circuit_loader(params={theta_param: theta})
return qml.expval(qml.PauliZ(0))

have_gradient = qml.grad(circuit)(theta_val)
want_gradient = [-1, -4]
assert np.allclose(have_gradient, want_gradient, **tol)

@pytest.mark.parametrize("shots", [None])
def test_gradient_with_parameter_expressions(self, shots, tol):
"""Tests that the gradient of a circuit with parameter expressions is calculated correctly."""
qiskit_circuit = QuantumCircuit(1)

theta_param = ParameterVector("θ", 3)
theta_val = np.array([3 * np.pi / 16, np.pi / 64, np.pi / 96])

phi_param = Parameter("φ")
phi_val = np.array(np.pi / 8)

# Apply an instruction with a regular parameter.
qiskit_circuit.rx(phi_param, 0)
# Apply an instruction with a parameter vector element.
qiskit_circuit.rx(theta_param[0], 0)
# Apply an instruction with a parameter expression involving one parameter.
qiskit_circuit.rx(theta_param[1] + theta_param[1], 0)
# Apply an instruction with a parameter expression involving two parameters.
qiskit_circuit.rx(3 * theta_param[2] + phi_param, 0)

pl_circuit_loader = qml.from_qiskit(qiskit_circuit)

dev = qml.device("default.qubit", wires=1, shots=shots)

@qml.qnode(dev)
def circuit(phi, theta):
pl_circuit_loader(params={phi_param: phi, theta_param: theta})
return qml.expval(qml.PauliZ(0))

have_phi_gradient, have_theta_gradient = qml.grad(circuit)(phi_val, theta_val)
want_phi_gradient, want_theta_gradient = [-2], [-1, -2, -3]
assert np.allclose(have_phi_gradient, want_phi_gradient, **tol)
assert np.allclose(have_theta_gradient, want_theta_gradient, **tol)

@pytest.mark.parametrize("shots", [None])
def test_differentiable_param_is_array(self, shots, tol):
"""Test that extracting the differentiable parameters works correctly
Expand Down Expand Up @@ -1672,7 +1758,6 @@ def circuit_native_pennylane(angle):


class TestPassingParameters:

def _get_parameter_vector_test_circuit(self, qubit_device_2_wires):
"""A test circuit for testing"""
theta = ParameterVector("v", 3)
Expand Down Expand Up @@ -1816,9 +1901,9 @@ class TestLoadPauliOp:
qml.sum(
qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=0)),
qml.prod(qml.PauliZ(wires=1), qml.PauliX(wires=0)),
)
),
),
]
],
)
def test_convert_with_default_coefficients(self, pauli_op, want_op):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator with the default
Expand All @@ -1839,9 +1924,9 @@ def test_convert_with_default_coefficients(self, pauli_op, want_op):
qml.sum(
qml.s_prod(3, qml.prod(qml.PauliX(wires=1), qml.PauliY(wires=0))),
qml.s_prod(7, qml.prod(qml.PauliZ(wires=1), qml.PauliX(wires=0))),
)
),
),
]
],
)
def test_convert_with_literal_coefficients(self, pauli_op, want_op):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator with literal
Expand All @@ -1850,7 +1935,6 @@ def test_convert_with_literal_coefficients(self, pauli_op, want_op):
have_op = load_pauli_op(pauli_op)
assert qml.equal(have_op, want_op)


def test_convert_with_parameter_coefficients(self):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator by assigning values
to each parameterized coefficient.
Expand Down Expand Up @@ -1907,9 +1991,9 @@ def test_convert_too_many_coefficients(self):
qml.sum(
qml.prod(qml.PauliX(wires=0), qml.PauliY(wires=1)),
qml.prod(qml.PauliZ(wires=0), qml.PauliX(wires=1)),
)
),
),
]
],
)
def test_convert_with_wires(self, pauli_op, wires, want_op):
"""Tests that a SparsePauliOp can be converted into a PennyLane operator with custom wires."""
Expand Down

0 comments on commit 90418f0

Please sign in to comment.