From fdee5e083a303d53557f0d80092f62e7eb2c5097 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Mon, 10 Jul 2023 14:45:41 -0400 Subject: [PATCH 01/12] Update interfaces module to use bind_new_parameters --- pennylane/interfaces/jax.py | 3 +- pennylane/interfaces/jax_jit.py | 12 +--- pennylane/interfaces/jax_jit_tuple.py | 4 +- pennylane/interfaces/tensorflow.py | 3 +- pennylane/interfaces/tensorflow_autograph.py | 13 +--- pennylane/tape/qscript.py | 66 ++++++++++++++++++++ 6 files changed, 73 insertions(+), 28 deletions(-) diff --git a/pennylane/interfaces/jax.py b/pennylane/interfaces/jax.py index e4b9d3787c6..cfa030d3884 100644 --- a/pennylane/interfaces/jax.py +++ b/pennylane/interfaces/jax.py @@ -29,8 +29,7 @@ def _set_copy_and_unwrap_tape(t, a, unwrap=True): """Copy a given tape with operations and set parameters""" - tc = t.copy(copy_operations=True) - tc.set_parameters(a) + tc = qml.tape.qscript.bind_new_parameters_tape(t, a, t.trainable_params) return convert_to_numpy_parameters(tc) if unwrap else tc diff --git a/pennylane/interfaces/jax_jit.py b/pennylane/interfaces/jax_jit.py index f5de4d3e586..875410c6985 100644 --- a/pennylane/interfaces/jax_jit.py +++ b/pennylane/interfaces/jax_jit.py @@ -27,6 +27,8 @@ from pennylane.measurements import ProbabilityMP from pennylane.transforms import convert_to_numpy_parameters +from .jax import set_parameters_on_copy_and_unwrap + dtype = jnp.float64 @@ -143,16 +145,6 @@ def _execute_legacy( ): # pylint: disable=dangerous-default-value,unused-argument total_params = np.sum([len(p) for p in params]) - # Copy a given tape with operations and set parameters - def _set_copy_and_unwrap_tape(t, a, unwrap=True): - tc = t.copy(copy_operations=True) - tc.set_parameters(a) - return convert_to_numpy_parameters(tc) if unwrap else tc - - def set_parameters_on_copy_and_unwrap(tapes, params, unwrap=True): - """Copy a set of tapes with operations and set parameters""" - return tuple(_set_copy_and_unwrap_tape(t, a, unwrap=unwrap) for t, a in zip(tapes, params)) - @jax.custom_vjp def wrapped_exec(params): result_shapes_dtypes = _extract_shape_dtype_structs(tapes, device) diff --git a/pennylane/interfaces/jax_jit_tuple.py b/pennylane/interfaces/jax_jit_tuple.py index 4352968000d..d1dd80b677e 100644 --- a/pennylane/interfaces/jax_jit_tuple.py +++ b/pennylane/interfaces/jax_jit_tuple.py @@ -27,14 +27,14 @@ from pennylane.interfaces.jax_jit import _numeric_type_to_dtype from pennylane.transforms import convert_to_numpy_parameters + dtype = jnp.float64 Zero = jax.custom_derivatives.SymbolicZero def _set_copy_and_unwrap_tape(t, a, unwrap=True): """Copy a given tape with operations and set parameters""" - tc = t.copy(copy_operations=True) - tc.set_parameters(a, trainable_only=False) + tc = qml.tape.qscript.bind_new_parameters_tape(t, a, list(range(len(a)))) return convert_to_numpy_parameters(tc) if unwrap else tc diff --git a/pennylane/interfaces/tensorflow.py b/pennylane/interfaces/tensorflow.py index 613f4108100..ef1758106d5 100644 --- a/pennylane/interfaces/tensorflow.py +++ b/pennylane/interfaces/tensorflow.py @@ -28,8 +28,7 @@ def _set_copy_and_unwrap_tape(t, a): """Copy a given tape with operations and set parameters""" - tc = t.copy(copy_operations=True) - tc.set_parameters(a, trainable_only=False) + tc = qml.tape.qscript.bind_new_parameters_tape(t, a, list(range(len(a)))) return convert_to_numpy_parameters(tc) diff --git a/pennylane/interfaces/tensorflow_autograph.py b/pennylane/interfaces/tensorflow_autograph.py index fbf494e75d6..e5342b67e77 100644 --- a/pennylane/interfaces/tensorflow_autograph.py +++ b/pennylane/interfaces/tensorflow_autograph.py @@ -31,21 +31,10 @@ _jac_restructured, _res_restructured, _to_tensors, + set_parameters_on_copy_and_unwrap, ) -def _set_copy_and_unwrap_tape(t, a): - """Copy a given tape with operations and set parameters""" - tc = t.copy(copy_operations=True) - tc.set_parameters(a, trainable_only=False) - return convert_to_numpy_parameters(tc) - - -def set_parameters_on_copy_and_unwrap(tapes, params): - """Copy a set of tapes with operations and set parameters""" - return tuple(_set_copy_and_unwrap_tape(t, a) for t, a in zip(tapes, params)) - - def _flatten_nested_list(x): """ Recursively flatten the list diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 6c943f51411..90df37f206a 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -35,6 +35,7 @@ VarianceMP, Shots, ) +from pennylane.typing import TensorLike from pennylane.operation import Observable, Operator, Operation from pennylane.queuing import AnnotatedQueue, process_queue @@ -1410,3 +1411,68 @@ def wrapper(*args, **kwargs): return qscript return wrapper + + +def bind_new_parameters_tape( + tape: QuantumScript, params: Sequence[TensorLike], indices: Sequence[int] +): + """Create a new operator with updated parameters. + + This function takes a :class:`~.tape.QuantumScript` and new parameters as + input, and returns a new ``QuantumScript`` containing the new parameters. + This function does not mutate the original ``QuantumScript``. + + Args: + tape (.tape.QuantumScript): Tape to update + params (Sequence[TensorLike]): New parameters to create tape with. This + must have the same shape as ``tape.get_parameters``. + + Returns: + .tape.QuantumScript: New tape with updated parameters + """ + new_ops = [] + idx = 0 + p_idx = 0 + + for op in tape.circuit: + if isinstance(op, Operator): + data = op.data + elif op.obs is not None: + data = op.obs.data + else: + data = () + + # determine if any parameters of the operator need to be rebinded + if any(i + idx in indices for i in range(len(data))): + new_params = [] + for i in range(len(data)): + if i + idx in indices: + new_params.append(params[p_idx]) + p_idx += 1 + else: + new_params.append(data[i]) + + if isinstance(op, Operator): + new_op = qml.ops.functions.bind_new_parameters(op, new_params) + new_op._check_batching(new_op.data) + else: + new_obs = qml.ops.functions.bind_new_parameters(op.obs, new_params) + new_obs._check_batching(new_obs.data) + new_op = op.__class__(obs=new_obs) + + new_ops.append(new_op) + else: + # no need to change the operator + new_ops.append(op) + + idx += len(data) + + new_prep = new_ops[: len(tape._prep)] + new_operations = new_ops[len(tape._prep) : len(tape.operations)] + new_measurements = new_ops[len(tape.operations) :] + + new_tape = qml.tape.QuantumScript(new_operations, new_measurements, new_prep, shots=tape.shots) + new_tape.trainable_params = tape.trainable_params + new_tape._qfunc_output = tape._qfunc_output + + return new_tape From f4c2d04b5b81669460b6bdbcf196fdcdc05762d0 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Mon, 10 Jul 2023 15:53:11 -0400 Subject: [PATCH 02/12] Fix some tests --- pennylane/pulse/parametrized_evolution.py | 17 ++++++++++++++++- .../core/test_pulse_generator_gradient.py | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pennylane/pulse/parametrized_evolution.py b/pennylane/pulse/parametrized_evolution.py index 383698decf0..fe76eee8ec1 100644 --- a/pennylane/pulse/parametrized_evolution.py +++ b/pennylane/pulse/parametrized_evolution.py @@ -18,11 +18,13 @@ This file contains the ``ParametrizedEvolution`` operator. """ -from typing import List, Union +from typing import List, Union, Sequence import warnings import pennylane as qml from pennylane.operation import AnyWires, Operation +from pennylane.typing import TensorLike +from pennylane.ops import functions from .parametrized_hamiltonian import ParametrizedHamiltonian from .hardware_hamiltonian import HardwareHamiltonian @@ -502,3 +504,16 @@ def fun(y, t): elif not self.hyperparameters["return_intermediate"]: mat = mat[-1] return qml.math.expand_matrix(mat, wires=self.wires, wire_order=wire_order) + + +@functions.bind_new_parameters.register +def bind_new_parameters_parametrized_evol(op: ParametrizedEvolution, params: Sequence[TensorLike]): + return ParametrizedEvolution( + op.H, + params=params, + t=op.t, + return_intermediate=op.hyperparameters["return_intermediate"], + complementary=op.hyperparameters["complementary"], + dense=op.dense, + **op.odeint_kwargs, + ) diff --git a/tests/gradients/core/test_pulse_generator_gradient.py b/tests/gradients/core/test_pulse_generator_gradient.py index 3692cca3b4c..4f9eaaaef5d 100644 --- a/tests/gradients/core/test_pulse_generator_gradient.py +++ b/tests/gradients/core/test_pulse_generator_gradient.py @@ -1394,7 +1394,9 @@ def ansatz(params): assert dev.num_executions == 1 + 12 # one forward execution, dim(DLA)=6 grad_backprop = jax.grad(qnode_backprop)(params) - assert all(qml.math.allclose(r, e) for r, e in zip(grad_pulse_grad, grad_backprop)) + assert all( + qml.math.allclose(r, e, atol=1e-7) for r, e in zip(grad_pulse_grad, grad_backprop) + ) @pytest.mark.parametrize("argnums", [[0, 1], 0, 1]) def test_simple_qnode_expval_multiple_params(self, argnums): From b9a09e2dc88727f5c3f3ce42e6479c83227de2bb Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Mon, 10 Jul 2023 17:08:14 -0400 Subject: [PATCH 03/12] pylint --- pennylane/interfaces/jax_jit.py | 1 - pennylane/interfaces/tensorflow_autograph.py | 1 - pennylane/pulse/parametrized_evolution.py | 2 +- pennylane/tape/qscript.py | 6 ++++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pennylane/interfaces/jax_jit.py b/pennylane/interfaces/jax_jit.py index 875410c6985..551004143aa 100644 --- a/pennylane/interfaces/jax_jit.py +++ b/pennylane/interfaces/jax_jit.py @@ -25,7 +25,6 @@ from pennylane.interfaces import InterfaceUnsupportedError from pennylane.interfaces.jax import _raise_vector_valued_fwd from pennylane.measurements import ProbabilityMP -from pennylane.transforms import convert_to_numpy_parameters from .jax import set_parameters_on_copy_and_unwrap diff --git a/pennylane/interfaces/tensorflow_autograph.py b/pennylane/interfaces/tensorflow_autograph.py index e5342b67e77..a8b8d68f288 100644 --- a/pennylane/interfaces/tensorflow_autograph.py +++ b/pennylane/interfaces/tensorflow_autograph.py @@ -23,7 +23,6 @@ import pennylane as qml from pennylane.measurements import SampleMP, StateMP -from pennylane.transforms import convert_to_numpy_parameters from .tensorflow import ( _compute_vjp, diff --git a/pennylane/pulse/parametrized_evolution.py b/pennylane/pulse/parametrized_evolution.py index fe76eee8ec1..089a36f0874 100644 --- a/pennylane/pulse/parametrized_evolution.py +++ b/pennylane/pulse/parametrized_evolution.py @@ -507,7 +507,7 @@ def fun(y, t): @functions.bind_new_parameters.register -def bind_new_parameters_parametrized_evol(op: ParametrizedEvolution, params: Sequence[TensorLike]): +def _bind_new_parameters_parametrized_evol(op: ParametrizedEvolution, params: Sequence[TensorLike]): return ParametrizedEvolution( op.H, params=params, diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 90df37f206a..06230fe422e 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -1430,6 +1430,8 @@ def bind_new_parameters_tape( Returns: .tape.QuantumScript: New tape with updated parameters """ + # pylint: disable=no-member + new_ops = [] idx = 0 p_idx = 0 @@ -1445,12 +1447,12 @@ def bind_new_parameters_tape( # determine if any parameters of the operator need to be rebinded if any(i + idx in indices for i in range(len(data))): new_params = [] - for i in range(len(data)): + for i, d in enumerate(data): if i + idx in indices: new_params.append(params[p_idx]) p_idx += 1 else: - new_params.append(data[i]) + new_params.append(d) if isinstance(op, Operator): new_op = qml.ops.functions.bind_new_parameters(op, new_params) From 3a058e06a7617a5639886d288382f4a51b0b4d62 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Tue, 11 Jul 2023 14:59:27 -0400 Subject: [PATCH 04/12] Address PR comments --- pennylane/tape/qscript.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 281a08811de..472d5fd16ee 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -1416,16 +1416,18 @@ def wrapper(*args, **kwargs): def bind_new_parameters_tape( tape: QuantumScript, params: Sequence[TensorLike], indices: Sequence[int] ): - """Create a new operator with updated parameters. + """Create a new tape with updated parameters. - This function takes a :class:`~.tape.QuantumScript` and new parameters as - input, and returns a new ``QuantumScript`` containing the new parameters. - This function does not mutate the original ``QuantumScript``. + This function takes a :class:`~.tape.QuantumScript` as input, and returns + a new ``QuantumScript`` containing the new parameters at the provided indices, + with the parameters at all other indices remaining the same. Args: tape (.tape.QuantumScript): Tape to update - params (Sequence[TensorLike]): New parameters to create tape with. This - must have the same shape as ``tape.get_parameters``. + params (Sequence[TensorLike]): New parameters to create the tape with. This + must have the same length as ``indices``. + indices (Sequence[int]): The parameter indices to update with the given parameters. + The index of a parameter is defined as its index in ``tape.get_parameters()``. Returns: .tape.QuantumScript: New tape with updated parameters @@ -1456,10 +1458,8 @@ def bind_new_parameters_tape( if isinstance(op, Operator): new_op = qml.ops.functions.bind_new_parameters(op, new_params) - new_op._check_batching(new_op.data) else: new_obs = qml.ops.functions.bind_new_parameters(op.obs, new_params) - new_obs._check_batching(new_obs.data) new_op = op.__class__(obs=new_obs) new_ops.append(new_op) From 8b8852e9e1b884528c16d03247ef90b578db2c44 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Tue, 11 Jul 2023 16:05:32 -0400 Subject: [PATCH 05/12] Change set_parameter tests to use bind_new_parameters --- pennylane/tape/qscript.py | 6 +++ tests/tape/test_qscript.py | 26 +---------- tests/tape/test_tape.py | 94 +++++++++++++++----------------------- 3 files changed, 45 insertions(+), 81 deletions(-) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 472d5fd16ee..720c0fe5fff 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -1434,6 +1434,9 @@ def bind_new_parameters_tape( """ # pylint: disable=no-member + if len(params) != len(indices): + raise ValueError("Number of provided parameters does not match number of indices") + new_ops = [] idx = 0 p_idx = 0 @@ -1478,3 +1481,6 @@ def bind_new_parameters_tape( new_tape._qfunc_output = tape._qfunc_output return new_tape + + +QuantumScript.bind_new_parameters = bind_new_parameters_tape diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index 4fa8548763c..5c49d182e79 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -249,7 +249,7 @@ def test_update_observables(self): ) def test_update_batch_size(self, x, rot, exp_batch_size): """Test that the batch size is correctly inferred from all operation's - batch_size, when creating and when using `set_parameters`.""" + batch_size when creating""" obs = [qml.RX(x, wires=0), qml.Rot(*rot, wires=1)] m = [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))] @@ -502,18 +502,6 @@ def test_shallow_copy(self): # check that the output dim is identical assert qs.output_dim == copied_qs.output_dim - # since the copy is shallow, mutating the parameters - # on one tape will affect the parameters on another tape - new_params = [np.array([0, 0]), 0.2] - qs.set_parameters(new_params) - - # check that they are the same objects in memory - for i, j in zip(qs.get_parameters(), new_params): - assert i is j - - for i, j in zip(copied_qs.get_parameters(), new_params): - assert i is j - # pylint: disable=unnecessary-lambda @pytest.mark.parametrize( "copy_fn", [lambda tape: tape.copy(copy_operations=True), lambda tape: copy.copy(tape)] @@ -550,18 +538,6 @@ def test_shallow_copy_with_operations(self, copy_fn): # check that the output dim is identical assert qs.output_dim == copied_qs.output_dim - # Since they have unique operations, mutating the parameters - # on one script will *not* affect the parameters on another script - new_params = [np.array([0, 0]), 0.2] - qs.set_parameters(new_params) - - for i, j in zip(qs.get_parameters(), new_params): - assert i is j - - for i, j in zip(copied_qs.get_parameters(), new_params): - assert not np.all(i == j) - assert i is not j - def test_deep_copy(self): """Test that deep copying a tape works, and copies all constituent data except parameters""" prep = [qml.BasisState(np.array([1, 0]), wires=(0, 1))] diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index d4f44896565..33317095d21 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -332,7 +332,7 @@ def f(x): ) def test_update_batch_size(self, x, rot, exp_batch_size): """Test that the batch size is correctly inferred from all operation's - batch_size, when creating and when using `set_parameters`.""" + batch_size, when creating and when using `bind_new_parameters`.""" # Test with tape construction with qml.queuing.AnnotatedQueue() as q: @@ -344,7 +344,7 @@ def test_update_batch_size(self, x, rot, exp_batch_size): tape = qml.tape.QuantumScript.from_queue(q) assert tape.batch_size == exp_batch_size - # Test with set_parameters + # Test with bind_new_parameters with qml.queuing.AnnotatedQueue() as q: qml.RX(0.2, wires=0) qml.Rot(1.0, 0.2, -0.3, wires=1) @@ -354,7 +354,7 @@ def test_update_batch_size(self, x, rot, exp_batch_size): tape = qml.tape.QuantumScript.from_queue(q) assert tape.batch_size is None - tape.set_parameters([x] + rot) + tape = tape.bind_new_parameters([x] + rot, [0, 1, 2, 3]) assert tape.batch_size == exp_batch_size @pytest.mark.parametrize( @@ -366,7 +366,7 @@ def test_update_batch_size(self, x, rot, exp_batch_size): ) def test_error_inconsistent_batch_sizes(self, x, rot, y): """Test that the batch size is correctly inferred from all operation's - batch_size, when creating and when using `set_parameters`.""" + batch_size, when creating and when using `bind_new_parameters`.""" with pytest.raises( ValueError, match="batch sizes of the quantum script operations do not match." @@ -387,7 +387,7 @@ def test_error_inconsistent_batch_sizes(self, x, rot, y): with pytest.raises( ValueError, match="batch sizes of the quantum script operations do not match." ): - tape.set_parameters([x] + rot + [y]) + tape.bind_new_parameters([x] + rot + [y], [0, 1, 2, 3, 4]) class TestIteration: @@ -727,15 +727,16 @@ def test_set_trainable_params_error(self, make_tape): def test_setting_parameters(self, make_tape): """Test that parameters are correctly modified after construction""" - tape, _ = make_tape + tape, params = make_tape new_params = [0.6543, -0.654, 0, 0.3, 0.6] - tape.set_parameters(new_params) + new_tape = tape.bind_new_parameters(new_params, [0, 1, 2, 3, 4]) - for pinfo, pval in zip(tape._par_info, new_params): + for pinfo, pval in zip(new_tape._par_info, new_params): assert pinfo["op"].data[pinfo["p_idx"]] == pval - assert tape.get_parameters() == new_params + assert new_tape.get_parameters() == new_params + assert tape.get_parameters() == params new_params = (0.1, -0.2, 1, 5, 0) tape.data = new_params @@ -751,23 +752,24 @@ def test_setting_free_parameters(self, make_tape): new_params = [-0.654, 0.3] tape.trainable_params = [1, 3] - tape.set_parameters(new_params) + new_tape = tape.bind_new_parameters(new_params, tape.trainable_params) count = 0 - for idx, pinfo in enumerate(tape._par_info): + for idx, pinfo in enumerate(new_tape._par_info): if idx in tape.trainable_params: assert pinfo["op"].data[pinfo["p_idx"]] == new_params[count] count += 1 else: assert pinfo["op"].data[pinfo["p_idx"]] == params[idx] - assert tape.get_parameters(trainable_only=False) == [ + assert new_tape.get_parameters(trainable_only=False) == [ params[0], new_params[0], params[2], new_params[1], params[4], ] + assert tape.get_parameters(trainable_only=False) == params def test_setting_parameters_unordered(self, make_tape): """Test that an 'unordered' trainable_params set does not affect @@ -776,29 +778,31 @@ def test_setting_parameters_unordered(self, make_tape): new_params = [-0.654, 0.3] tape.trainable_params = [3, 1] - tape.set_parameters(new_params) + new_tape = tape.bind_new_parameters(new_params, tape.trainable_params) - assert tape.get_parameters(trainable_only=True) == new_params - assert tape.get_parameters(trainable_only=False) == [ + assert new_tape.get_parameters(trainable_only=True) == new_params + assert new_tape.get_parameters(trainable_only=False) == [ params[0], new_params[0], params[2], new_params[1], params[4], ] + assert tape.get_parameters(trainable_only=False) == params def test_setting_all_parameters(self, make_tape): """Test that all parameters are correctly modified after construction""" - tape, _ = make_tape + tape, params = make_tape new_params = [0.6543, -0.654, 0, 0.3, 0.6] tape.trainable_params = [1, 3] - tape.set_parameters(new_params, trainable_only=False) + new_tape = tape.bind_new_parameters(new_params, [0, 1, 2, 3, 4]) - for pinfo, pval in zip(tape._par_info, new_params): + for pinfo, pval in zip(new_tape._par_info, new_params): assert pinfo["op"].data[pinfo["p_idx"]] == pval - assert tape.get_parameters(trainable_only=False) == new_params + assert new_tape.get_parameters(trainable_only=False) == new_params + assert tape.get_parameters(trainable_only=False) == params def test_setting_parameters_error(self, make_tape): """Test that exceptions are raised if incorrect parameters @@ -806,11 +810,10 @@ def test_setting_parameters_error(self, make_tape): tape, _ = make_tape with pytest.raises(ValueError, match="Number of provided parameters does not match"): - tape.set_parameters([0.54]) + tape.bind_new_parameters([0.54], [0, 1, 2, 3, 4]) with pytest.raises(ValueError, match="Number of provided parameters does not match"): - tape.trainable_params = [2, 3] - tape.set_parameters([0.54, 0.54, 0.123]) + tape.bind_new_parameters([0.54, 0.54, 0.123], [0, 1]) def test_array_parameter(self): """Test that array parameters integrate properly""" @@ -826,10 +829,12 @@ def test_array_parameter(self): b = np.array([0, 1, 0, 0]) new_params = [b, 0.543, 0.654, 0.123] - tape.set_parameters(new_params) - assert tape.get_parameters() == new_params + new_tape = tape.bind_new_parameters(new_params, [0, 1, 2, 3]) + assert new_tape.get_parameters() == new_params + assert tape.get_parameters() == params - assert np.all(op_.data[0] == b) + assert np.all(op_.data[0] == a) + assert np.all(new_tape[0].data[0] == b) def test_measurement_parameter(self): """Test that measurement parameters integrate properly""" @@ -846,10 +851,12 @@ def test_measurement_parameter(self): H2 = np.array([[0, 1], [1, 1]]) new_params = [0.543, 0.654, 0.123, H2] - tape.set_parameters(new_params) - assert tape.get_parameters() == new_params + new_tape = tape.bind_new_parameters(new_params, [0, 1, 2, 3]) + assert new_tape.get_parameters() == new_params + assert tape.get_parameters() == params - assert np.all(obs.data[0] == H2) + assert np.all(obs.data[0] == H) + assert np.all(new_tape[1].obs.data[0] == H2) class TestInverseAdjoint: @@ -900,7 +907,6 @@ def test_decomposition(self): # check that modifying the new tape does not affect the old tape new_tape.trainable_params = [0] - new_tape.set_parameters([10]) assert tape.get_parameters() == [0.1, 0.2, 0.3] assert tape.trainable_params == [0, 1, 2] @@ -1237,10 +1243,10 @@ def test_execute_parameters(self, tol): assert tape.get_parameters() == params # test setting parameters - tape.set_parameters(params=[0.5, 0.6]) - res3 = dev.execute(tape) + new_tape = tape.bind_new_parameters(params=[0.5, 0.6], indices=[0, 1]) + res3 = dev.execute(new_tape) assert not np.allclose(res1, res3, atol=tol, rtol=0) - assert tape.get_parameters() == [0.5, 0.6] + assert new_tape.get_parameters() == [0.5, 0.6] def test_no_output_execute(self): """Test that tapes with no measurement process return @@ -1477,18 +1483,6 @@ def test_shallow_copy(self): # check that the output dim is identical assert tape.output_dim == copied_tape.output_dim - # since the copy is shallow, mutating the parameters - # on one tape will affect the parameters on another tape - new_params = [np.array([0, 0]), 0.2] - tape.set_parameters(new_params) - - # check that they are the same objects in memory - for i, j in zip(tape.get_parameters(), new_params): - assert i is j - - for i, j in zip(copied_tape.get_parameters(), new_params): - assert i is j - @pytest.mark.parametrize("copy_fn", [lambda tape: tape.copy(copy_operations=True), copy.copy]) def test_shallow_copy_with_operations(self, copy_fn): """Test that shallow copying of a tape and operations allows @@ -1521,18 +1515,6 @@ def test_shallow_copy_with_operations(self, copy_fn): # check that the output dim is identical assert tape.output_dim == copied_tape.output_dim - # Since they have unique operations, mutating the parameters - # on one tape will *not* affect the parameters on another tape - new_params = [np.array([0, 0]), 0.2] - tape.set_parameters(new_params) - - for i, j in zip(tape.get_parameters(), new_params): - assert i is j - - for i, j in zip(copied_tape.get_parameters(), new_params): - assert not np.all(i == j) - assert i is not j - def test_deep_copy(self): """Test that deep copying a tape works, and copies all constituent data except parameters""" with QuantumTape() as tape: From 359b0397a293684bbdb595271abcb363ff07dbc5 Mon Sep 17 00:00:00 2001 From: Edward Jiang <34989448+eddddddy@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:12:31 -0400 Subject: [PATCH 06/12] Update tests/tape/test_qscript.py Co-authored-by: Mudit Pandey --- tests/tape/test_qscript.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index 5c49d182e79..c649b905d2f 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -249,7 +249,7 @@ def test_update_observables(self): ) def test_update_batch_size(self, x, rot, exp_batch_size): """Test that the batch size is correctly inferred from all operation's - batch_size when creating""" + batch_size when creating a QuantumScript.""" obs = [qml.RX(x, wires=0), qml.Rot(*rot, wires=1)] m = [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))] From c967675e9598d97c5e62bdcb1712d63d4ec4917a Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Thu, 13 Jul 2023 15:27:47 -0400 Subject: [PATCH 07/12] Only iterate over necessary ops --- pennylane/tape/qscript.py | 50 ++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 720c0fe5fff..dd07e89d49c 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -1437,40 +1437,32 @@ def bind_new_parameters_tape( if len(params) != len(indices): raise ValueError("Number of provided parameters does not match number of indices") - new_ops = [] - idx = 0 - p_idx = 0 + # determine the ops that need to be updated + op_indices = {} + for param_idx, idx in enumerate(sorted(indices)): + pinfo = tape._par_info[idx] + op_idx, p_idx = pinfo["op_idx"], pinfo["p_idx"] - for op in tape.circuit: - if isinstance(op, Operator): - data = op.data - elif op.obs is not None: - data = op.obs.data - else: - data = () - - # determine if any parameters of the operator need to be rebinded - if any(i + idx in indices for i in range(len(data))): - new_params = [] - for i, d in enumerate(data): - if i + idx in indices: - new_params.append(params[p_idx]) - p_idx += 1 - else: - new_params.append(d) + if op_idx not in op_indices: + op_indices[op_idx] = {} - if isinstance(op, Operator): - new_op = qml.ops.functions.bind_new_parameters(op, new_params) - else: - new_obs = qml.ops.functions.bind_new_parameters(op.obs, new_params) - new_op = op.__class__(obs=new_obs) + op_indices[op_idx][p_idx] = param_idx - new_ops.append(new_op) + new_ops = tape.circuit + + for op_idx, p_indices in op_indices.items(): + op = new_ops[op_idx] + data = op.data if isinstance(op, Operator) else op.obs.data + + new_params = [params[p_indices[i]] if i in p_indices else d for i, d in enumerate(data)] + + if isinstance(op, Operator): + new_op = qml.ops.functions.bind_new_parameters(op, new_params) else: - # no need to change the operator - new_ops.append(op) + new_obs = qml.ops.functions.bind_new_parameters(op.obs, new_params) + new_op = op.__class__(obs=new_obs) - idx += len(data) + new_ops[op_idx] = new_op new_prep = new_ops[: len(tape._prep)] new_operations = new_ops[len(tape._prep) : len(tape.operations)] From 07e42a87431a46f58457b45aefe6f6ececa34cb3 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Fri, 14 Jul 2023 10:40:36 -0400 Subject: [PATCH 08/12] Make method --- pennylane/interfaces/jax.py | 2 +- pennylane/interfaces/jax_jit_tuple.py | 2 +- pennylane/interfaces/tensorflow.py | 2 +- pennylane/tape/qscript.py | 125 +++++++++++++------------- 4 files changed, 63 insertions(+), 68 deletions(-) diff --git a/pennylane/interfaces/jax.py b/pennylane/interfaces/jax.py index cfa030d3884..20e66ceceec 100644 --- a/pennylane/interfaces/jax.py +++ b/pennylane/interfaces/jax.py @@ -29,7 +29,7 @@ def _set_copy_and_unwrap_tape(t, a, unwrap=True): """Copy a given tape with operations and set parameters""" - tc = qml.tape.qscript.bind_new_parameters_tape(t, a, t.trainable_params) + tc = t.bind_new_parameters(a, t.trainable_params) return convert_to_numpy_parameters(tc) if unwrap else tc diff --git a/pennylane/interfaces/jax_jit_tuple.py b/pennylane/interfaces/jax_jit_tuple.py index d1dd80b677e..4ca6f2e518a 100644 --- a/pennylane/interfaces/jax_jit_tuple.py +++ b/pennylane/interfaces/jax_jit_tuple.py @@ -34,7 +34,7 @@ def _set_copy_and_unwrap_tape(t, a, unwrap=True): """Copy a given tape with operations and set parameters""" - tc = qml.tape.qscript.bind_new_parameters_tape(t, a, list(range(len(a)))) + tc = t.bind_new_parameters(a, list(range(len(a)))) return convert_to_numpy_parameters(tc) if unwrap else tc diff --git a/pennylane/interfaces/tensorflow.py b/pennylane/interfaces/tensorflow.py index ef1758106d5..7a2335ccdf7 100644 --- a/pennylane/interfaces/tensorflow.py +++ b/pennylane/interfaces/tensorflow.py @@ -28,7 +28,7 @@ def _set_copy_and_unwrap_tape(t, a): """Copy a given tape with operations and set parameters""" - tc = qml.tape.qscript.bind_new_parameters_tape(t, a, list(range(len(a)))) + tc = t.bind_new_parameters(a, list(range(len(a)))) return convert_to_numpy_parameters(tc) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index dd07e89d49c..9ad51c3c6e3 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -719,6 +719,66 @@ def set_parameters(self, params, trainable_only=True): self._update_batch_size() self._update_output_dim() + def bind_new_parameters(self, params: Sequence[TensorLike], indices: Sequence[int]): + """Create a new tape with updated parameters. + + This function takes a :class:`~.tape.QuantumScript` as input, and returns + a new ``QuantumScript`` containing the new parameters at the provided indices, + with the parameters at all other indices remaining the same. + + Args: + params (Sequence[TensorLike]): New parameters to create the tape with. This + must have the same length as ``indices``. + indices (Sequence[int]): The parameter indices to update with the given parameters. + The index of a parameter is defined as its index in ``tape.get_parameters()``. + + Returns: + .tape.QuantumScript: New tape with updated parameters + """ + # pylint: disable=no-member + + if len(params) != len(indices): + raise ValueError("Number of provided parameters does not match number of indices") + + # determine the ops that need to be updated + op_indices = {} + for param_idx, idx in enumerate(sorted(indices)): + pinfo = self._par_info[idx] + op_idx, p_idx = pinfo["op_idx"], pinfo["p_idx"] + + if op_idx not in op_indices: + op_indices[op_idx] = {} + + op_indices[op_idx][p_idx] = param_idx + + new_ops = self.circuit + + for op_idx, p_indices in op_indices.items(): + op = new_ops[op_idx] + data = op.data if isinstance(op, Operator) else op.obs.data + + new_params = [params[p_indices[i]] if i in p_indices else d for i, d in enumerate(data)] + + if isinstance(op, Operator): + new_op = qml.ops.functions.bind_new_parameters(op, new_params) + else: + new_obs = qml.ops.functions.bind_new_parameters(op.obs, new_params) + new_op = op.__class__(obs=new_obs) + + new_ops[op_idx] = new_op + + new_prep = new_ops[: len(self._prep)] + new_operations = new_ops[len(self._prep) : len(self.operations)] + new_measurements = new_ops[len(self.operations) :] + + new_tape = qml.tape.QuantumScript( + new_operations, new_measurements, new_prep, shots=self.shots + ) + new_tape.trainable_params = self.trainable_params + new_tape._qfunc_output = self._qfunc_output + + return new_tape + # ======================================================== # MEASUREMENT SHAPE # @@ -1411,68 +1471,3 @@ def wrapper(*args, **kwargs): return qscript return wrapper - - -def bind_new_parameters_tape( - tape: QuantumScript, params: Sequence[TensorLike], indices: Sequence[int] -): - """Create a new tape with updated parameters. - - This function takes a :class:`~.tape.QuantumScript` as input, and returns - a new ``QuantumScript`` containing the new parameters at the provided indices, - with the parameters at all other indices remaining the same. - - Args: - tape (.tape.QuantumScript): Tape to update - params (Sequence[TensorLike]): New parameters to create the tape with. This - must have the same length as ``indices``. - indices (Sequence[int]): The parameter indices to update with the given parameters. - The index of a parameter is defined as its index in ``tape.get_parameters()``. - - Returns: - .tape.QuantumScript: New tape with updated parameters - """ - # pylint: disable=no-member - - if len(params) != len(indices): - raise ValueError("Number of provided parameters does not match number of indices") - - # determine the ops that need to be updated - op_indices = {} - for param_idx, idx in enumerate(sorted(indices)): - pinfo = tape._par_info[idx] - op_idx, p_idx = pinfo["op_idx"], pinfo["p_idx"] - - if op_idx not in op_indices: - op_indices[op_idx] = {} - - op_indices[op_idx][p_idx] = param_idx - - new_ops = tape.circuit - - for op_idx, p_indices in op_indices.items(): - op = new_ops[op_idx] - data = op.data if isinstance(op, Operator) else op.obs.data - - new_params = [params[p_indices[i]] if i in p_indices else d for i, d in enumerate(data)] - - if isinstance(op, Operator): - new_op = qml.ops.functions.bind_new_parameters(op, new_params) - else: - new_obs = qml.ops.functions.bind_new_parameters(op.obs, new_params) - new_op = op.__class__(obs=new_obs) - - new_ops[op_idx] = new_op - - new_prep = new_ops[: len(tape._prep)] - new_operations = new_ops[len(tape._prep) : len(tape.operations)] - new_measurements = new_ops[len(tape.operations) :] - - new_tape = qml.tape.QuantumScript(new_operations, new_measurements, new_prep, shots=tape.shots) - new_tape.trainable_params = tape.trainable_params - new_tape._qfunc_output = tape._qfunc_output - - return new_tape - - -QuantumScript.bind_new_parameters = bind_new_parameters_tape From dac40ad0c07c422c74ca7644817121bcb17828f0 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Fri, 14 Jul 2023 12:29:59 -0400 Subject: [PATCH 09/12] Add back old tests --- tests/tape/test_tape.py | 226 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 33317095d21..99fcdb110d2 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -859,6 +859,232 @@ def test_measurement_parameter(self): assert np.all(new_tape[1].obs.data[0] == H2) +class TestParametersOld: + """Tests for parameter processing, setting, and manipulation""" + + @pytest.fixture + def make_tape(self): + params = [0.432, 0.123, 0.546, 0.32, 0.76] + + with QuantumTape() as tape: + qml.RX(params[0], wires=0) + qml.Rot(*params[1:4], wires=0) + qml.CNOT(wires=[0, "a"]) + qml.RX(params[4], wires=4) + qml.expval(qml.PauliX(wires="a")) + qml.probs(wires=[0, "a"]) + + return tape, params + + @pytest.fixture + def make_tape_with_hermitian(self): + params = [0.432, 0.123, 0.546, 0.32, 0.76] + hermitian = qml.numpy.eye(2, requires_grad=False) + + with QuantumTape() as tape: + qml.RX(params[0], wires=0) + qml.Rot(*params[1:4], wires=0) + qml.CNOT(wires=[0, "a"]) + qml.RX(params[4], wires=4) + qml.expval(qml.Hermitian(hermitian, wires="a")) + + return tape, params, hermitian + + def test_parameter_processing(self, make_tape): + """Test that parameters are correctly counted and processed""" + tape, params = make_tape + assert tape.num_params == len(params) + assert tape.trainable_params == list(range(len(params))) + assert tape.get_parameters() == params + + @pytest.mark.parametrize("operations_only", [False, True]) + def test_parameter_processing_operations_only(self, make_tape_with_hermitian, operations_only): + """Test the operations_only flag for getting the parameters on a tape with + qml.Hermitian is measured""" + tape, circuit_params, hermitian = make_tape_with_hermitian + num_all_params = len(circuit_params) + 1 # + 1 for hermitian + assert tape.num_params == num_all_params + assert tape.trainable_params == list(range(num_all_params)) + assert ( + tape.get_parameters(operations_only=operations_only) == circuit_params + if operations_only + else circuit_params + [hermitian] + ) + + def test_set_trainable_params(self, make_tape): + """Test that setting trainable parameters works as expected""" + tape, params = make_tape + trainable = [0, 2, 3] + tape.trainable_params = trainable + assert tape._trainable_params == trainable + assert tape.num_params == 3 + assert tape.get_parameters() == [params[i] for i in tape.trainable_params] + + # add additional trainable parameters + trainable = {1, 2, 3, 4} + tape.trainable_params = trainable + assert tape._trainable_params == [1, 2, 3, 4] + assert tape.num_params == 4 + assert tape.get_parameters() == [params[i] for i in tape.trainable_params] + + # set trainable_params in wrong order + trainable = {3, 4, 1} + tape.trainable_params = trainable + assert tape._trainable_params == [1, 3, 4] + assert tape.num_params == 3 + assert tape.get_parameters() == [params[i] for i in tape.trainable_params] + + def test_changing_params(self, make_tape): + """Test that changing trainable parameters works as expected""" + tape, params = make_tape + trainable = (0, 2, 3) + tape.trainable_params = trainable + assert tape._trainable_params == list(trainable) + assert tape.num_params == 3 + assert tape.get_parameters() == [params[i] for i in tape.trainable_params] + assert tape.get_parameters(trainable_only=False) == params + + def test_set_trainable_params_error(self, make_tape): + """Test that exceptions are raised if incorrect parameters + are set as trainable""" + tape, _ = make_tape + + with pytest.raises(ValueError, match="must be non-negative integers"): + tape.trainable_params = [-1, 0] + + with pytest.raises(ValueError, match="must be non-negative integers"): + tape.trainable_params = (0.5,) + + with pytest.raises(ValueError, match="only has 5 parameters"): + tape.trainable_params = {0, 7} + + def test_setting_parameters(self, make_tape): + """Test that parameters are correctly modified after construction""" + tape, _ = make_tape + new_params = [0.6543, -0.654, 0, 0.3, 0.6] + + tape.set_parameters(new_params) + + for pinfo, pval in zip(tape._par_info, new_params): + assert pinfo["op"].data[pinfo["p_idx"]] == pval + + assert tape.get_parameters() == new_params + + new_params = (0.1, -0.2, 1, 5, 0) + tape.data = new_params + + for pinfo, pval in zip(tape._par_info, new_params): + assert pinfo["op"].data[pinfo["p_idx"]] == pval + + assert tape.get_parameters() == list(new_params) + + def test_setting_free_parameters(self, make_tape): + """Test that free parameters are correctly modified after construction""" + tape, params = make_tape + new_params = [-0.654, 0.3] + + tape.trainable_params = [1, 3] + tape.set_parameters(new_params) + + count = 0 + for idx, pinfo in enumerate(tape._par_info): + if idx in tape.trainable_params: + assert pinfo["op"].data[pinfo["p_idx"]] == new_params[count] + count += 1 + else: + assert pinfo["op"].data[pinfo["p_idx"]] == params[idx] + + assert tape.get_parameters(trainable_only=False) == [ + params[0], + new_params[0], + params[2], + new_params[1], + params[4], + ] + + def test_setting_parameters_unordered(self, make_tape): + """Test that an 'unordered' trainable_params set does not affect + the setting of parameter values""" + tape, params = make_tape + new_params = [-0.654, 0.3] + + tape.trainable_params = [3, 1] + tape.set_parameters(new_params) + + assert tape.get_parameters(trainable_only=True) == new_params + assert tape.get_parameters(trainable_only=False) == [ + params[0], + new_params[0], + params[2], + new_params[1], + params[4], + ] + + def test_setting_all_parameters(self, make_tape): + """Test that all parameters are correctly modified after construction""" + tape, _ = make_tape + new_params = [0.6543, -0.654, 0, 0.3, 0.6] + + tape.trainable_params = [1, 3] + tape.set_parameters(new_params, trainable_only=False) + + for pinfo, pval in zip(tape._par_info, new_params): + assert pinfo["op"].data[pinfo["p_idx"]] == pval + + assert tape.get_parameters(trainable_only=False) == new_params + + def test_setting_parameters_error(self, make_tape): + """Test that exceptions are raised if incorrect parameters + are attempted to be set""" + tape, _ = make_tape + + with pytest.raises(ValueError, match="Number of provided parameters does not match"): + tape.set_parameters([0.54]) + + with pytest.raises(ValueError, match="Number of provided parameters does not match"): + tape.trainable_params = [2, 3] + tape.set_parameters([0.54, 0.54, 0.123]) + + def test_array_parameter(self): + """Test that array parameters integrate properly""" + a = np.array([1, 1, 0, 0]) / np.sqrt(2) + params = [a, 0.32, 0.76, 1.0] + + with QuantumTape() as tape: + op_ = qml.QubitStateVector(params[0], wires=[0, 1]) + qml.Rot(params[1], params[2], params[3], wires=0) + + assert tape.num_params == len(params) + assert tape.get_parameters() == params + + b = np.array([0, 1, 0, 0]) + new_params = [b, 0.543, 0.654, 0.123] + tape.set_parameters(new_params) + assert tape.get_parameters() == new_params + + assert np.all(op_.data[0] == b) + + def test_measurement_parameter(self): + """Test that measurement parameters integrate properly""" + H = np.array([[1, 0], [0, -1]]) + params = [0.32, 0.76, 1.0, H] + + with QuantumTape() as tape: + qml.Rot(params[0], params[1], params[2], wires=0) + obs = qml.Hermitian(params[3], wires=0) + qml.expval(obs) + + assert tape.num_params == len(params) + assert tape.get_parameters() == params + + H2 = np.array([[0, 1], [1, 1]]) + new_params = [0.543, 0.654, 0.123, H2] + tape.set_parameters(new_params) + assert tape.get_parameters() == new_params + + assert np.all(obs.data[0] == H2) + + class TestInverseAdjoint: """Tests for tape inversion""" From bca5a1cd5ccd7ddd0e9c426e78a99131ca51e349 Mon Sep 17 00:00:00 2001 From: Edward Jiang <34989448+eddddddy@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:42:19 -0400 Subject: [PATCH 10/12] Update pennylane/tape/qscript.py Co-authored-by: Matthew Silverman --- pennylane/tape/qscript.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 9ad51c3c6e3..68183dacd62 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -771,7 +771,7 @@ def bind_new_parameters(self, params: Sequence[TensorLike], indices: Sequence[in new_operations = new_ops[len(self._prep) : len(self.operations)] new_measurements = new_ops[len(self.operations) :] - new_tape = qml.tape.QuantumScript( + new_tape = self.__class__( new_operations, new_measurements, new_prep, shots=self.shots ) new_tape.trainable_params = self.trainable_params From 1d5898e02aff514e5b83b170279a320c5c288bde Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Fri, 14 Jul 2023 14:33:09 -0400 Subject: [PATCH 11/12] black --- pennylane/tape/qscript.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index 68183dacd62..1ae1ab185f9 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -771,9 +771,7 @@ def bind_new_parameters(self, params: Sequence[TensorLike], indices: Sequence[in new_operations = new_ops[len(self._prep) : len(self.operations)] new_measurements = new_ops[len(self.operations) :] - new_tape = self.__class__( - new_operations, new_measurements, new_prep, shots=self.shots - ) + new_tape = self.__class__(new_operations, new_measurements, new_prep, shots=self.shots) new_tape.trainable_params = self.trainable_params new_tape._qfunc_output = self._qfunc_output From 9d4b0470058f448157835c1a38e6d28e95c68e05 Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Fri, 14 Jul 2023 15:24:21 -0400 Subject: [PATCH 12/12] changelog --- doc/releases/changelog-dev.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 03b48058170..b886096b51e 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -40,6 +40,10 @@ * The experimental device interface is integrated with the `QNode` for Jax. [(#4323)](https://github.com/PennyLaneAI/pennylane/pull/4323) +* The `QuantumScript` class now has a `bind_new_parameters` method that allows creation of + new `QuantumScript` objects with the provided parameters. + [(#4345)](https://github.com/PennyLaneAI/pennylane/pull/4345) +

Breaking changes 💔

* The `do_queue` keyword argument in `qml.operation.Operator` has been removed. Instead of