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 function for convert circuit parameters to numpy #3899

Merged
merged 20 commits into from Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Expand Up @@ -151,6 +151,10 @@
* `Sum`, `Prod`, and `SProd` operator data is now a flat list, instead of nested.
[(#3958)](https://github.com/PennyLaneAI/pennylane/pull/3958)

* `qml.transforms.convert_to_numpy_parameters` is added to convert a circuit with interface-specific parameters to one
with only numpy parameters. This transform is designed to replace `qml.tape.Unwrap`.
[(#3899)](https://github.com/PennyLaneAI/pennylane/pull/3899)

<h3>Breaking changes</h3>

* Both JIT interfaces are not compatible with Jax `>0.4.3`, we raise an error for those versions.
Expand Down
9 changes: 9 additions & 0 deletions pennylane/transforms/__init__.py
Expand Up @@ -152,6 +152,13 @@
~transforms.sign_expand
~transforms.sum_expand

This transform accepts a single tape and returns a single tape:

.. autosummary::
:toctree: api

~transforms.convert_to_numpy_parameters

Decorators and utility functions
--------------------------------

Expand Down Expand Up @@ -185,6 +192,7 @@
~transforms.fold_global
~transforms.poly_extrapolate
~transforms.richardson_extrapolate

"""
# Import the decorators first to prevent circular imports when used in other transforms
from .batch_transform import batch_transform, map_batch_transform
Expand All @@ -195,6 +203,7 @@
from .batch_partial import batch_partial
from .classical_jacobian import classical_jacobian
from .condition import cond, Conditional
from .convert_to_numpy_parameters import convert_to_numpy_parameters
from .compile import compile
from .decompositions import zyz_decomposition, xyx_decomposition, two_qubit_decomposition
from .defer_measurements import defer_measurements
Expand Down
88 changes: 88 additions & 0 deletions pennylane/transforms/convert_to_numpy_parameters.py
@@ -0,0 +1,88 @@
# Copyright 2018-2023 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This file contains preprocessings steps that may be called internally
during execution.
"""
import copy

import pennylane as qml
from pennylane import math
from pennylane.tape import QuantumScript


def _convert_op_to_numpy_data(op: qml.operation.Operator) -> qml.operation.Operator:
if math.get_interface(*op.data) == "numpy":
return op
# Use operator method to change parameters when it become available
copied_op = copy.copy(op)
copied_op.data = math.unwrap(op.data)
return copied_op


def _convert_measurement_to_numpy_data(
m: qml.measurements.MeasurementProcess,
) -> qml.measurements.MeasurementProcess:
if m.obs is None or math.get_interface(*m.obs.data) == "numpy":
return m
# Use measurement method to change parameters when it becomes available
copied_m = copy.copy(m)
copied_m.obs.data = math.unwrap(m.obs.data)
timmysilv marked this conversation as resolved.
Show resolved Hide resolved
return copied_m


# pylint: disable=protected-access
def convert_to_numpy_parameters(circuit: QuantumScript) -> QuantumScript:
"""Transforms a circuit to one with purely numpy parameters.

Args:
circuit (QuantumScript): a circuit with parameters of any interface

Returns:
QuantumScript: A circuit with purely numpy parameters

See also :class:`pennylane.tape.Unwrap`. ``convert_to_numpy_parameters`` function creates a new :class:`pennylane.tape.QuantumScript`
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
instead of modifying one in place.

>>> ops = [qml.S(0), qml.RX(torch.tensor(0.1234), 0)]
>>> measurements = [qml.state(), qml.expval(qml.Hermitian(torch.eye(2), 0))]
>>> circuit = qml.tape.QuantumScript(ops, measurements )
>>> new_circuit = convert_to_numpy_parameters(circuit)
>>> new_circuit.circuit
[S(wires=[0]),
RX(0.1234000027179718, wires=[0]),
state(wires=[]),
expval(Hermitian(array([[1., 0.],
[0., 1.]], dtype=float32), wires=[0]))]

If the component's data does not need to be transformed, it is left uncopied.

>>> circuit[0] is new_circuit[0]
True
>>> circuit[1] is new_circuit[1]
False
>>> circuit[2] is new_circuit[2]
True
>>> circuit[3] is new_circuit[3]
False

"""
new_prep = (_convert_op_to_numpy_data(op) for op in circuit._prep)
new_ops = (_convert_op_to_numpy_data(op) for op in circuit._ops)
new_measurements = (_convert_measurement_to_numpy_data(m) for m in circuit.measurements)
new_circuit = circuit.__class__(new_ops, new_measurements, new_prep)
# must preserve trainable params as we lose information about the machine learning interface
new_circuit.trainable_params = circuit.trainable_params
timmysilv marked this conversation as resolved.
Show resolved Hide resolved
new_circuit._qfunc_output = circuit._qfunc_output
return new_circuit
1 change: 1 addition & 0 deletions tests/tests_passing_pylint.txt
Expand Up @@ -24,6 +24,7 @@ tests/transforms/test_sign_expand.py
tests/ops/qubit/test_special_unitary.py
tests/ops/qubit/test_matrix_ops.py
tests/ops/op_math/test_exp.py
tests/transforms/test_convert_to_numpy_parameters.py
tests/transforms/test_decompositions.py
tests/transforms/test_qfunc_transform.py
tests/ops/op_math/test_exp.py
Expand Down
98 changes: 98 additions & 0 deletions tests/transforms/test_convert_to_numpy_parameters.py
@@ -0,0 +1,98 @@
# Copyright 2018-2023 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This file tests the convert_to_numpy_parameters function."""
import pytest

import numpy as np

import pennylane as qml
from pennylane.transforms import convert_to_numpy_parameters
from pennylane.transforms.convert_to_numpy_parameters import (
_convert_op_to_numpy_data,
_convert_measurement_to_numpy_data,
)


ml_frameworks_list = [
pytest.param("autograd", marks=pytest.mark.autograd),
pytest.param("jax", marks=pytest.mark.jax),
pytest.param("torch", marks=pytest.mark.torch),
pytest.param("tensorflow", marks=pytest.mark.tf),
]


@pytest.mark.parametrize("framework", ml_frameworks_list)
def test_convert_arrays_to_numpy(framework):
"""Tests that convert_to_numpy_parameters works with autograd arrays."""

x = qml.math.asarray(np.array(1.234), like=framework)
y = qml.math.asarray(np.array(0.652), like=framework)
M = qml.math.asarray(np.eye(2), like=framework)
state = qml.math.asarray(np.array([1, 0]), like=framework)

numpy_data = np.array(0.62)

ops = [qml.RX(x, 0), qml.RY(y, 1), qml.CNOT((0, 1)), qml.RZ(numpy_data, 0)]
m = [qml.state(), qml.expval(qml.Hermitian(M, 0))]
prep = [qml.QubitStateVector(state, 0)]

qs = qml.tape.QuantumScript(ops, m, prep)
new_qs = convert_to_numpy_parameters(qs)

# check ops that should be unaltered
assert new_qs[3] is qs[3]
assert new_qs[4] is qs[4]
assert new_qs.measurements[0] is qs.measurements[0]

for ind in (0, 1, 2, 6):
assert qml.equal(new_qs[ind], qs[ind], check_interface=False, check_trainability=False)
assert qml.math.get_interface(*new_qs[ind].data) == "numpy"


@pytest.mark.autograd
def test_preserves_trainable_params():
"""Test that convert_to_numpy_parameters preserves the trainable parameters property."""
ops = [qml.RX(qml.numpy.array(2.0), 0), qml.RY(qml.numpy.array(3.0), 0)]
qs = qml.tape.QuantumScript(ops)
qs.trainable_params = {0}
output = convert_to_numpy_parameters(qs)
assert output.trainable_params == [0]


@pytest.mark.autograd
def test_unwraps_arithmetic_op():
"""Test that the operator helper function can handle operator arithmetic objects."""
op1 = qml.s_prod(qml.numpy.array(2.0), qml.PauliX(0))
op2 = qml.s_prod(qml.numpy.array(3.0), qml.PauliY(0))

sum_op = qml.sum(op1, op2)

unwrapped_op = _convert_op_to_numpy_data(sum_op)
assert qml.math.get_interface(*unwrapped_op.data) == "numpy"
assert qml.math.get_interface(*unwrapped_op.data) == "numpy"


@pytest.mark.autograd
def test_unwraps_arithmetic_op_measurement():
"""Test that the measurement helper function can handle operator arithmetic objects."""
op1 = qml.s_prod(qml.numpy.array(2.0), qml.PauliX(0))
op2 = qml.s_prod(qml.numpy.array(3.0), qml.PauliY(0))

sum_op = qml.sum(op1, op2)
m = qml.expval(sum_op)

unwrapped_m = _convert_measurement_to_numpy_data(m)
unwrapped_op = unwrapped_m.obs
assert qml.math.get_interface(*unwrapped_op.data) == "numpy"
assert qml.math.get_interface(*unwrapped_op.data) == "numpy"