'NoneType' object is not iterable error when working with mid-circuit measurements or qml.ctrl

Hi,

I have the following decomposition within a “Pooling” gate(subclass of qml.operation.Operation):

    def decomposition(self):
        '''
        Decomposes pool gate, tracing out a wire or measuring and applying a conditional transformation.
        Wire 1 is pooled into wire 0.
        '''
        params = self.parameters[0]
        wires = self.wires
        if self.pool_type == "trace":
            return [
                qml.CRZ(params[0], wires=[wires[1], wires[0]]),
                qml.PauliX(wires=wires[1]),
                qml.CRX(params[1], wires=[wires[1], wires[0]])
            ]
        if self.pool_type == "measure":
            return qml.cond(qml.measure(wires=wires[1]), qml.U3)(params[0], params[1], params[2], wires=wires[0])

Whenever I try to run a circuit using the “measure” version, I get the error in the title. Upon closer inspection, it’s similar to some trouble I had earlier with qml.ctrl, where the error is from PennyLane internal preprocess.py, in the following code:

def _operator_decomposition_gen(
    op: qml.operation.Operator,
    acceptance_function: Callable[[qml.operation.Operator], bool],
    decomposer: Callable[[qml.operation.Operator], Sequence[qml.operation.Operator]],
    max_expansion: Optional[int] = None,
    current_depth=0,
    name: str = "device",
) -> Generator[qml.operation.Operator, None, None]:
    """A generator that yields the next operation that is accepted."""
    max_depth_reached = False
    if max_expansion is not None and max_expansion <= current_depth:
        max_depth_reached = True
    if acceptance_function(op) or max_depth_reached:
        yield op
    else:
        try:
            decomp = decomposer(op)
            current_depth += 1
        except qml.operation.DecompositionUndefinedError as e:
            raise DeviceError(
                f"Operator {op} not supported on {name} and does not provide a decomposition."
            ) from e

        for sub_op in decomp:
            yield from _operator_decomposition_gen(
                sub_op,
                acceptance_function,
                decomposer=decomposer,
                max_expansion=max_expansion,
                current_depth=current_depth,
                name=name,
            )

Where op = Pooling (measure)(Array([6.16770658, 2.81488335, 5.38861346], dtype=float64), wires=[0, 1]) but decomp = None, which means the error results from the for sub_op in decomp line. A similar thing had occured with using qml.ctrl, as the ctrled gate had no decomp and I scrapped that whole idea. Is there a way around this? Do i have to add something to my class definition?

Hey @somearthling, interesting question!

Using qml.cond as a decomposition for a custom operator won’t work because it returns a function, not a PennyLane operator (see here: qml.cond — PennyLane 0.35.1 documentation).

There are a couple ways around this:

  1. If you aren’t reusing the measured wire, you can replace the classical controls with quantum controls :slight_smile:. This is the easier way around, but there are limitations.
  2. You can use qml.ops.Conditional instead, but it gets a little messy. Here’s a toy example:
import pennylane as qml
from functools import partial

class MCMOperator(qml.operation.Operator):

    def decomposition(self):
        m0 = qml.measure(self.wires)
        return [m0.measurements[0], qml.ops.Conditional(m0, qml.PauliX(self.wires))]

@partial(qml.devices.preprocess.decompose, stopping_condition = lambda obj: not isinstance(obj, MCMOperator))
@qml.qnode(qml.device('default.qubit'))
def circuit():
    qml.Hadamard(0)
    MCMOperator(wires=0)
    qml.Hadamard(0)
    return qml.expval(qml.PauliZ(0))

print(circuit())
print(qml.draw(circuit)())
0.0
0: ──H──┤↗├──X──H─┤  <Z>
         ╚═══╝       

I’ll explain the second option and why it works. The function qml.devices.preprocess.decompose (see here) tells pennylane how much to decompose operations until a stopping condition is met. In this case, if PennyLane sees that an operator in your QNode is not an instance of the new operator we made (i.e., the stopping condition is False), it will be decomposed as normal. If PennyLane sees an instance of the new operator in your QNode, then it stops decomposing past what’s in the decomposition method.

For your specific application, I think option #2 is best even though it might be a bit tricky to understand. Let me know if that helps!