Constructing arbitrary Hamiltonians
At the heart of digital-analog quantum computing is the description and execution of analog blocks, which represent a set of interacting qubits under some interaction Hamiltonian.
For this purpose, Qadence relies on the hamiltonian_factory function to create arbitrary Hamiltonian blocks to be used as generators of HamEvo or as observables to be measured.
Arbitrary all-to-all Hamiltonians
Section titled “Arbitrary all-to-all Hamiltonians”Arbitrary all-to-all interaction Hamiltonians can be easily created by passing the number of qubits in the first argument. The type of interaction can be chosen from the available ones in the Interaction enum type.
from qadence import hamiltonian_factoryfrom qadence import N, X, Y, Zfrom qadence import Interaction
n_qubits = 3
hamilt = hamiltonian_factory(n_qubits, interaction=Interaction.ZZ)AddBlock(0,1,2)├── [mul: 1.000]│ └── KronBlock(0,1)│ ├── Z(0)│ └── Z(1)├── [mul: 1.000]│ └── KronBlock(0,2)│ ├── Z(0)│ └── Z(2)└── [mul: 1.000] └── KronBlock(1,2) ├── Z(1) └── Z(2)Alternatively, a custom interaction function can also be defined. The input should be two integer indices and and it should return a composition of pauli terms representing the interaction between qubits and :
def custom_int(i: int, j: int): return X(i) @ X(j) + Y(i) @ Y(j)
n_qubits = 2
hamilt = hamiltonian_factory(n_qubits, interaction=custom_int)AddBlock(0,1)└── [mul: 1.000] └── AddBlock(0,1) ├── KronBlock(0,1) │ ├── X(0) │ └── X(1) └── KronBlock(0,1) ├── Y(0) └── Y(1)Single-qubit terms can also be added by passing the respective operator directly to the detuning argument. For example, the total magnetization is commonly used as an observable to be measured:
total_mag = hamiltonian_factory(n_qubits, detuning = Z)AddBlock(0,1)├── [mul: 1.000]│ └── Z(0)└── [mul: 1.000] └── Z(1)For further customization, arbitrary coefficients can be passed as arrays to the interaction_strength and detuning_strength arguments for the two-qubits and single-qubit terms respectively.
n_qubits = 3
hamilt = hamiltonian_factory( n_qubits, interaction=Interaction.ZZ, detuning=Z, interaction_strength=[0.5, 0.2, 0.1], detuning_strength=[0.1, 0.5, -0.3])AddBlock(0,1,2)├── [mul: 0.100]│ └── Z(0)├── [mul: 0.500]│ └── Z(1)├── [mul: -0.30]│ └── Z(2)├── [mul: 0.500]│ └── KronBlock(0,1)│ ├── Z(0)│ └── Z(1)├── [mul: 0.200]│ └── KronBlock(0,2)│ ├── Z(0)│ └── Z(2)└── [mul: 0.100] └── KronBlock(1,2) ├── Z(1) └── Z(2)For one more example, let's create a transverse-field Ising model,
n_qubits = 4n_edges = int(0.5 * n_qubits * (n_qubits - 1))
z_terms = [1.0] * n_qubitszz_terms = [2.0] * n_edges
zz_ham = hamiltonian_factory( n_qubits, interaction=Interaction.ZZ, detuning=Z, interaction_strength=zz_terms, detuning_strength=z_terms)
x_terms = [-1.0] * n_qubitsx_ham = hamiltonian_factory(n_qubits, detuning = X, detuning_strength = x_terms)
transverse_ising = zz_ham + x_hamAddBlock(0,1,2,3)├── AddBlock(0,1,2,3)│ ├── [mul: 1.000]│ │ └── Z(0)│ ├── [mul: 1.000]│ │ └── Z(1)│ ├── [mul: 1.000]│ │ └── Z(2)│ ├── [mul: 1.000]│ │ └── Z(3)│ ├── [mul: 2.000]│ │ └── KronBlock(0,1)│ │ ├── Z(0)│ │ └── Z(1)│ ├── [mul: 2.000]│ │ └── KronBlock(0,2)│ │ ├── Z(0)│ │ └── Z(2)│ ├── [mul: 2.000]│ │ └── KronBlock(0,3)│ │ ├── Z(0)│ │ └── Z(3)│ ├── [mul: 2.000]│ │ └── KronBlock(1,2)│ │ ├── Z(1)│ │ └── Z(2)│ ├── [mul: 2.000]│ │ └── KronBlock(1,3)│ │ ├── Z(1)│ │ └── Z(3)│ └── [mul: 2.000]│ └── KronBlock(2,3)│ ├── Z(2)│ └── Z(3)└── AddBlock(0,1,2,3) ├── [mul: -1.00] │ └── X(0) ├── [mul: -1.00] │ └── X(1) ├── [mul: -1.00] │ └── X(2) └── [mul: -1.00] └── X(3)Arbitrary Hamiltonian topologies
Section titled “Arbitrary Hamiltonian topologies”Arbitrary interaction topologies can be created using the Qadence Register.
Simply pass the register with the desired topology as the first argument to the hamiltonian_factory:
from qadence import Register
reg = Register.square(qubits_side=2)
square_hamilt = hamiltonian_factory(reg, interaction=Interaction.NN)AddBlock(0,1,2,3)├── [mul: 1.000]│ └── KronBlock(0,1)│ ├── N(0)│ └── N(1)├── [mul: 1.000]│ └── KronBlock(0,3)│ ├── N(0)│ └── N(3)├── [mul: 1.000]│ └── KronBlock(1,2)│ ├── N(1)│ └── N(2)└── [mul: 1.000] └── KronBlock(2,3) ├── N(2) └── N(3)Adding variational parameters
Section titled “Adding variational parameters”Finally, fully parameterized Hamiltonians can be created by passing a string to the strength arguments, and used to prefix the name of the variational parameters.
n_qubits = 3
nn_ham = hamiltonian_factory( n_qubits, interaction=Interaction.NN, detuning=N, interaction_strength="c", detuning_strength="d")AddBlock(0,1,2)├── [mul: d_0]│ └── N(0)├── [mul: d_1]│ └── N(1)├── [mul: d_2]│ └── N(2)├── [mul: c_01]│ └── KronBlock(0,1)│ ├── N(0)│ └── N(1)├── [mul: c_02]│ └── KronBlock(0,2)│ ├── N(0)│ └── N(2)└── [mul: c_12] └── KronBlock(1,2) ├── N(1) └── N(2)Alternatively, fully customizable sympy functions can be passed in an array using the Qadence parameters.
Furthermore, the use_all_node_pairs = True option can be passed so that interactions are created for every single
node pair in the register, irrespectively of the topology of the edges. This is useful for creating Hamiltonians
that depend on qubit distance.
from qadence import VariationalParameter, Register
# Square register of 4 qubits with a dimensionless distance of 8.0reg = Register.square(2, spacing = 8.0)
# Get the distances between all pairs of qubitsdistance_dict = reg.distances
# Create interaction strength with variational parameter and 1/r termstrength_list = []for node_pair in reg.all_node_pairs: param = VariationalParameter("x" + f"_{node_pair[0]}{node_pair[1]}") dist_factor = reg.distances[node_pair] strength_list.append(param / dist_factor)
nn_ham = hamiltonian_factory( reg, interaction=Interaction.NN, interaction_strength=strength_list, use_all_node_pairs=True,)AddBlock(0,1,2,3)├── [mul: 0.125*x_01]│ └── KronBlock(0,1)│ ├── N(0)│ └── N(1)├── [mul: 0.088*x_02]│ └── KronBlock(0,2)│ ├── N(0)│ └── N(2)├── [mul: 0.125*x_03]│ └── KronBlock(0,3)│ ├── N(0)│ └── N(3)├── [mul: 0.125*x_12]│ └── KronBlock(1,2)│ ├── N(1)│ └── N(2)├── [mul: 0.088*x_13]│ └── KronBlock(1,3)│ ├── N(1)│ └── N(3)└── [mul: 0.125*x_23] └── KronBlock(2,3) ├── N(2) └── N(3)