Replace usage of `qml.init` with `Template.shape`
The init module has been deprecated, and the recommend approach for generating initial weights is to use the Template.shape method:
>>> from pennylane.templates import StronglyEntanglingLayers
>>> qml.init.strong_ent_layers_normal(n_layers=3, n_wires=2) # deprecated
>>> np.random.random(StronglyEntanglingLayers.shape(n_layers=3, n_wires=2)) # new approach
We should update demos using the init module to instead use the shape method.
is this meant to work for user-defined templates too? Are templates supposed to have only trainable parameters or only data parameters?
In the image there's a template we defined during qhack, what would be the correct approach to randomly initialize only the parameters, not the x here?

@emgilfuster that is a good question. @mariaschuld, I believe the @qml.template decorator is deprecated?
for information completeness, here's another image:

The random_params function is how we used to initialize the parameters. weights is the naive implementation of the new approach, which is of coursed doomed.
Wait, I am not sure I follow. The init functions are still valid as they were, since they just generate random arrays of the right shape. We can leave them in the code base for a while longer, but encourage users to rather query the template's shape method.
This method is not implemented by default, i.e. it depends on the implementation of the template what shapes are returned and in which format. Of course, the old design of templates via the decorator (which is still possible, just discouraged) means that a template is not a class, and does not elegantly support adding such method [although one could probably hack this somehow].
In short: One should code up templates as Operation subclasses and if they take tensors, one should add a shape method. But the old logic is still entirely supported.
The port to the new design is quite new and there may be a mix of both logics for a while, which we'll start to clean up soon...
I understand, thanks @mariaschuld and @josh146 !
This thread actually gave me an idea - we could update the @template decorator to create new-style templates. For example,
def template(n_params, n_wires=qml.operation.AnyWires, shapes_fn=lambda: None):
def _wrapper(fn):
class Template(qml.operation.Operation):
num_params = n_params
num_wires = n_wires
par_domain = "A"
def __init__(self, *args, **kwargs):
active_tape = qml.tape.get_active_tape()
if active_tape is None:
with qml.tape.QuantumTape() as tape:
fn(*args, **kwargs)
else:
with active_tape.stop_recording(), qml.tape.QuantumTape() as tape:
fn(*args, **kwargs)
self.expanded_tape = tape
super().__init__(*args[:n_params], wires=kwargs["wires"])
def expand(self):
return self.expanded_tape
shapes = staticmethod(shapes_fn)
return Template
return _wrapper
We can then apply this new decorator to our template function, and automatically:
- Turn it into an operation
- Add an expand method
- Add a static shape method
- Automatically add in all required abstract class attributes
def block(x, params, wires):
for w in wires:
qml.Hadamard(wires=w)
qml.RZ(x, wires=w)
qml.RY(params[0][w], wires=w)
qml.broadcast(qml.CRZ, wires, "ring", parameters=params[1])
# Decorator turns `ansatz` into a Template operation
@template(
n_params=2,
shapes_fn=lambda n_layers, n_wires: (tuple(), [n_layers, 2, n_wires])
)
def ansatz(x, params, wires):
for j, block_params in enumerate(params):
block(x, block_params, wires)
dev = qml.device("default.qubit", wires=3)
@qml.qnode(dev)
def circuit(*weights):
ansatz(*weights, wires=[0, 1, 2])
return qml.expval(qml.PauliZ(0))
We can use our template now for built-in parameter initialization:
>>> weights = [np.random.random(i) for i in ansatz.shapes(n_layers=2, n_wires=3)]
>>> print(qml.draw(circuit)(*weights))
0: ──H──RZ(0.194)──RY(0.563)──╭C─────────────────────────╭RZ(0.457)──H──────────RZ(0.194)──RY(0.985)──╭C───────────────────────╭RZ(0.14)──┤ ⟨Z⟩
1: ──H──RZ(0.194)──RY(0.14)───╰RZ(0.776)──╭C──────────H──│───────────RZ(0.194)──RY(0.376)─────────────╰RZ(0.0197)──╭C──────────│──────────┤
2: ──H──RZ(0.194)──RY(0.685)──────────────╰RZ(0.695)─────╰C──────────H──────────RZ(0.194)──RY(0.132)───────────────╰RZ(0.432)──╰C─────────┤
We could even take this further, and allow for backwards pass registration:
# Decorator turns `ansatz` into a Template operation
@template(n_params=2, shapes_fn=lambda n_layers, n_wires: (tuple(), [n_layers, 2, n_wires]))
def ansatz(x, params, wires):
# forward pass
for j, block_params in enumerate(params):
block(x, block_params, wires)
# backward pass
def grad(x, params, wires):
for j, block_params in enumerate(params):
block(x, block_params, wires)
return grad
That's quite nice!
The only complexity is the call of super in init - often it makes more sense to hand-extract which inputs make it to the final "gate params". But having the decorator as a simple class-construction helper would be cool.
Yes, I was thinking that as well at first. But then I thought about it more - the decorator would only be used in cases like the current decorator, where ansatz parameters ~ gate parameters? So it should work fine there, and more advanced users/developers would instead graduate to the Operator subclassing...
Is someone working on it?