# How do I create a parameterized Hamiltonian with the same coefficient for certain terms?

Hello! I’m trying to create a Hamiltonian that will be used in conjunction with `qml.evolve()`. However, I want the Hamiltonian to be a parameterized one since I want the coefficients to also be optimized during the learning process. I’m creating my parameterized Hamiltonian as follows:

``````import pennylane as qml
import pennylane.numpy as np
import jax.numpy as jnp
import jax

class QuantumPerceptron():
def __init__ (self, n_wires, L, native_coupling = 1.0):
self.n_wires = n_wires
self.native_coupling = native_coupling
self.L = L

self.create_parameterized_hamiltonian()

def create_parameterized_hamiltonian(self):

self.create_native_hamiltonian()

self.H = self. H_native

def create_native_hamiltonian(self):

self.native_fields_squeezing = [self.get_control_field() for i in range(self.n_wires-1)]
self.H_native_operators_squeezing = [qml.PauliZ(i) @ qml.PauliZ(i+1) for i in range(self.n_wires-1)] # Assuming nearest neighbor interactions

self.native_fields_identity = [self.get_control_field() for i in range(self.n_wires)]
self.H_native_operators_identity = [qml.Identity(i) for i in range(self.n_wires)]

self.total_native_fields = self.native_fields_squeezing + self.native_fields_identity
self.H_total_native_operators = self.H_native_operators_squeezing + self.H_native_operators_identity

self.H_native = qml.dot(self.total_native_fields, self.H_total_native_operators)

def get_control_field(self):

def control_field(p,t):
# p is the trainable parameter
# t is the time
return p

return control_field
``````

In particular, let’s say that I want to create this Hamiltonian for 5 wires. If I create my Hamiltonian using the above functions, I will get something like

``````  (control_field(params_0, t)*(PauliZ(wires=[0]) @ PauliZ(wires=[1])))
+ (control_field(params_1, t)*(PauliZ(wires=[1]) @ PauliZ(wires=[2])))
+ (control_field(params_2, t)*(PauliZ(wires=[2]) @ PauliZ(wires=[3])))
+ (control_field(params_3, t)*(PauliZ(wires=[3]) @ PauliZ(wires=[4])))
+ (control_field(params_4, t)*(Identity(wires=[0])))
+ (control_field(params_5, t)*(Identity(wires=[1])))
+ (control_field(params_6, t)*(Identity(wires=[2])))
+ (control_field(params_7, t)*(Identity(wires=[3])))
+ (control_field(params_8, t)*(Identity(wires=[4])))
``````

Thus, when I do `qml.evolve(Hamiltonian)` it must be called as `qml.evolve(Hamiltonian)([0,1,2,3,4,5,6,7,8], 1)` for it to run.

As one can see, each `PauliZ(i) @ PauliZ(i+1)` term contains a different `control_field(params, t)` coefficient. Ideally, I would want each `PauliZ(i) @ PauliZ(i+1)` term to have the same coefficient and similarly for each `Identity(i)` term. So, in this particular example, I’d like for my Hamiltonian to look like

``````  (control_field(params_0, t)*(PauliZ(wires=[0]) @ PauliZ(wires=[1])))
+ (control_field(params_0, t)*(PauliZ(wires=[1]) @ PauliZ(wires=[2])))
+ (control_field(params_0, t)*(PauliZ(wires=[2]) @ PauliZ(wires=[3])))
+ (control_field(params_0, t)*(PauliZ(wires=[3]) @ PauliZ(wires=[4])))
+ (control_field(params_1, t)*(Identity(wires=[0])))
+ (control_field(params_1, t)*(Identity(wires=[1])))
+ (control_field(params_1, t)*(Identity(wires=[2])))
+ (control_field(params_1, t)*(Identity(wires=[3])))
+ (control_field(params_1, t)*(Identity(wires=[4])))
``````

I want it such that I can just call `qml.evolve(Hamiltonian)([0,1], 1)` to run the evolution.

To further explain: The reason I want to do this is so that during the training process, the coefficient parameters are updated “as one” - the coefficient affecting `PauliZ(wires=[0]) @ PauliZ(wires=[1])`, for example, is the same coefficient affecting the `PauliZ(wires=[2]) @ PauliZ(wires=[3])` term.

Here’s my `qml.about()` if it helps!

``````Name: PennyLane
Version: 0.33.1
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/PennyLaneAI/pennylane
Author:
Author-email:
Location: /Users/.../python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane-Lightning

Platform info:           macOS-13.4.1-x86_64-i386-64bit
Python version:          3.11.5
Numpy version:           1.26.2
Scipy version:           1.11.4
Installed devices:
- default.gaussian (PennyLane-0.33.1)
- default.mixed (PennyLane-0.33.1)
- default.qubit (PennyLane-0.33.1)
- default.qubit.jax (PennyLane-0.33.1)
- default.qubit.legacy (PennyLane-0.33.1)
- default.qubit.tf (PennyLane-0.33.1)
- default.qubit.torch (PennyLane-0.33.1)
- default.qutrit (PennyLane-0.33.1)
- null.qubit (PennyLane-0.33.1)
- lightning.qubit (PennyLane-Lightning-0.33.1)
``````

Hey @NickGut0711,

This should all be possible in PennyLane with just a bit of control flow on your coefficients when you construct the parameterized Hamiltonian. This demo will be helpful: Differentiable pulse programming with qubits in PennyLane | PennyLane Demos. Particularly this code block:

``````coeffs = [1.0] * 2
coeffs += [lambda p, t: jnp.sin(p[0] * t) + jnp.sin(p[1] * t) for _ in range(3)]
ops = [qml.PauliX(i) @ qml.PauliX(i + 1) for i in range(2)]
ops += [qml.PauliZ(i) for i in range(3)]

Ht = qml.dot(coeffs, ops)

# random coefficients
key = jax.random.PRNGKey(777)
subkeys = jax.random.split(key, 3) # create list of 3 subkeys
params = [jax.random.uniform(subkeys[i], shape=[2], maxval=5) for i in range(3)]
print(Ht(params, 0.5))
``````

Let me know if that helps!

Hello @isaacdevlugt!

I looked at the demo, and it’s effectively doing the same thing as before. Here’s what I changed in my code

``````self.native_fields_squeezing = [lambda p, t: p[0] for _ in range(self.n_wires-1)]
self.H_native_operators_squeezing = [qml.PauliZ(i) @ qml.PauliZ(i+1) for i in range(self.n_wires-1)] # Assuming nearest neighbor interactions

self.native_fields_identity = [lambda p, t: p[1] for _ in range(self.n_wires)]
self.H_native_operators_identity = [qml.Identity(i) for i in range(self.n_wires)]
``````

To test this I do

``````perceptron = QuantumPerceptron(n_wires, L)
H_perceptron = perceptron.H

H_perceptron([0.001, 0.002], t=1)
``````

and I still get the error where it expects 9 parameters and instead receives just 2 (to make this clear I just want the first 4 coefficients to be `p[0]` and the last 5 coefficients to be `p[1]`).

``````---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/Users/.../... .ipynb Cell 5 line 1
----> H_perceptron([0.001, 0.002], t=1)

File ~/.../pennylane/pulse/parametrized_hamiltonian.py:238, in ParametrizedHamiltonian.__call__(self, params, t)
236 def __call__(self, params, t):
237     if len(params) != len(self.coeffs_parametrized):
--> 238         raise ValueError(
239             \"The length of the params argument and the number of scalar-valued functions \"
240             f\"must be the same. Received len(params) = {len(params)} parameters but \"
241             f\"expected {len(self.coeffs_parametrized)} parameters.\"
242         )
243     return self.H_fixed() + self.H_parametrized(params, t)

ValueError: The length of the params argument and the number of scalar-valued functions must be the same. Received len(params) = 2 parameters but expected 9 parameters."
}
``````

But as you can see, my idea is to just have params be a `length=2` array such that each term that is multiplied by some coefficient is being multiplied by the same “parameter/coefficient object”. If they are different parameters then, when my model is training, each term that has a certain parameter/coefficient will be updated independently, and I don’t want that. I know that this is probably some simple control flow issue like you said that I’m not really seeing right now. Do you see the solution here? I’ll keep looking at this.

Hey @NickGut0711, I might be misunderstanding what you want to do but let me consult another person internally here and see what they say!

1 Like

Hi @NickGut0711 I think I can help here.
We designed `ParametrizedHamiltonian` such that it would always require the same number of coefficients as there are terms in the parametrized Hamiltonian. That means for the case of having multiple equal terms you need to repeat the coefficients (solution 1 below) OR you define those equal terms as one operator and add those to the ParametrizedHamiltonian (solution 2 below, probably what you are looking for)

``````import pennylane as qml
from pennylane import X, Y, Z, I
import jax.numpy as jnp
import jax
jax.config.update("jax_enable_x64", True)
qml.operation.enable_new_opmath()

n = 5
ops = [Z(i) @ Z(i+1) for i in range(n-1)]
ops += [I(i) for i in range(n)]
coeffs = [qml.pulse.constant for _ in range(len(ops))] # same as your control field that is just a constant
H = qml.dot(coeffs, ops)

# In case you want to use the parametrized Hamiltonian in a qnode:

dev = qml.device("default.qubit.jax", wires=range(n))
@qml.qnode(dev, interface="jax")
def qnode(p):
qml.evolve(H)([p[0]] * (n-1) + [p[1]] * n, t=10.)
return qml.expval(Z(0))

p = jnp.array([1., 2.])
print(qnode(p))

# In case you really want to work on the Hamiltonian level directly, you can wrap it in a callable

def Ham(p, t):
return H([p[0]] * (n-1) + [p[1]] * n, t)

print(Ham(p, 0.5))
``````

Solution2: Here you would summarize all the Z@Z terms and Identity terms into one operator, each:

``````import pennylane as qml
from pennylane import X, Y, Z, I
import jax.numpy as jnp
import jax
jax.config.update("jax_enable_x64", True)
qml.operation.enable_new_opmath()

n = 5
H1 = qml.sum(*[Z(i) @ Z(i+1) for i in range(n-1)])
H2 = qml.sum(*[I(i) for i in range(n)])

H = qml.pulse.constant * H1 + qml.pulse.constant * H2 # requires only 2 parameters

# Working on the Hamiltonian level and qnode level is straight forward now

dev = qml.device("default.qubit.jax", wires=range(n))
@qml.qnode(dev, interface="jax")
def qnode(p):
qml.evolve(H)(p, t=10.)
return qml.expval(Z(0))

p = jnp.array([1., 2.])
print(qnode(p))

print(H(p, 0.5)) # call the Hamiltonian directly
``````

Hope that helps! Let me know if something is unclear

1 Like

Hi @Qottmann! Both of these solutions really helped. I was thinking about using `qml.pulse.constant` anyways. Thank you!

Happy to hear it helped

Just a small FYI, the `qml.pulse.constant` is just a shorthand for your

``````def control_field(p,t):
# p is the trainable parameter
# t is the time
return p
``````

and is doing the exact same

1 Like