Defining Operations including qml.ctrl raises problems

Dear Pennylane Team,

I am currently trying to implement a quantum circuit composed of various Operations and Operations of Operations. However, I face trouble as soon as I want to include the command qml.ctrl() in any of my Operator-definitions.

Let me explain the issue with a minimal example:
If I define the Operator like this:

class controlled_H_gate(Operation):
    num_wires = AnyWires
    grad_method = "A"

    def __init__(self,wires, id=None):
        super().__init__(wires=wires, id=id) # calls the init of the operator class

    @staticmethod
    def compute_decomposition(wires):
        n_wires_1 = wires[0:1]
        n_aux_1 = wires[1:2]
        return qml.ctrl(qml.H, control = n_aux_1, control_values=[0])(wires=n_wires_1[0:1])

and want to call it within a quantum circuit

n = 2
dev_test = qml.device("default.qubit", wires=n)

@qml.qnode(dev_test)
def test_circuit():
    w = dev_test.wires
    controlled_H_gate(wires=w)
    return qml.state()

it will run without an error but the operator is apperently not applied as the output-state and a verification with

fig, ax = qml.draw_mpl(test_circuit, level=“device”)()
plt.show()

shows.
If I modify my circuit, by returning a list (as I would usually do), i.e.:

class controlled_H_gate(Operation):
    num_wires = AnyWires
    grad_method = "A"

    def __init__(self,wires, id=None):
        super().__init__(wires=wires, id=id) # calls the init of the operator class

    @staticmethod
    def compute_decomposition(wires):
        n_wires_1 = wires[0:1]
        n_aux_1 = wires[1:2]
        return [qml.ctrl(qml.H, control = n_aux_1, control_values=[0])(wires=n_wires_1[0:1])]

and then try ot run the quantum circuit I get the error message:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[110], line 10
      7     controlled_H_gate(wires=w)
      8     return qml.state()
---> 10 print( test_circuit())
     11 fig, ax = qml.draw_mpl(test_circuit, level="device")() #, level="device"
     12 plt.show()

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\workflow\qnode.py:882, in QNode.__call__(self, *args, **kwargs)
    879     from ._capture_qnode import capture_qnode  # pylint: disable=import-outside-toplevel
    881     return capture_qnode(self, *args, **kwargs)
--> 882 return self._impl_call(*args, **kwargs)

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\workflow\qnode.py:855, in QNode._impl_call(self, *args, **kwargs)
    852 # Calculate the classical jacobians if necessary
    853 self._transform_program.set_classical_component(self, args, kwargs)
--> 855 res = qml.execute(
    856     (tape,),
    857     device=self.device,
    858     diff_method=self.diff_method,
    859     interface=self.interface,
    860     transform_program=self._transform_program,
    861     gradient_kwargs=self.gradient_kwargs,
    862     **self.execute_kwargs,
    863 )
    864 res = res[0]
    866 # convert result to the interface in case the qfunc has no parameters

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\workflow\execution.py:239, in execute(tapes, device, diff_method, interface, transform_program, grad_on_execution, cache, cachesize, max_diff, device_vjp, postselect_mode, mcm_method, gradient_kwargs, mcm_config, config, inner_transform)
    234 transform_program, inner_transform = _setup_transform_program(
    235     transform_program, device, config, cache, cachesize
    236 )
    238 #### Executing the configured setup #####
--> 239 tapes, post_processing = transform_program(tapes)
    241 if transform_program.is_informative:
    242     return post_processing(tapes)

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\transforms\core\transform_program.py:643, in TransformProgram.__call__(self, *args, **kwargs)
    641 if type(args[0]).__name__ == "Jaxpr":
    642     return self.__call_jaxpr(*args, **kwargs)
--> 643 return self.__call_tapes(*args, **kwargs)

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\transforms\core\transform_program.py:580, in TransformProgram.__call_tapes(self, tapes)
    578 if argnums is not None:
    579     tape.trainable_params = argnums[j]
