Skip to content
Pasqal Documentation

Pulse-level programming with Pulser

Qadence offers a direct interface with Pulser1, an open-source pulse-level interface written in Python and specifically designed for programming neutral atom quantum computers.

Using directly Pulser requires advanced knowledge on pulse-level programming and on how neutral atom devices work. Qadence abstracts this complexity out by using the familiar block-based interface for building pulse sequences in Pulser while leaving the possibility to directly manipulate them if required by, for instance, optimal pulse shaping.

For inquiries and more details on the cloud credentials, please contact info@pasqal.com.

When simulating pulse sequences written using Pulser, the underlying constructed Hamiltonian is equivalent to a digital-analog quantum computing program (see digital-analog emulation for more details) with the following interaction term:

Hint=i<jC6RiRj6n^in^j \mathcal{H}_{\textrm{int}} = \sum_{i<j} \frac{C_6}{|R_i - R_j|^6} \hat{n}_i \hat{n}_j

where C6C_6 is an interaction strength coefficient dependent on the principal quantum number of chosen the neutral atom system, RiR_i are atomic positions in Cartesian coordinates and n^=1σiz2\hat{n} = \frac{1-\sigma^z_i}{2} the number operator.

Currently, the Pulser backend supports the following operations:

gate description trainable parameter
RX, RY Single qubit rotations. Notice that the interaction is on and this affects the resulting gate fidelity. rotation angle
AnalogRX, AnalogRY, AnalogRZ Span a single qubit rotation among the entire register. rotation angle
entangle Fully entangle the register. interaction time
AnalogInteraction An idle block to to free-evolve for a duration according to the interaction. free evolution time

Sequence the Bell state on a two qubit register

Section titled “Sequence the Bell state on a two qubit register”

The next example illustrates how to create a pulse sequence to prepare a Bell state. This is a sequence of an entanglement operation, represented as an entangle gate (using CZ interactions) in the XX-basis and a YY rotation for readout in the ZZ-basis:

from qadence import chain, entangle, RY
bell_state = chain(
entangle("t", qubit_support=(0,1)),
RY(0, "y"),
)
bell_state = ChainBlock(0,1)
├── AnalogEntanglement(t=0.9007440457171584, support=(0, 1))
└── RY(0) [params: ['y']]

Next, a Register with two qubits is combined with the resulting ChainBlock to form a circuit. Then, the QuantumModel converts the circuit into a proper parametrized pulse sequence with the Pulser backend. Supplying the parameter values allows to sample the pulse sequence outcome:

import torch
import matplotlib.pyplot as plt
from qadence import Register, QuantumCircuit, QuantumModel, PI
register = Register.line(2, spacing = 8.0) # Two qubits with a distance of 8µm
circuit = QuantumCircuit(register, bell_state)
model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr")
params = {
"t": torch.tensor([1000]), # ns
"y": torch.tensor([3*PI/2]),
}
# Return the final state vector
final_vector = model.run(params)
# Sample from the result state vector
sample = model.sample(params, n_shots=50)[0]
final_vector = tensor([[-0.7114-0.0169j, -0.0338+0.0155j, 0.0110-0.0457j, 0.6631-0.2245j]])
sample = Counter({'00': 27, '11': 23})

Plot the distribution:

2025-06-04T09:57:35.131562 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/
assign_paramters
model.assign_parameters(params).draw(show=False)
2025-06-04T09:57:35.249660 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

At variance with other backends, Pulser provides the concept of Device. A Device instance encapsulates all the properties for the definition of a real neutral atoms processor, including but not limited to the maximum laser amplitude for pulses, the maximum distance between two qubits and the maximum duration of the pulse. For more information, please check this tutorial (external).

Qadence offers a simplified interface with only two devices which are detailed here:

  • IDEALIZED (default): ideal device which should be used only for testing purposes. It does not restrict the simulation of pulse sequences.
  • REALISTIC: device specification close to real neutral atom quantum processors.

One can use the Configuration of the Pulser backend to select the appropriate device:

from qadence import BackendName, DiffMode
from qadence import RealisticDevice
# Choose a realistic device
register = Register.line(2, spacing = 8.0, device_specs = RealisticDevice())
circuit = QuantumCircuit(register, bell_state)
model = QuantumModel(
circuit,
backend=BackendName.PULSER,
diff_mode=DiffMode.GPSR,
)
params = {
"t": torch.tensor([1000]), # ns
"y": torch.tensor([3*PI/2]),
}
# Sample from the result state vector
sample = model.sample(params, n_shots=50)[0]
sample = Counter({'11': 26, '00': 24})

A major advantage of the block-based interface in Qadence is the ease to compose complex operations from a restricted set of primitive ones. In the following, a custom entanglement operation is used as an example.

The operation consists of moving all the qubits to the XX-basis. This is realized when the atomic interaction performs a controlled-ZZ operation during the free evolution. As seen before, this is implemented with the AnalogInteraction and AnalogRY blocks together with appropriate parameters.

from qadence import AnalogRY, chain, AnalogInteraction
# Custom entanglement operation.
def my_entanglement(duration):
return chain(
AnalogRY(-PI / 2),
AnalogInteraction(duration)
)
protocol = chain(
my_entanglement("t"),
RY(0, "y"),
)
register = Register.line(2, spacing = 8.0)
circuit = QuantumCircuit(register, protocol)
model = QuantumModel(circuit, backend=BackendName.PULSER, diff_mode=DiffMode.GPSR)
params = {
"t": torch.tensor([500]), # ns
"y": torch.tensor([PI / 2]),
}
sample = model.sample(params, n_shots=50)[0]
2025-06-04T09:57:35.637373 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

Finally, let's put all together by constructing a digital-analog version of a quantum neural network circuit with feature map and variational ansatz.

from qadence import kron, feature_map, BasisSet
from qadence.operations import RX, RY, AnalogRX
hea_one_layer = chain(
kron(RY(0, "th00"), RY(1, "th01")),
kron(RX(0, "th10"), RX(1, "th11")),
kron(RY(0, "th20"), RY(1, "th21")),
entangle("t", qubit_support=(0,1)),
)
protocol = chain(
feature_map(1, param="x", fm_type=BasisSet.FOURIER),
hea_one_layer,
AnalogRX(PI/4)
)
register = Register.line(2, spacing=8.0)
circuit = QuantumCircuit(register, protocol)
model = QuantumModel(circuit, backend=BackendName.PULSER, diff_mode=DiffMode.GPSR)
params = {
"x": torch.tensor([0.8]), # rad
"t": torch.tensor([900]), # ns
"th00": torch.rand(1), # rad
"th01": torch.rand(1), # rad
"th10": torch.rand(1), # rad
"th11": torch.rand(1), # rad
"th20": torch.rand(1), # rad
"th21": torch.rand(1), # rad
}
model.assign_parameters(params).draw(draw_phase_area=True, show=False)
2025-06-04T09:57:35.802557 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/