Batching custom templates

I am trying to make an encoder that embeds classical data into relative phases of computational basis states (Phase Encoding). I finished coding the circuitry, I just do not know how to format it so that it will work with batches of input. I tried returning a list of tapes when implementing it into my broader machine learning model, but this was unsuccessful. Is there a recommended way to go about this? I am exporting the circuit as a TorchLayer in my actual code.

import pennylane as qml
from pennylane.operation import Operation, AnyWires
import torch
import math
import numpy as np

class PhaseEncoder(Operation):
    num_params = 0
    num_wires = AnyWires
    par_domain = None

    def __init__(self, wires=None):
        super().__init__(wires=wires)
        self.inputs = None

    # Not sure of the best way to pass non-trainable params, so this was what I came up with
    def set_inputs(self, inputs, max, num_wires):
        inputs = inputs*2*np.pi/max
        self.inputs = inputs.detach().cpu().numpy()
        self.num_wires = num_wires

    def expand(self):
        if self.inputs is None:
            raise ValueError("No inputs given to phase encoder")

        
        with qml.tape.QuantumTape() as tape:
            # Phase shift will target last qubit so only two X gates are required
            first_target_wire_flip = False
            second_target_wire_flip = False
            for i in range(self.num_wires):
                qml.Hadamard(wires=i)
            # Iterate through the inputs
            for count, i in enumerate(self.inputs):
                binary_string = format(count, f'0{self.num_wires}b')
                binary_list = [int(bit) for bit in binary_string]
                
                if any(x > 0 for x in self.inputs[:2**(self.num_wires-1)]) and first_target_wire_flip == False:
                    first_target_wire_flip = True
                    qml.PauliX(wires=self.num_wires-1)
                
                if count >= 2**(self.num_wires-1) and second_target_wire_flip==False:
                    second_target_wire_flip = True
                    qml.PauliX(wires=self.num_wires-1)
                # Apply nothing if the input is 0    
                if i == 0:
                    continue

                qml.ctrl(op=qml.PhaseShift(phi=i,wires=self.num_wires-1), control=[_ for _ in range(0, self.num_wires-1)],control_values=binary_list[::-1][:-1])

        return tape
######
#Test#
######
scale_to = 13
x = torch.tensor([0,0,3,4,0,1,3,3,0,1])

num_wires = math.ceil((math.log2(x.shape[-1])))

dev = qml.device('default.qubit', wires=num_wires)

@qml.qnode(dev)
def circuit():
    PhaseEncoder(wires=dev.wires).set_inputs(x,scale_to,num_wires)
    return [qml.expval(qml.PauliZ(wire)) for wire in dev.wires]

drawer = qml.draw(circuit, expansion_strategy="device")
print(drawer())

Version Information:
Platform info: Linux-5.15.90.1-microsoft-standard-WSL2-x86_64-with-glibc2.35
Python version: 3.9.0
Numpy version: 1.23.5
Scipy version: 1.9.1
Installed devices:

  • default.gaussian (PennyLane-0.31.1)
  • default.mixed (PennyLane-0.31.1)
  • default.qubit (PennyLane-0.31.1)
  • default.qubit.autograd (PennyLane-0.31.1)
  • default.qubit.jax (PennyLane-0.31.1)
  • default.qubit.tf (PennyLane-0.31.1)
  • default.qubit.torch (PennyLane-0.31.1)
  • default.qutrit (PennyLane-0.31.1)
  • null.qubit (PennyLane-0.31.1)
  • lightning.qubit (PennyLane-Lightning-0.31.0)
  • qiskit.aer (PennyLane-qiskit-0.30.1)
  • qiskit.basicaer (PennyLane-qiskit-0.30.1)
  • qiskit.ibmq (PennyLane-qiskit-0.30.1)
  • qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.30.1)
  • qiskit.ibmq.sampler (PennyLane-qiskit-0.30.1)
  • lightning.gpu (PennyLane-Lightning-GPU-0.30.0)
  • cirq.mixedsimulator (PennyLane-Cirq-0.29.0)
  • cirq.pasqal (PennyLane-Cirq-0.29.0)
  • cirq.qsim (PennyLane-Cirq-0.29.0)
  • cirq.qsimh (PennyLane-Cirq-0.29.0)
  • cirq.simulator (PennyLane-Cirq-0.29.0)

Hello @Anthony_Smaldone,

Welcome to the forum! To make a template batchable, carefully following these instructions should in principle do the trick, at least for default.qubit. You can also look at how other PennyLane templates do this, for example this one deals with batches in the last 5 lines. Maybe you can adapt it to your needs.

Cheers,

Alvaro

2 Likes

Thank you for sending me that tutorial. I have restructured my code to following the Operation format, yet I am still having trouble with the batching. I looked at the last few lines of the AngleEmbedding code, and saw it returned:

return [rotation(features[i], wires=wires[i]) for i in range(len(wires))]

At this stage, the features are shape (features,batch), so does this rotation inherently handle being given a vector of inputs rather than a single angle, allowing the batching to take place? If so, should my pennylane gate operations follow suit in my code? My revised code:

import pennylane as qml
import torch
import math

class PhaseEncoder(qml.operation.Operation):

    # Define how many wires the operator acts on in total.
    # In our case this may be one or two, which is why we
    # use the AnyWires Enumeration to indicate a variable number.
    num_wires = qml.operation.AnyWires

    # This attribute tells PennyLane what differentiation method to use. Here
    # we request parameter-shift (or "analytic") differentiation.
    grad_method = None

    def __init__(self, wires, inputs, do_queue=None, id=None):

        # checking the inputs --------------

        if inputs is None:
            raise ValueError("Expected inputs to encoder; got None.")
        
        if wires is None:
            raise ValueError("Expected wires for encoder; got None.")
            
        shape = qml.math.shape(inputs)
        
        if shape[-1] > 2**len(wires):
            raise ValueError("Not enough wires to encode "+str(shape[-1])+" values.")

        # note: we use the framework-agnostic math library since
        # trainable inputs could be tensors of different types

        #------------------------------------
        # do_flip is not trainable but influences the action of the operator,
        # which is why we define it to be a hyperparameter
        self._hyperparameters = {
            "inputs": inputs
        }

        # The parent class expects all trainable parameters to be fed as positional
        # arguments, and all wires acted on fed as a keyword argument.
        # The id keyword argument allows users to give their instance a custom name.
        # The do_queue keyword argument specifies whether or not
        # the operator is queued when created in a tape context.
        # Note that do_queue is deprecated. In the future, please use
        # qml.QueuingManager.stop_recording().
        super().__init__(wires=wires, do_queue=do_queue, id=id)

    @property
    def num_params(self):
        # if it is known before creation, define the number of parameters to expect here,
        # which makes sure an error is raised if the wrong number was passed
        return 0

    @staticmethod
    def compute_decomposition(wires, inputs):  # pylint: disable=arguments-differ
        # Overwriting this method defines the decomposition of the new gate, as it is
        # called by Operator.decomposition().
        # The general signature of this function is (*parameters, wires, **hyperparameters).
        op_list = []
        n_qubits = len(wires)
        
        batched = qml.math.ndim(inputs) > 1
        inputs = qml.math.T(inputs) if batched else inputs
        
        first_target_wire_flip = False
        second_target_wire_flip = False
        for wire in wires:
            op_list.append(qml.Hadamard(wires=wire))
        
        # Iterate through the inputs
        for count, i in enumerate(inputs):
            binary_string = format(count, f'0{n_qubits}b')
            binary_list = [int(bit) for bit in binary_string]

            if any(x > 0 for x in inputs[:,:2**(n_qubits-1)].squeeze()) and first_target_wire_flip == False:
                first_target_wire_flip = True
                op_list.append(qml.PauliX(wires=wires[-1]))
            
            if count >= 2**(n_qubits-1) and second_target_wire_flip==False:
                second_target_wire_flip = True
                op_list.append(qml.PauliX(wires=wires[-1]))
            # Apply nothing if the input is 0    
            if i[count] == 0:
                continue

            #op_list.append(qml.ctrl(op=qml.PhaseShift(phi=i,wires=wires[-1]), control=[_ for _ in range(0, n_qubits-1)],control_values=binary_list[::-1][:-1]))

            op_list.append(qml.ctrl(op=qml.PhaseShift(phi=i[count],wires=wires[-1]), control=[_ for _ in wires[:-1]],control_values=binary_list[::-1][:-1]))
        return op_list




# PhaseEncode into 1 qubit each (a batch of 3)
test_tensor = torch.tensor([[0.7934, 0.8882],
                            [0.5348, 0.1101],
                            [0.9403, 0.1511]])
num_wires = math.ceil((math.log2(test_tensor.shape[-1])))

dev = qml.device("default.qubit", wires=num_wires)
@qml.qnode(dev)

def circuit(inputs):
    PhaseEncoder(wires=dev.wires, inputs=inputs)
    return qml.expval(qml.PauliX(0))


print(circuit(test_tensor))

I believe I have found a solution, I will post it shortly.

1 Like

This has worked for me. Will Pennylane eventually have a built in template for phase encoding as is seen in TorchQuantum?

import pennylane as qml
import math
import numpy as np