--> 580 new_tapes, fn = transform(tape, *targs, **tkwargs)
    581 execution_tapes.extend(new_tapes)
    583 fns.append(fn)

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\devices\preprocess.py:401, in decompose(tape, stopping_condition, stopping_condition_shots, skip_initial_state_prep, decomposer, name, error)
    398     return (tape,), null_postprocessing
    399 try:
--> 401     new_ops = [
    402         final_op
    403         for op in tape.operations[len(prep_op) :]
    404         for final_op in _operator_decomposition_gen(
    405             op,
    406             stopping_condition,
    407             decomposer=decomposer,
    408             name=name,
    409             error=error,
    410         )
    411     ]
    412 except RecursionError as e:
    413     raise error(
    414         "Reached recursion limit trying to decompose operations. "
    415         "Operator decomposition may have entered an infinite loop."
    416     ) from e

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\devices\preprocess.py:401, in <listcomp>(.0)
    398     return (tape,), null_postprocessing
    399 try:
--> 401     new_ops = [
    402         final_op
    403         for op in tape.operations[len(prep_op) :]
    404         for final_op in _operator_decomposition_gen(
    405             op,
    406             stopping_condition,
    407             decomposer=decomposer,
    408             name=name,
    409             error=error,
    410         )
    411     ]
    412 except RecursionError as e:
    413     raise error(
    414         "Reached recursion limit trying to decompose operations. "
    415         "Operator decomposition may have entered an infinite loop."
    416     ) from e

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\devices\preprocess.py:72, in _operator_decomposition_gen(op, acceptance_function, decomposer, max_expansion, current_depth, name, error)
     67     raise error(
     68         f"Operator {op} not supported with {name} and does not provide a decomposition."
     69     ) from e
     71 for sub_op in decomp:
---> 72     yield from _operator_decomposition_gen(
     73         sub_op,
     74         acceptance_function,
     75         decomposer=decomposer,
     76         max_expansion=max_expansion,
     77         current_depth=current_depth,
     78         name=name,
     79         error=error,
     80     )

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\devices\preprocess.py:60, in _operator_decomposition_gen(op, acceptance_function, decomposer, max_expansion, current_depth, name, error)
     58 if max_expansion is not None and max_expansion <= current_depth:
     59     max_depth_reached = True
---> 60 if acceptance_function(op) or max_depth_reached:
     61     yield op
     62 else:

File ~\Desktop\TrainingProject\jax-env\lib\site-packages\pennylane\devices\default_qubit.py:61, in stopping_condition(op)
     59 def stopping_condition(op: qml.operation.Operator) -> bool:
     60     """Specify whether or not an Operator object is supported by the device."""
---> 61     if op.name == "QFT" and len(op.wires) >= 6:
     62         return False
     63     if op.name == "GroverOperator" and len(op.wires) >= 13:

AttributeError: 'list' object has no attribute 'name'

If I instead remove the ctrl, everything works as expected.
I am well aware, that for this simple example I do not need to include the control in the operator itself, but in my more complicated-quite-nested circuit it would be very helpful.

Is there a way how to include ctrl in an Operation-definition correctly?

Many thanks and best regards,
Pia

Hi @Pia ,

Thank you for bringing this up. I think you may have uncovered a hidden bug. I’ve forwarded it to our team, so I hope we can find a workaround that we can share. I’ll let you know what we find next week.

1 Like

Thanks for asking this @Pia ,

We have two ways of creating controlled operations in pennylane.

  1. ctrl(op, control_wires): which takes in an operation and returns a controlled operation
  2. ctrl(qfunc, control_wires)(*args, **kwargs), which takes a quantum function and returns a quantum function that returns nothing.

The quantum function just “queues” operations, instead of actually returning them. But for decompositions, we take the actual list output of the method and don’t care about the queueing behaviour.

If you switch to using the first method, where an operator goes and and an operator goes out, you should be able to get the decomposition to work.

op = qml.H(wires=n_wires_1[0])
return return [qml.ctrl(op, control = n_aux_1, control_values=[0])]

I see :slight_smile: thank you!

That works! Thanks a lot :slight_smile: