Skip to content

Commit

Permalink
erge branch 'pauli_sentence_matris' of https://github.com/PennyLaneAI…
Browse files Browse the repository at this point in the history
…/pennylane into pauli_sentence_matris
  • Loading branch information
albi3ro committed Apr 25, 2024
2 parents 23ee710 + 66116e4 commit fe08066
Show file tree
Hide file tree
Showing 25 changed files with 611 additions and 405 deletions.
16 changes: 16 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@

<h4>Other improvements</h4>

* `qml.draw` and `qml.draw_mpl` will now attempt to sort the wires if no wire order
is provided by the user or the device.
[(#5576)](https://github.com/PennyLaneAI/pennylane/pull/5576)

* `qml.ops.Conditional` now stores the `data`, `num_params`, and `ndim_param` attributes of
the operator it wraps.
[(#5473)](https://github.com/PennyLaneAI/pennylane/pull/5473)
Expand Down Expand Up @@ -384,6 +388,10 @@

<h3>Breaking changes 💔</h3>

* Applying a `gradient_transform` to a QNode directly now gives the same shape and type independent
of whether there is classical processing in the node.
[(#4945)](https://github.com/PennyLaneAI/pennylane/pull/4945)

* State measurements preserve `dtype`.
[(#5547)](https://github.com/PennyLaneAI/pennylane/pull/5547)

Expand Down Expand Up @@ -492,6 +500,10 @@

<h3>Bug fixes 🐛</h3>

* Fixed a bug where the shape and type of derivatives obtained by applying a gradient transform to
a QNode differed, based on whether the QNode uses classical coprocessing.
[(#4945)](https://github.com/PennyLaneAI/pennylane/pull/4945)

* `ApproxTimeEvolution`, `CommutingEvolution`, `QDrift`, and `TrotterProduct`
now de-queue their input observable.
[(#5524)](https://github.com/PennyLaneAI/pennylane/pull/5524)
Expand Down Expand Up @@ -581,6 +593,10 @@
* Fixes a bug in `hamiltonian_expand` that produces incorrect output dimensions when shot vectors are combined with parameter broadcasting.
[(#5494)](https://github.com/PennyLaneAI/pennylane/pull/5494)

* Allows `default.qubit` to measure Identity on no wires, and observables containing Identity on
no wires.
[(#5570)](https://github.com/PennyLaneAI/pennylane/pull/5570/)

* Fixes a bug where `TorchLayer` does not work with shot vectors.
[(#5492)](https://github.com/PennyLaneAI/pennylane/pull/5492)

Expand Down
17 changes: 17 additions & 0 deletions pennylane/devices/tests/test_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,23 @@ def circuit(state):
assert np.allclose(np.var(res), expected_var, atol=tol(False))


@flaky(max_runs=10)
class TestSumExpval:
"""Test expectation values of Sum observables."""

def test_sum_containing_identity_on_no_wires(self, device, tol):
"""Test that the device can handle Identity on no wires."""
dev = device(1)

@qml.qnode(dev)
def circuit():
qml.X(0)
return qml.expval(qml.Z(0) + 3 * qml.I())

res = circuit()
assert qml.math.allclose(res, 2.0, atol=tol(dev.shots))


@flaky(max_runs=10)
class TestVar:
"""Tests for the variance return type"""
Expand Down
45 changes: 39 additions & 6 deletions pennylane/drawer/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def draw(
Args:
qnode (.QNode or Callable): the input QNode or quantum function that is to be drawn.
wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit
wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit.
If not provided, the wire order defaults to the device wires. If device wires are not
available, the circuit wires are sorted if possible.
show_all_wires (bool): If True, all wires, including empty wires, are printed.
decimals (int): How many decimal points to include when formatting operation parameters.
``None`` will omit parameters from operation labels.
Expand Down Expand Up @@ -230,7 +232,14 @@ def transformed_circuit(x):
@wraps(qnode)
def wrapper(*args, **kwargs):
tape = qml.tape.make_qscript(qnode)(*args, **kwargs)
_wire_order = wire_order or tape.wires

if wire_order:
_wire_order = wire_order
else:
try:
_wire_order = sorted(tape.wires)
except TypeError:
_wire_order = tape.wires

return tape_text(
tape,
Expand Down Expand Up @@ -273,7 +282,15 @@ def wrapper(*args, **kwargs):
finally:
qnode.expansion_strategy = original_expansion_strategy

_wire_order = wire_order or qnode.device.wires or qnode.tape.wires
if wire_order:
_wire_order = wire_order
elif qnode.device.wires:
_wire_order = qnode.device.wires
else:
try:
_wire_order = sorted(tapes[0].wires)
except TypeError:
_wire_order = tapes[0].wires

if tapes is not None:
cache = {"tape_offset": 0, "matrices": []}
Expand Down Expand Up @@ -327,7 +344,9 @@ def draw_mpl(
qnode (.QNode or Callable): the input QNode/quantum function that is to be drawn.
Keyword Args:
wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit
wire_order (Sequence[Any]): the order (from top to bottom) to print the wires of the circuit.
If not provided, the wire order defaults to the device wires. If device wires are not
available, the circuit wires are sorted if possible.
show_all_wires (bool): If True, all wires, including empty wires, are printed.
decimals (int): How many decimal points to include when formatting operation parameters.
Default ``None`` will omit parameters from operation labels.
Expand Down Expand Up @@ -547,7 +566,13 @@ def circuit2(x, y):
@wraps(qnode)
def wrapper(*args, **kwargs):
tape = qml.tape.make_qscript(qnode)(*args, **kwargs)
_wire_order = wire_order or tape.wires
if wire_order:
_wire_order = wire_order
else:
try:
_wire_order = sorted(tape.wires)
except TypeError:
_wire_order = tape.wires

return tape_mpl(
tape,
Expand Down Expand Up @@ -592,7 +617,15 @@ def wrapper(*args, **kwargs_qnode):
finally:
qnode.expansion_strategy = original_expansion_strategy

_wire_order = wire_order or qnode.device.wires or tape.wires
if wire_order:
_wire_order = wire_order
elif qnode.device.wires:
_wire_order = qnode.device.wires
else:
try:
_wire_order = sorted(tape.wires)
except TypeError:
_wire_order = tape.wires

return tape_mpl(
tape,
Expand Down
48 changes: 29 additions & 19 deletions pennylane/gradients/gradient_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ def reorder_grads(grads, tape_specs):
return _move_first_axis_to_third_pos(grads, num_params, shots.num_copies, num_measurements)


tdot = partial(qml.math.tensordot, axes=[[0], [0]])
stack = qml.math.stack


# pylint: disable=too-many-return-statements,too-many-branches
def _contract_qjac_with_cjac(qjac, cjac, tape):
"""Contract a quantum Jacobian with a classical preprocessing Jacobian.
Expand All @@ -397,52 +401,58 @@ def _contract_qjac_with_cjac(qjac, cjac, tape):
cjac = cjac[0]

cjac_is_tuple = isinstance(cjac, tuple)
if not cjac_is_tuple:
is_square = cjac.ndim == 2 and cjac.shape[0] == cjac.shape[1]

if not qml.math.is_abstract(cjac) and (
is_square and qml.math.allclose(cjac, qml.numpy.eye(cjac.shape[0]))
):
# Classical Jacobian is the identity. No classical processing is present in the QNode
return qjac

multi_meas = num_measurements > 1

# This block only figures out whether there is a single tape parameter or not
if cjac_is_tuple:
multi_params = True
single_tape_param = False
else:
# Peel out a single measurement's and single shot setting's qjac
_qjac = qjac
if multi_meas:
_qjac = _qjac[0]
if has_partitioned_shots:
_qjac = _qjac[0]
multi_params = isinstance(_qjac, tuple)

tdot = partial(qml.math.tensordot, axes=[[0], [0]])
single_tape_param = not isinstance(_qjac, tuple)

if not multi_params:
if single_tape_param:
# Without dimension (e.g. expval) or with dimension (e.g. probs)
def _reshape(x):
return qml.math.reshape(x, (1,) if x.shape == () else (1, -1))

if not (multi_meas or has_partitioned_shots):
# Single parameter, single measurements
# Single parameter, single measurements, no shot vector
return tdot(_reshape(qjac), cjac)

if not (multi_meas and has_partitioned_shots):
# Single parameter, multiple measurements or shot vector, but not both
return tuple(tdot(_reshape(q), cjac) for q in qjac)

# Single parameter, multiple measurements
# Single parameter, multiple measurements, and shot vector
return tuple(tuple(tdot(_reshape(_q), cjac) for _q in q) for q in qjac)

if not multi_meas:
# Multiple parameters, single measurement
qjac = qml.math.stack(qjac)
qjac = stack(qjac)
if not cjac_is_tuple:
return tdot(qjac, qml.math.stack(cjac))
cjac = stack(cjac)
if has_partitioned_shots:
return tuple(tdot(stack(q), cjac) for q in qjac)
return tdot(qjac, cjac)
if has_partitioned_shots:
return tuple(tuple(tdot(q, c) for c in cjac if c is not None) for q in qjac)
return tuple(tdot(qjac, c) for c in cjac if c is not None)

# Multiple parameters, multiple measurements
if not cjac_is_tuple:
return tuple(tdot(qml.math.stack(q), qml.math.stack(cjac)) for q in qjac)
return tuple(tuple(tdot(qml.math.stack(q), c) for c in cjac if c is not None) for q in qjac)
cjac = stack(cjac)
if has_partitioned_shots:
return tuple(tuple(tdot(stack(_q), cjac) for _q in q) for q in qjac)
return tuple(tdot(stack(q), cjac) for q in qjac)
if has_partitioned_shots:
return tuple(
tuple(tuple(tdot(stack(_q), c) for c in cjac if c is not None) for _q in q)
for q in qjac
)
return tuple(tuple(tdot(stack(q), c) for c in cjac if c is not None) for q in qjac)
5 changes: 5 additions & 0 deletions pennylane/measurements/expval.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def circuit(x):

if isinstance(op, qml.Identity) and len(op.wires) == 0:
# temporary solution to merge https://github.com/PennyLaneAI/pennylane/pull/5106
# allow once we have testing and confidence in qml.expval(I())
raise NotImplementedError(
"Expectation values of qml.Identity() without wires are currently not allowed."
)
Expand Down Expand Up @@ -111,6 +112,8 @@ def process_samples(
shot_range: Tuple[int] = None,
bin_size: int = None,
):
if not self.wires:
return qml.math.squeeze(self.eigvals())
# estimate the ev
op = self.mv if self.mv is not None else self.obs
with qml.queuing.QueuingManager.stop_recording():
Expand All @@ -133,6 +136,8 @@ def process_state(self, state: Sequence[complex], wire_order: Wires):
# arithmetic operators
# we use ``self.wires`` instead of ``self.obs`` because the observable was
# already applied to the state
if not self.wires:
return qml.math.squeeze(self.eigvals())
with qml.queuing.QueuingManager.stop_recording():
prob = qml.probs(wires=self.wires).process_state(state=state, wire_order=wire_order)
# In case of broadcasting, `prob` has two axes and this is a matrix-vector product
Expand Down
7 changes: 2 additions & 5 deletions pennylane/measurements/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,18 +261,15 @@ def _get_num_basis_states(num_wires, device):
base = 2 if cutoff is None else cutoff
return base**num_wires

@qml.QueuingManager.stop_recording()
def diagonalizing_gates(self):
"""Returns the gates that diagonalize the measured wires such that they
are in the eigenbasis of the circuit observables.
Returns:
List[.Operation]: the operations that diagonalize the observables
"""
try:
# pylint: disable=no-member
return self.expand().operations
except qml.operation.DecompositionUndefinedError:
return []
return self.obs.diagonalizing_gates() if self.obs else []

def __eq__(self, other):
return qml.equal(self, other)
Expand Down
16 changes: 15 additions & 1 deletion pennylane/pauli/grouping/group_observables.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,20 @@ def group_observables(observables, coefficients=None, grouping_type="qwc", metho
"The coefficients list must be the same length as the observables list."
)

no_wires_obs = []
wires_obs = []
for ob in observables:
if len(ob.wires) == 0:
no_wires_obs.append(ob)
else:
wires_obs.append(ob)
if not wires_obs:
if coefficients is None:
return [no_wires_obs]
return [no_wires_obs], [coefficients]

pauli_grouping = PauliGroupingStrategy(
observables, grouping_type=grouping_type, graph_colourer=method
wires_obs, grouping_type=grouping_type, graph_colourer=method
)

temp_opmath = not qml.operation.active_new_opmath() and any(
Expand All @@ -243,6 +255,8 @@ def group_observables(observables, coefficients=None, grouping_type="qwc", metho
if temp_opmath:
qml.operation.disable_new_opmath()

partitioned_paulis[0].extend(no_wires_obs)

if coefficients is None:
return partitioned_paulis

Expand Down
2 changes: 1 addition & 1 deletion pennylane/pauli/pauli_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ def _to_sparse_mat(self, wire_order, buffer_size=None):
return matrix

def _to_dense_mat(self, wire_order):
"""Compute the sparse matrix of the Pauli sentence by efficiently adding the Pauli words
"""Compute the dense matrix of the Pauli sentence by efficiently adding the Pauli words
that it is composed of. See pauli_sparse_matrices.md for the technical details."""
pauli_words = list(self) # Ensure consistent ordering

Expand Down
4 changes: 3 additions & 1 deletion pennylane/templates/subroutines/fable.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class FABLE(Operation):
Args:
input_matrix (tensor_like): a :math:`(2^n \times 2^n)` matrix to be encoded,
where :math:`n` is the number of wires used
where :math:`n` is an integer
wires (Iterable[int, str], Wires): the wires the operation acts on. The number of wires can
be computed as :math:`(2 \times n + 1)`.
tol (float): rotation gates that have an angle value smaller than this tolerance are removed
id (str or None): string representing the operation (optional)
Expand Down
6 changes: 4 additions & 2 deletions pennylane/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ def _flatten(x):
elif isinstance(x, qml.wires.Wires):
# Reursive calls to flatten `Wires` will cause infinite recursion (`Wires` atoms are `Wires`).
# Since Wires are always flat, just yield.
for item in x:
yield item
yield from x
elif isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
for item in x:
yield from _flatten(item)
Expand Down Expand Up @@ -168,6 +167,9 @@ def expand_vector(vector, original_wires, expanded_wires):
Returns:
array: :math:`2^m` vector where m = len(expanded_wires).
"""
if len(original_wires) == 0:
val = qml.math.squeeze(vector)
return val * qml.math.ones(2 ** len(expanded_wires))
if isinstance(expanded_wires, numbers.Integral):
expanded_wires = list(range(expanded_wires))

Expand Down
9 changes: 9 additions & 0 deletions tests/devices/qubit/test_measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ def qnode(t1, t2):
t1, t2 = 0.5, 1.0
assert qml.math.allclose(qnode(t1, t2), jax.jit(qnode)(t1, t2))

def test_measure_identity_no_wires(self):
"""Test that measure can handle the expectation value of identity on no wires."""
state = np.random.random([2, 2, 2])
out = measure(qml.measurements.ExpectationMP(qml.I()), state)
assert qml.math.allclose(out, 1.0)

out2 = measure(qml.measurements.ExpectationMP(2 * qml.I()), state)
assert qml.math.allclose(out2, 2)


class TestBroadcasting:
"""Test that measurements work when the state has a batch dim"""
Expand Down
Loading

0 comments on commit fe08066

Please sign in to comment.