class PhaseEncoder(qml.operation.Operation):

    r'''
    Encodes 2^n features in the phase of the computational basis states of n qubits.
    Follows the procedure detailed in https://arxiv.org/abs/2007.14288.
    
    Note:
    The values given will not be normalized to pi or 2*pi, this should be considered in pre-processing.
    If there are too few features to populate the rest of the computational basis states with the provided wires,
            the remaining states will not be not be given a phase.
    
    If too many or too few features given the number of wires, this Operation will throw an error.
        i.e. if ceil((math.log2(features_shape))) != len(wires)
    
        
    
    Args:
    features (tensor_like): input tensor of dimension up to 2^len(wires)
    do_queue (bool): indicates whether the operator should be recorded when created in
            a tape context. This argument is deprecated, instead of setting it to ``False``
            use :meth:`~.queuing.QueuingManager.stop_recording`.
    id (str): custom label given to an operator instance,
        can be useful for some applications where the instance has to be identified.
    
    
    Example:
    # PhaseEncode into 1 qubit each (3 batches)
    list_of_features = [[0.7934, 0.0],[0.5348, 0.1101],[0.9403, 0.1511]]

    dev = qml.device("default.qubit", wires=4)
    @qml.qnode(dev)
    def circuit(features):
        PhaseEncoder(wires=[0], features=features)
        return qml.expval(qml.PauliX(0))
        
    circuit(list_of_features)
    '''
    
    num_wires = qml.operation.AnyWires
    grad_method = None

    def __init__(self, wires, features, do_queue=None, id=None):

        # checking the features

        if features is None:
            raise ValueError("Expected features to encoder; got None.")
        
        if wires is None:
            raise ValueError("Expected wires for encoder; got None.")
            
        shape = qml.math.shape(features)     
        
        if math.ceil((math.log2(shape[-1]))) > len(wires):
            raise ValueError(f"Not enough wires to encode {shape[-1]} values. Given {len(wires)} wires; expected {math.ceil((math.log2(shape[-1])))}.")
            
        if math.ceil((math.log2(shape[-1]))) < len(wires):
            raise ValueError(f"Too many wires to encode {shape[-1]} values. Given {len(wires)} wires; expected {math.ceil((math.log2(shape[-1])))}.")

        self._hyperparameters = {
            "features": features
        }

        super().__init__(wires=wires, do_queue=do_queue, id=id)

    @property
    def num_params(self):
        return 0

    @staticmethod
    def compute_decomposition(wires, features):
        op_list = []
        n_qubits = len(wires)
        
        # check if features are batched
        batched = qml.math.ndim(features) > 1
        features = qml.math.T(features) if batched else features
        
        # apply hadamard to all qubits used for encoding
        for wire in wires:
            op_list.append(qml.Hadamard(wires=wire))
        
        # For simplicity, phase shift gates will target the last qubit
        # phases are computed based on increasing order of the binary
        # representation of the computational basis state.
        # The first half of phases to be computed begin with an open
        # control on the phase shifting target (ex. |001>,|010>,...|011>),
        # thus an X gate and eventual closing X at the halfway mark (ex. before |100>)
        # are applied
        wire_flip = False
        op_list.append(qml.PauliX(wires=wires[-1]))
        
        # Iterate through the features
        for count in range(qml.math.shape(features)[0]):
            binary_string = format(count, f'0{n_qubits}b')
            binary_list = [int(bit) for bit in binary_string]
            
            # Add a closing X gate to begin assigning phases to
            # states where the last qubit is in the |1> state
            if count >= 2**(n_qubits-1) and wire_flip==False:
                wire_flip = True
                op_list.append(qml.PauliX(wires=wires[-1]))

            # Apply a controlled phase shift gate arbitrarily targeting the last wire
            # This is controlled by all wires given to the PhaseEncoder
            # The open vs closed controls are dictated by the current computational basis state being assigned a phase
            
            op_list.append(qml.ctrl(op=qml.PhaseShift(phi=features[count],wires=wires[-1]), control=[_ for _ in wires[:-1]],control_values=binary_list[::-1][:-1]))
        
        return op_list


# PhaseEncode into 1 qubit each (3 batches)
list_of_features = [[0.7934, 0.0],[0.5348, 0.1101],[0.9403, 0.1511]]

dev = qml.device("default.qubit", wires=4)
@qml.qnode(dev)
def circuit(features):
    PhaseEncoder(wires=[0], features=features)
    return qml.expval(qml.PauliX(0))

print(circuit(list_of_features))

Hi @Anthony_Smaldone ! Good to hear that you find it :slight_smile:
In PennyLane we use this template for Phase Enconding, but we assume that we have the same number of features and qubits.

I have been thinking a little more about your question and I think I misunderstood you. I think what you want to do is to apply the DiagonalQubitUnitary gate :smile:

Note that to get the state you want, you must put hadamards initially to create the equiprobable superposition

Does this solve your question?

3 Likes

While I have already constructed the PhaseEncoder, this is a much simpler solution! I did not know about this Pennylane operation, thank you for directing me to this!

2 Likes