Skip to content

Commit 0b18c7f

Browse files
dwierichsastralcai
andauthored
Use TemporaryAND in MulticontrolledX decompositions (#8172)
**Context:** There are multiple decompositions of `MulticontrolledX` that use auxiliary wires. **Description of the Change:** If the auxiliary wires are of `work_wire_type="zeroed"`, we may replace `Toffoli` gates acting on them by `TemporaryAND` gates. **Benefits:** Cheaper decompositions **Possible Drawbacks:** N/A **Related GitHub Issues:** [sc-95311] --------- Co-authored-by: Astral Cai <astral.cai@xanadu.ai>
1 parent 4d21739 commit 0b18c7f

File tree

4 files changed

+127
-56
lines changed

4 files changed

+127
-56
lines changed

‎doc/releases/changelog-dev.md‎

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
<h3>New features since last release</h3>
55

6-
* The `qml.sample` function can now receive an optional `dtype` parameter
6+
* The `qml.sample` function can now receive an optional `dtype` parameter
77
which sets the type and precision of the samples returned by this measurement process.
88
[(#8189)](https://github.com/PennyLaneAI/pennylane/pull/8189)
99
[(#8271)](https://github.com/PennyLaneAI/pennylane/pull/8271)
1010

1111
* The Resource estimation toolkit was upgraded and has migrated from
1212
:mod:`~.labs` to PennyLane as the :mod:`~.estimator` module.
13-
13+
1414
* The `qml.estimator.WireResourceManager`, `qml.estimator.Allocate`, and `qml.estimator.Deallocate`
1515
classes were added to track auxiliary wires for resource estimation.
1616
[(#8203)](https://github.com/PennyLaneAI/pennylane/pull/8203)
@@ -131,14 +131,18 @@
131131

132132
<h3>Improvements 🛠</h3>
133133

134+
* Various decompositions of :class:`~.MultiControlledX` now utilize :class:`~.TemporaryAND` in
135+
place of :class:`~.Toffoli` gates, leading to cheaper decompositions.
136+
[(#8172)](https://github.com/PennyLaneAI/pennylane/pull/8172)
137+
134138
* `qml.to_openqasm` now supports mid circuit measurements and conditionals of unprocessed measurement values.
135139
[(#8210)](https://github.com/PennyLaneAI/pennylane/pull/8210)
136140

137141
* The `QNode` primitive in the experimental program capture now captures the unprocessed `ExecutionConfig`, instead of
138142
one processed by the device.
139143
[(#8258)](https://github.com/PennyLaneAI/pennylane/pull/8258)
140144

141-
* The function :func:`qml.clifford_t_decomposition` with `method="gridsynth"` are now compatible
145+
* The function :func:`qml.clifford_t_decomposition` with `method="gridsynth"` are now compatible
142146
with quantum just-in-time compilation via the `@qml.qjit` decorator.
143147
[(#7711)](https://github.com/PennyLaneAI/pennylane/pull/7711)
144148

@@ -418,7 +422,7 @@
418422

419423
<h4>Other improvements</h4>
420424

421-
* Two new `draw` and `generate_mlir_graph` functions have been introduced in the `qml.compiler.python_compiler.visualization` module
425+
* Two new `draw` and `generate_mlir_graph` functions have been introduced in the `qml.compiler.python_compiler.visualization` module
422426
to visualize circuits with the new unified compiler framework when xDSL and/or Catalyst compilation passes are applied.
423427
[(#8040)](https://github.com/PennyLaneAI/pennylane/pull/8040)
424428
[(#8091)](https://github.com/PennyLaneAI/pennylane/pull/8091)
@@ -604,10 +608,10 @@
604608
* Added a new `ResourceConfig` class that helps track the configuration for errors, precisions and custom decompositions for the resource estimation pipeline.
605609
[(#8195)](https://github.com/PennyLaneAI/pennylane/pull/8195)
606610

607-
* Renamed `estimate_resources` to `estimate` for concision.
611+
* Renamed `estimate_resources` to `estimate` for concision.
608612
[(#8232)](https://github.com/PennyLaneAI/pennylane/pull/8232)
609613

610-
* Added an internal `dequeue()` method to the `ResourceOperator` class to simplify the
614+
* Added an internal `dequeue()` method to the `ResourceOperator` class to simplify the
611615
instantiation of resource operators which require resource operators as input.
612616
[(#7974)](https://github.com/PennyLaneAI/pennylane/pull/7974)
613617

@@ -625,7 +629,7 @@
625629
[(#8036)](https://github.com/PennyLaneAI/pennylane/pull/8036)
626630
[(#8084)](https://github.com/PennyLaneAI/pennylane/pull/8084)
627631
[(#8113)](https://github.com/PennyLaneAI/pennylane/pull/8113)
628-
632+
629633
* Added more templates with state of the art resource estimates. Users can now use the `ResourceQPE`,
630634
`ResourceControlledSequence`, and `ResourceIterativeQPE` templates with the resource estimation tool.
631635
[(#8053)](https://github.com/PennyLaneAI/pennylane/pull/8053)
@@ -658,9 +662,9 @@
658662

659663
[(#8223)](https://github.com/PennyLaneAI/pennylane/pull/8223)
660664

661-
* :class:`~.PrepSelPrep` has been made more reliable by deriving the attributes ``coeffs`` and ``ops`` from the property ``lcu`` instead of storing
665+
* :class:`~.PrepSelPrep` has been made more reliable by deriving the attributes ``coeffs`` and ``ops`` from the property ``lcu`` instead of storing
662666
them independently. In addition, it is now is more consistent with other PennyLane operators, dequeuing its
663-
input ``lcu``.
667+
input ``lcu``.
664668
[(#8169)](https://github.com/PennyLaneAI/pennylane/pull/8169)
665669

666670
* `MidMeasureMP` now inherits from `Operator` instead of `MeasurementProcess`.
@@ -949,7 +953,7 @@
949953
[(#8159)](https://github.com/PennyLaneAI/pennylane/pull/8159)
950954
[(#8160)](https://github.com/PennyLaneAI/pennylane/pull/8160)
951955

952-
* A `diagonalize_mcms` option has been added to the `ftqc.decomposition.convert_to_mbqc_formalism` tape transform that, when set, arbitrary-basis mid-circuit measurements are mapped into corresponding diagonalizing gates and Z-basis mid-circuit measurements.
956+
* A `diagonalize_mcms` option has been added to the `ftqc.decomposition.convert_to_mbqc_formalism` tape transform that, when set, arbitrary-basis mid-circuit measurements are mapped into corresponding diagonalizing gates and Z-basis mid-circuit measurements.
953957
[(#8105)](https://github.com/PennyLaneAI/pennylane/pull/8105)
954958

955959
* The `autograph` keyword argument has been removed from the `QNode` constructor.
@@ -1086,7 +1090,7 @@
10861090

10871091
* Add nightly RC builds script to `.github/workflows`.
10881092
[(#8148)](https://github.com/PennyLaneAI/pennylane/pull/8148)
1089-
1093+
10901094
<h3>Documentation 📝</h3>
10911095

10921096
* The "Simplifying Operators" section in the :doc:`Compiling circuits </introduction/compiling_circuits>` page was pushed further down the page to show more relevant sections first.
@@ -1125,7 +1129,7 @@
11251129
Trimmed the outdated part of discussion regarding different choices of `alpha`.
11261130
[(#8100)](https://github.com/PennyLaneAI/pennylane/pull/8100)
11271131

1128-
* A warning was added to the :doc:`interfaces documentation </introduction/interfaces>` under the Pytorch section saying that all Pytorch floating-point inputs are promoted
1132+
* A warning was added to the :doc:`interfaces documentation </introduction/interfaces>` under the Pytorch section saying that all Pytorch floating-point inputs are promoted
11291133
to `torch.float64`.
11301134
[(#8124)](https://github.com/PennyLaneAI/pennylane/pull/8124)
11311135

@@ -1138,7 +1142,7 @@
11381142
and :func:`~pennylane.ctrl`.
11391143
[(#8215)](https://github.com/PennyLaneAI/pennylane/pull/8215)
11401144

1141-
* Parameter batching now works for Z-basis gates when executing with `default.mixed`.
1145+
* Parameter batching now works for Z-basis gates when executing with `default.mixed`.
11421146
[(#8251)](https://github.com/PennyLaneAI/pennylane/pull/8251)
11431147

11441148
* `qml.ctrl(qml.Barrier(), control_wires)` now just returns the original Barrier operation, but placed
@@ -1159,7 +1163,7 @@
11591163
that broke the decompositions if the target ``ops`` of the ``Select`` operator were parametrized.
11601164
This enables the new decomposition system with ``Select`` of parametrized target ``ops``.
11611165
[(#8186)](https://github.com/PennyLaneAI/pennylane/pull/8186)
1162-
1166+
11631167
* `Exp` and `Evolution` now have improved decompositions, allowing them to handle more situations
11641168
more robustly. In particular, the generator is simplified prior to decomposition. Now more
11651169
time evolution ops can be supported on devices that do not natively support them.

‎pennylane/ops/functions/assert_valid.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,15 @@ def _test_decomposition_rule(op, rule: DecompositionRule, heuristic_resources=Fa
168168
non_zero_gate_counts == actual_gate_counts
169169
), f"{non_zero_gate_counts} != {actual_gate_counts}"
170170

171-
# Add projector to the additional wires (work wires) on the tape
172-
work_wires = tape.wires - op.wires
173-
all_wires = op.wires + work_wires
174-
if work_wires:
175-
op = op @ qml.Projector([0] * len(work_wires), wires=work_wires)
176-
tape.operations.insert(0, qml.Projector([0] * len(work_wires), wires=work_wires))
177-
178171
# Tests that the decomposition produces the same matrix
179172
if op.has_matrix:
173+
# Add projector to the additional wires (work wires) on the tape
174+
work_wires = tape.wires - op.wires
175+
all_wires = op.wires + work_wires
176+
if work_wires:
177+
op = op @ qml.Projector([0] * len(work_wires), wires=work_wires)
178+
tape.operations.insert(0, qml.Projector([0] * len(work_wires), wires=work_wires))
179+
180180
op_matrix = op.matrix(wire_order=all_wires)
181181
decomp_matrix = qml.matrix(tape, wire_order=all_wires)
182182
assert qml.math.allclose(

‎pennylane/ops/op_math/decompositions/controlled_decompositions.py‎

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import numpy as np
2121

22+
import pennylane as qml
2223
from pennylane import allocation, control_flow, math, ops, queuing
2324
from pennylane.decomposition import (
2425
adjoint_resource_rep,
@@ -417,12 +418,13 @@ def _mcx_many_workers_condition(num_control_wires, num_work_wires, **__):
417418

418419

419420
def _mcx_many_workers_resource(num_control_wires, work_wire_type, **__):
421+
422+
if work_wire_type == "borrowed":
423+
return {ops.Toffoli: 4 * (num_control_wires - 2)}
420424
return {
421-
ops.Toffoli: (
422-
4 * (num_control_wires - 2)
423-
if work_wire_type == "borrowed"
424-
else 2 * (num_control_wires - 2) + 1
425-
)
425+
qml.TemporaryAND: num_control_wires - 2,
426+
adjoint_resource_rep(qml.TemporaryAND): num_control_wires - 2,
427+
ops.Toffoli: 1,
426428
}
427429

428430

@@ -433,27 +435,32 @@ def _mcx_many_workers(wires, work_wires, work_wire_type, **__):
433435
"""Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.2 of
434436
https://arxiv.org/abs/quant-ph/9503016, which requires a suitably large register of
435437
work wires"""
436-
437438
target_wire, control_wires = wires[-1], wires[:-1]
438439
work_wires = work_wires[: len(control_wires) - 2]
439440

441+
if work_wire_type == "borrowed":
442+
up_gate = down_gate = ops.Toffoli
443+
else:
444+
down_gate = qml.TemporaryAND
445+
up_gate = ops.adjoint(qml.TemporaryAND)
446+
440447
@control_flow.for_loop(1, len(work_wires), 1)
441448
def loop_up(i):
442-
ops.Toffoli(wires=[control_wires[i], work_wires[i], work_wires[i - 1]])
449+
up_gate(wires=[control_wires[i], work_wires[i], work_wires[i - 1]])
443450

444451
@control_flow.for_loop(len(work_wires) - 1, 0, -1)
445452
def loop_down(i):
446-
ops.Toffoli(wires=[control_wires[i], work_wires[i], work_wires[i - 1]])
453+
down_gate(wires=[control_wires[i], work_wires[i], work_wires[i - 1]])
447454

448455
if work_wire_type == "borrowed":
449456
ops.Toffoli(wires=[control_wires[0], work_wires[0], target_wire])
450457
loop_up()
451458

452-
ops.Toffoli(wires=[control_wires[-1], control_wires[-2], work_wires[-1]])
459+
down_gate(wires=[control_wires[-1], control_wires[-2], work_wires[-1]])
453460
loop_down()
454461
ops.Toffoli(wires=[control_wires[0], work_wires[0], target_wire])
455462
loop_up()
456-
ops.Toffoli(wires=[control_wires[-1], control_wires[-2], work_wires[-1]])
463+
up_gate(wires=[control_wires[-1], control_wires[-2], work_wires[-1]])
457464

458465
if work_wire_type == "borrowed":
459466
loop_down()
@@ -497,16 +504,27 @@ def _mcx_many_borrowed_workers(wires, **kwargs):
497504

498505

499506
def _mcx_two_workers_condition(num_control_wires, num_work_wires, **__):
500-
return num_control_wires > 2 and num_work_wires >= 2
507+
return num_control_wires > 2 and (
508+
num_work_wires >= 2 or (num_work_wires == 1 and num_control_wires < 6)
509+
)
501510

502511

503512
def _mcx_two_workers_resource(num_control_wires, work_wire_type, **__):
513+
514+
is_small_mcx = num_control_wires < 6
515+
504516
if work_wire_type == "zeroed":
505517
n_ccx = 2 * num_control_wires - 3
506-
return {ops.Toffoli: n_ccx, ops.X: n_ccx - 3 if num_control_wires < 6 else n_ccx - 5}
518+
n_temporary_ccx_pairs = 2 - is_small_mcx
519+
return {
520+
ops.Toffoli: n_ccx - 2 * n_temporary_ccx_pairs,
521+
ops.X: n_ccx - 3 if is_small_mcx else n_ccx - 5,
522+
qml.TemporaryAND: n_temporary_ccx_pairs,
523+
adjoint_resource_rep(qml.TemporaryAND): n_temporary_ccx_pairs,
524+
}
507525
# Otherwise, we assume the work wires are borrowed
508526
n_ccx = 4 * num_control_wires - 8
509-
return {ops.Toffoli: n_ccx, ops.X: n_ccx - 4 if num_control_wires < 6 else n_ccx - 8}
527+
return {ops.Toffoli: n_ccx, ops.X: n_ccx - 4 if is_small_mcx else n_ccx - 8}
510528

511529

512530
@register_condition(_mcx_two_workers_condition)
@@ -523,30 +541,48 @@ def _mcx_two_workers(wires, work_wires, work_wire_type, **__):
523541
`arXiv:2407.17966 <https://arxiv.org/abs/2407.17966>`__
524542
525543
"""
526-
544+
# Unpack work wires for readability. There might just be one of them if it is a "small" MCX
545+
# (less than 6 controls)
546+
work0, *work1 = work_wires
527547
# First use the work wire to prepare the first two control wires as conditionally clean.
528-
ops.Toffoli([wires[0], wires[1], work_wires[0]])
548+
left_elbow = ops.Toffoli if work_wire_type == "borrowed" else qml.TemporaryAND
549+
left_elbow([wires[0], wires[1], work0])
550+
529551
middle_ctrl_indices = _build_log_n_depth_ccx_ladder(wires[:-1])
530552

531-
# Apply the MCX in the middle
553+
# Apply the MCX in the middle. This is just a single Toffoli without work wires for "small" MCX
532554
if len(middle_ctrl_indices) == 1:
533-
ops.Toffoli([work_wires[0], wires[middle_ctrl_indices[0]], wires[-1]])
555+
ops.Toffoli([work0, wires[middle_ctrl_indices[0]], wires[-1]])
534556
else:
535557
middle_wires = [wires[i] for i in middle_ctrl_indices]
536-
_mcx_one_worker(work_wires[:1] + middle_wires + wires[-1:], work_wires[1:])
558+
# No toggle detection needed for the inner MCX decomposition, even for borrowed work wires
559+
_mcx_one_worker(
560+
[work0] + middle_wires + wires[-1:],
561+
work1,
562+
work_wire_type=work_wire_type,
563+
_skip_toggle_detection=True,
564+
)
537565

538566
# Uncompute the first ladder
539567
ops.adjoint(_build_log_n_depth_ccx_ladder, lazy=False)(wires[:-1])
540-
ops.Toffoli([wires[0], wires[1], work_wires[0]])
568+
569+
right_elbow = ops.Toffoli if work_wire_type == "borrowed" else qml.adjoint(qml.TemporaryAND)
570+
right_elbow([wires[0], wires[1], work0])
541571

542572
if work_wire_type == "borrowed":
543-
# Perform toggle-detection of the work wire is borrowed
573+
# Perform toggle-detection if the work wire is borrowed
544574
middle_ctrl_indices = _build_log_n_depth_ccx_ladder(wires[:-1])
545575
if len(middle_ctrl_indices) == 1:
546-
ops.Toffoli([work_wires[0], wires[middle_ctrl_indices[0]], wires[-1]])
576+
ops.Toffoli([work0, wires[middle_ctrl_indices[0]], wires[-1]])
547577
else:
548578
middle_wires = [wires[i] for i in middle_ctrl_indices]
549-
_mcx_one_worker(work_wires[:1] + middle_wires + wires[-1:], work_wires[1:])
579+
_mcx_one_worker(
580+
[work0] + middle_wires + wires[-1:],
581+
work1,
582+
work_wire_type=work_wire_type,
583+
_skip_toggle_detection=True,
584+
)
585+
550586
ops.adjoint(_build_log_n_depth_ccx_ladder, lazy=False)(wires[:-1])
551587

552588

@@ -557,10 +593,11 @@ def _mcx_two_workers(wires, work_wires, work_wire_type, **__):
557593
@register_condition(lambda num_control_wires, **_: num_control_wires > 2)
558594
@register_resources(
559595
lambda num_control_wires, **_: _mcx_two_workers_resource(num_control_wires, "zeroed"),
560-
work_wires={"zeroed": 2},
596+
work_wires=lambda num_control_wires, **_: {"zeroed": 1 + (num_control_wires >= 6)},
561597
)
562598
def _mcx_two_zeroed_workers(wires, **kwargs):
563-
with allocation.allocate(2, state="zero", restored=True) as work_wires:
599+
is_small_mcx = (len(wires) - 1) < 6
600+
with allocation.allocate(2 - is_small_mcx, state="zero", restored=True) as work_wires:
564601
kwargs.update({"work_wires": work_wires, "work_wire_type": "zeroed"})
565602
_mcx_two_workers(wires, **kwargs)
566603

@@ -572,10 +609,11 @@ def _mcx_two_zeroed_workers(wires, **kwargs):
572609
@register_condition(lambda num_control_wires, **_: num_control_wires > 2)
573610
@register_resources(
574611
lambda num_control_wires, **_: _mcx_two_workers_resource(num_control_wires, "borrowed"),
575-
work_wires={"borrowed": 2},
612+
work_wires=lambda num_control_wires, **_: {"borrowed": 2 - (num_control_wires < 6)},
576613
)
577614
def _mcx_two_borrowed_workers(wires, **kwargs):
578-
with allocation.allocate(2, state="any", restored=True) as work_wires:
615+
is_small_mcx = (len(wires) - 1) < 6
616+
with allocation.allocate(2 - is_small_mcx, state="any", restored=True) as work_wires:
579617
kwargs.update({"work_wires": work_wires, "work_wire_type": "borrowed"})
580618
_mcx_two_workers(wires, **kwargs)
581619

@@ -589,37 +627,57 @@ def _mcx_one_worker_condition(num_control_wires, num_work_wires, **__):
589627

590628
def _mcx_one_worker_resource(num_control_wires, work_wire_type, **__):
591629
if work_wire_type == "zeroed":
592-
n_ccx = 2 * num_control_wires - 3
593-
return {ops.Toffoli: n_ccx, ops.X: n_ccx - 3}
630+
n_ccx = 2 * num_control_wires - 5
631+
return {
632+
ops.Toffoli: n_ccx,
633+
qml.TemporaryAND: 1,
634+
adjoint_resource_rep(qml.TemporaryAND): 1,
635+
ops.X: n_ccx - 1,
636+
}
594637
# Otherwise, we assume the work wire is borrowed
595638
n_ccx = 4 * num_control_wires - 8
596639
return {ops.Toffoli: n_ccx, ops.X: n_ccx - 4}
597640

598641

599642
@register_condition(_mcx_one_worker_condition)
600643
@register_resources(_mcx_one_worker_resource)
601-
def _mcx_one_worker(wires, work_wires, work_wire_type="zeroed", **__):
644+
def _mcx_one_worker(wires, work_wires, work_wire_type="zeroed", _skip_toggle_detection=False, **__):
602645
r"""
603646
Synthesise a multi-controlled X gate with :math:`k` controls using :math:`1` auxiliary qubit. It
604647
produces a circuit with :math:`2k-3` Toffoli gates and depth :math:`O(k)` if the auxiliary is zeroed
605648
and :math:`4k-3` Toffoli gates and depth :math:`O(k)` if the auxiliary is borrowed as described in
606649
Sec. 5.1 of [1].
607650
651+
.. note::
652+
653+
The keyword argument ``_skip_toggle_detection`` is only supposed to be used when utilizing
654+
``_mcx_one_worker`` as a subroutine within a decomposition rule, but not when using
655+
it as a decomposition rule itself. This is because ``_mcx_one_worker_resource`` does not
656+
support/take into account this keyword argument.
657+
608658
References:
609659
1. Khattar and Gidney, Rise of conditionally clean ancillae for optimizing quantum circuits
610660
`arXiv:2407.17966 <https://arxiv.org/abs/2407.17966>`__
611661
612662
"""
613-
614-
ops.Toffoli([wires[0], wires[1], work_wires[0]])
663+
if work_wire_type == "borrowed":
664+
ops.Toffoli([wires[0], wires[1], work_wires[0]])
665+
else:
666+
_skip_toggle_detection = True
667+
qml.TemporaryAND([wires[0], wires[1], work_wires[0]])
615668

616669
final_ctrl_index = _build_linear_depth_ladder(wires[:-1])
617670
ops.Toffoli([work_wires[0], wires[final_ctrl_index], wires[-1]])
618671
ops.adjoint(_build_linear_depth_ladder, lazy=False)(wires[:-1])
619-
ops.Toffoli([wires[0], wires[1], work_wires[0]])
620672

621673
if work_wire_type == "borrowed":
622-
# Perform toggle-detection of the work wire is borrowed
674+
ops.Toffoli([wires[0], wires[1], work_wires[0]])
675+
else:
676+
ops.adjoint(qml.TemporaryAND([wires[0], wires[1], work_wires[0]]))
677+
678+
if not _skip_toggle_detection:
679+
# Perform toggle-detection unless skipped explicitly. By default, toggle detection
680+
# is skipped for `work_wire_type="zeroed"` but not for `work_wire_type="borrowed"`.
623681
_build_linear_depth_ladder(wires[:-1])
624682
ops.Toffoli([work_wires[0], wires[final_ctrl_index], wires[-1]])
625683
ops.adjoint(_build_linear_depth_ladder, lazy=False)(wires[:-1])

0 commit comments

Comments
 (0)