Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changing Pulser backend defaults and improved documentation #37

Merged
merged 8 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Lint and type check Qadence.
name: linting and type check

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_fast.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Run Qadence fast tests.
name: fast tests

on:
push:
Expand Down
206 changes: 108 additions & 98 deletions docs/digital_analog_qc/pulser-basic.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
Qadence offers a direct interface with Pulser[^1], a pulse-level programming interface
specifically designed for neutral atom quantum computers.
Qadence offers a direct interface with Pulser[^1], an open-source pulse-level interface written in Python and specifically designed for programming neutral atom quantum computers.

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

!!! note
The Pulser backend is still experimental and the interface might change in the future.

Let's see it in action.

## Default qubit interaction

When simulating pulse sequences written using Pulser, the underlying Hamiltonian it
constructs is equivalent to a DAQC computing paradigm with the following interaction
constructs is equivalent to a digital-analog quantum computing program with the following interaction
Hamiltonian (see [digital-analog emulation](analog-basics.md) for more details):

$$
Expand All @@ -13,33 +22,25 @@ where $C_6$ is an interaction coefficient which depends on the principal quantum
the neutral atom system, $R_i$ are the atomic position in Cartesian coordinates
and $\hat{n} = \frac{1-\sigma^z_i}{2}$ is the number operator.

Using directly Pulser requires deep knowledge on pulse-level programming and on how
neutral atom devices work. Qadence abstracts out this complexity by using the familiar
block-based interface for building pulse sequences in Pulser while leaving the possibility
to directly manipulate them if required.
Notice that this interaction is **always-on** for any computation performed with the Pulser backend and cannot be switched off.

Let's see it in action.
## Pulse sequences with Qadence

!!! note
The Pulser backend is still under heavy development and the interface might change in the future.

## Generate pulses with Qadence
The current backend has the following operations:
Currently, the backend supports the following operations:

| gate | description | trainable parameter |
|-------------|--------------------------------------------------------------------------------------------------|---------------------|
| `Rot` | Single qubit rotations. | rotation angle |
| `AnalogRot` | Span a single qubit rotation among the entire register. | rotation angle |
| `RX`, `RY`, `RZ` | Single qubit rotations. Notice that the interaction is on and this affects the resulting gate fidelity. | rotation angle |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, RZ is not supported now.

Copy link
Collaborator Author

@madagra madagra Oct 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I forgot to remove it. I can add it, though, it should be straightforward. What do you think @vytautas-a?

| `AnalogRX`, `AnalogRY`, `AnalogRZ` | Span a single qubit rotation among the entire register. | rotation angle |
| `entangle` | Fully entangle the register. | interaction time |
| `wait` | An idle block to wait for the system to evolve for a specific time according to the interaction. | free evolution time |

## Two qubits register: Bell state

## Two qubits register
Using the `chain` block makes it easy to create a gate sequence. Here is an
example of how to create a Bell state.
The `entangle` operation uses `CZ` interactions to entangle states on the `X`
basis. We move the qubits back to the `Z` basis for the readout using a `Y`
rotation.
Using the `chain` block makes it easy to create a gate sequence. Here is an example of how to create a Bell state.
The `entangle` operation uses `CZ` interactions (according to the interaction Hamiltonian introduced in the first paragraph of this section)
to entangle states on the `X` basis. We move the qubits back to
the `Z` basis for the readout using a `Y` rotation.

```python exec="on" source="material-block" session="pulser-basic"
from qadence import chain, entangle, RY
Expand All @@ -50,60 +51,92 @@ bell_state = chain(
)
```

To convert the chain block into a pulse sequence, we define a `Register` with
two qubits and combine it to create a circuit as usual. Then we construct a `QuantumModel`
with a Pulser backend to convert it into a proper pulse sequence.
To convert the chain block into a pulse sequence, we define a `Register` with two qubits and combine it to create a circuit as usual. Then we construct a `QuantumModel` with a Pulser backend to convert it into a proper parametrized pulse sequence. Supplying the
parameter values allows to sample from the pulse sequence result.

```python exec="on" source="material-block" session="pulser-basic"
import torch
import matplotlib.pyplot as plt
from qadence import Register, QuantumCircuit, QuantumModel

register = Register(2)
circuit = QuantumCircuit(register, bell_state)
model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr')
```

To run the pulse sequence we have to provide values for the parametrized block we defined.
```python exec="on" source="material-block" result="json" session="pulser-basic"
import torch
model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr")

params = {
"wait": torch.tensor([383]), # ns
"y": torch.tensor([torch.pi/2]),
}

# Visualise the final state vector
# return the final state vector
final_vector = model.run(params)
print(final_vector)

# sample from the result state vector and plot the distribution
sample = model.sample(params, n_shots=50)[0]
print(sample)
```
```python exec="on" source="material-block" html="1" session="pulser-basic"
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.bar(sample.keys(), sample.values())
from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(fig)) # markdown-exec: hide
```


One can visualise the pulse sequence using the `assign_paramters` method.
One can visualise the pulse sequence with different parameters using the `assign_paramters` method.

```python exec="on" source="material-block" html="1" session="pulser-basic"
model.assign_parameters(params).draw(show=False)
from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
```

## Change device specifications

At variance with other backends, the Pulser one provides the concept of `Device`, borrowed from the [`pulser`](https://pulser.readthedocs.io/en/stable/) library.

A `Device` instance encapsulate all the properties defining a real neutral atoms processor, including but not limited to the maximum laser amplitude for the pulses, the maximum distance between two qubits and the maximum duration of the pulse.

`qadence` offers a simplified interface with only two devices which can be found [here][qadence.backends.pulser.devices]

* `REALISTIC` (default): device specification very similar to a real neutral atom quantum processor.
* `IDEALIZED`: ideal device which should be used only for testing purposes. It does not have any limitation in what can be run with it.

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

```python exec="on" source="material-block" session="pulser-basic"
from qadence.backends.pulser.devices import Device

register = Register(2)
circuit = QuantumCircuit(register, bell_state)

model = QuantumModel(
circuit,
backend="pulser",
diff_mode="gpsr",
configuration={"device_type": Device.IDEALIZED}
)

# alternatively directly one of the devices available in Pulser
# can also be supplied in the same way
from pulser.devices import AnalogDevice

model = QuantumModel(
circuit,
backend="pulser",
diff_mode="gpsr",
configuration={"device_type": AnalogDevice}
)
```

## Create your own gate
A big advantage of the `chain` block is it makes it easy to create complex
operations from simple ones. Take the entanglement operation as an example.

The operation consists of moving _all_ the qubits to the `X` basis having the
atoms' interaction perform a controlled-Z operation during the free evolution.
And we can easily recreate this pattern using the `AnFreeEvo` and `AnRY` blocks.
A big advantage of using the block-based interface
if `qadence` is that it makes it easy to create complex
operations from simple ones as a block composition.
Take the entanglement operation as an example.

The operation consists of moving _all_ the qubits to the `X` basis having the atoms' interaction perform a controlled-Z operation
during the free evolution. And we can easily recreate this pattern using the `wait` (corresponding to free evolution) and `AnalogRY` blocks with appropriate parameters.

```python exec="on" source="material-block" session="pulser-basic"
from qadence import AnalogRY, chain, wait
Expand Down Expand Up @@ -140,14 +173,16 @@ from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(fig)) # markdown-exec: hide
```

One can also easily access and manipulate the underlying pulse sequence.

```python exec="on" source="material-block" html="1" session="pulser-basic"
model.assign_parameters(params).draw(draw_phase_area=True, show=False)
from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
```


## Large qubits registers

The constructor `Register(n_qubits)` generates a linear register that works fine
with two or three qubits. But for the blocks we have so far, large registers
work better with a square loop layout like the following.
Expand All @@ -170,7 +205,10 @@ protocol = chain(

register = Register.square(qubits_side=2)
circuit = QuantumCircuit(register, protocol)
model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr')
model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr")

# add modulation to the pulse sequence by modifying the
# backend configuration
model.backend.backend.config.with_modulation = True

params = {
Expand All @@ -185,6 +223,9 @@ plt.xticks(rotation='vertical')
from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(fig)) # markdown-exec: hide
```

Again, let's plot the corresponding pulse sequence.

```python exec="on" source="material-block" html="1" session="pulser-basic"
model.assign_parameters(params).draw(draw_phase_area=True, show=False)
from docs import docsutils # markdown-exec: hide
Expand All @@ -195,81 +236,50 @@ print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
The gates shown here don't work with arbitrary registers since they rely on
the registered geometry to work properly.

## Working with observables

The current backend version does not support Qadence `Observables`. However, it's
still possible to use the regular Pulser simulations to calculate expected
values.

To do so, we use the `assign_parameters` property to recover the Pulser sequence.

```python exec="on" source="material-block" session="pulser-basic"
params = {
"t": torch.tensor([383]), # ns
}

built_sequence = model.assign_parameters(params)
```


Next, we create the desired observables using `Qutip` [^2].
## Digital-analog QNN circuit


```python exec="on" source="material-block" session="pulser-basic"
import qutip

zz = qutip.tensor([qutip.qeye(2), qutip.sigmaz(), qutip.qeye(2), qutip.sigmaz()])
xy = qutip.tensor([qutip.qeye(2), qutip.sigmax(), qutip.qeye(2), qutip.sigmay()])
yx = qutip.tensor([qutip.qeye(2), qutip.sigmay(), qutip.qeye(2), qutip.sigmax()])
```


We use the Pulser `Simulation` class to run the sequence and call the method `expect` over our observables.

```python exec="on" source="material-block" result="json" session="pulser-basic"
from pulser_simulation import Simulation

sim = Simulation(built_sequence)
result = sim.run()

final_result = result.expect([zz, xy + yx])
print(final_result[0][-1], final_result[1][-1])
```

We use the Pulser `Simulation` class to run the sequence and call the method
`expect` over our observables.

Here the `final_result` contains the expected values during the evolution (one
point per nanosecond). In this case, looking only at the final values, we see
the qubits `q0` and `q2` are on the *Bell-diagonal* state $\Big(|00\rangle -i |11\rangle\Big)/\sqrt{2}$.
Finally, let's put all together by constructing a digital-analog
version of a quantum neural network circuit with feature map and variational
ansatz.

```python exec="on" source="material-block" html="1" session="pulser-basic"
from qadence import fourier_feature_map, RX, RY
from qadence import kron, fourier_feature_map
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(
fourier_feature_map(1, param="x"),
entangle("t", qubit_support=(0,1)),
RY(0, "th1"),
RX(0, "th2"),
hea_one_layer,
AnalogRX(torch.pi/4)
)

register = Register(2)
circuit = QuantumCircuit(register, protocol)
model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr')
model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr")

params = {
"x": torch.tensor([0.8]),
"x": torch.tensor([0.8]), # rad
"t": torch.tensor([900]), # ns
"th1": torch.tensor([1.5]),
"th2": torch.tensor([0.9])
"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)
model.assign_parameters(params).draw(draw_phase_area=True, show=True)
from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
```

## References

[^1]: [Pulser: An open-source package for the design of pulse sequences in programmable neutral-atom arrays](https://pulser.readthedocs.io/en/stable/)
[^2]: [Qutip](https://qutip.org)
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ programs**_ with tunable interaction defined on _**arbitrary qubit register layo

Documentation can be found here: [https://pasqal-qadence.readthedocs-hosted.com/en/latest](https://pasqal-qadence.readthedocs-hosted.com/en/latest).

The library name is from music terminology: Qadence allows to compose blocks into complex quantum programs in such
a seamless way that they flow like music.

## Remarks
Quadence uses torch.float64 as the default datatype for tensors (torch.complex128 for complex tensors).

Expand Down
2 changes: 1 addition & 1 deletion qadence/backends/pulser/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
@dataclass
class Configuration(BackendConfiguration):
# device type
device_type: Device = Device.IDEALIZED
device_type: Device = Device.REALISTIC

# atomic spacing
spacing: Optional[float] = None
Expand Down
4 changes: 2 additions & 2 deletions qadence/backends/pulser/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
channel_objects=(
Rydberg.Global(
max_abs_detuning=2 * pi * 4,
max_amp=2 * pi * 3,
max_amp=2 * pi * 1.5,
clock_period=4,
min_duration=16,
max_duration=2**26,
max_duration=4000,
mod_bandwidth=16,
eom_config=RydbergEOM(
limiting_beam=RydbergBeam.RED,
Expand Down