Devices and Virtual Devices

[1]:
import numpy as np
from dataclasses import replace
from pulser.devices import Device, VirtualDevice, AnalogDevice, MockDevice
from pulser.channels import Rydberg, Raman, Microwave
from pulser import Pulse, Sequence, Register

Physical devices

To perform any computation using Pulser, it is necessary to choose a device. For convenience, some examples of typical physical devices are included and can be accessed via pulser.devices.
These devices are instances of the Device class. They are constrained by physical considerations and all their parameters are defined.

As an example, we present below the specifications of the physical device AnalogDevice, which can be accessed via the Device.print_specs() method.

[2]:
# Display AnalogDevice's specifications
AnalogDevice.print_specs()
---------------------------
AnalogDevice Specifications
---------------------------

Register parameters:
 - Dimensions: 2D
 - Rydberg level: 60
 - Maximum number of atoms: 25
 - Maximum distance from origin: 35 μm
 - Minimum distance between neighbouring atoms: 5 μm
 - Maximum layout filling fraction: 0.5
 - SLM Mask: No
 - Maximum sequence duration: 4000 ns

Channels:
 - 'rydberg_global': Rydberg.Global(Max Absolute Detuning: 125.66370614359172 rad/µs, Max Amplitude: 12.566370614359172 rad/µs, Clock period: 4 ns, Minimum pulse duration: 16 ns, Maximum pulse duration: 100000000 ns, Modulation Bandwidth: 8 MHz, Basis: 'ground-rydberg')

Virtual Devices

Converting a Device into a Virtual Device

However, we sometimes want to perform the computations on a more permissive device, a device that would have more dimensions or more atoms for instance, or more types of channels. This can be done on an emulator behaving like a device. The VirtualDevice class is useful to define such an emulator, a virtual device.

Let’s start by configuring a virtual device having the same parameters as AnalogDevice. To do this, we use the Device.to_virtual() method that creates a virtual device from a physical one.

[3]:
# Converting the Device object in a VirtualDevice object
VirtualAnalog = AnalogDevice.to_virtual()
print(VirtualAnalog)
VirtualDevice(name='AnalogDevice', dimensions=2, rydberg_level=60, min_atom_distance=5, max_atom_num=25, max_radial_distance=35, interaction_coeff_xy=None, supports_slm_mask=False, max_layout_filling=0.5, max_sequence_duration=4000, max_runs=None, requires_layout=True, reusable_channels=False, channel_ids=('rydberg_global',), channel_objects=(Rydberg.Global(Max Absolute Detuning: 125.66370614359172 rad/µs, Max Amplitude: 12.566370614359172 rad/µs, Clock period: 4 ns, Minimum pulse duration: 16 ns, Maximum pulse duration: 100000000 ns, Modulation Bandwidth: 8 MHz, Basis: 'ground-rydberg'),), dmm_objects=(), default_noise_model=None)

Changing parameters of a virtual device with dataclasses.replace()

As proposed earlier, a virtual device gives us the possibility to modify the parameters of a physical device. The function dataclasses.replace() can be used to create a new VirtualDevice having some parameters changed.
For example, simulations can be run on a virtual device having the same properties as AnalogDevice but allowing working in 3 dimensions.
[4]:
# Adding a dimension to the emulator
VirtualAnalog3D = replace(VirtualAnalog, dimensions=3)
print(VirtualAnalog3D)
VirtualDevice(name='AnalogDevice', dimensions=3, rydberg_level=60, min_atom_distance=5, max_atom_num=25, max_radial_distance=35, interaction_coeff_xy=None, supports_slm_mask=False, max_layout_filling=0.5, max_sequence_duration=4000, max_runs=None, requires_layout=True, reusable_channels=False, channel_ids=('rydberg_global',), channel_objects=(Rydberg.Global(Max Absolute Detuning: 125.66370614359172 rad/µs, Max Amplitude: 12.566370614359172 rad/µs, Clock period: 4 ns, Minimum pulse duration: 16 ns, Maximum pulse duration: 100000000 ns, Modulation Bandwidth: 8 MHz, Basis: 'ground-rydberg'),), dmm_objects=(), default_noise_model=None)

Unrealistic parameters of a virtual device

Among the parameters defined in a VirtualDevice, some are called unrealistic as they do not refer to something physically implementable.
For instance, it is possible to change the Rydberg level used in the simulation or to call a channel multiple times in the same sequence. Modifying the Rydberg level has an impact on the coefficient \(C_6\) (see here for the correspondance between the Rydberg level and \(C_6\) coefficient).
[5]:
# Changing the Rydberg level
VirtualAnalog3D.change_rydberg_level(61)
print(VirtualAnalog3D)
VirtualDevice(name='AnalogDevice', dimensions=3, rydberg_level=61, min_atom_distance=5, max_atom_num=25, max_radial_distance=35, interaction_coeff_xy=None, supports_slm_mask=False, max_layout_filling=0.5, max_sequence_duration=4000, max_runs=None, requires_layout=True, reusable_channels=False, channel_ids=('rydberg_global',), channel_objects=(Rydberg.Global(Max Absolute Detuning: 125.66370614359172 rad/µs, Max Amplitude: 12.566370614359172 rad/µs, Clock period: 4 ns, Minimum pulse duration: 16 ns, Maximum pulse duration: 100000000 ns, Modulation Bandwidth: 8 MHz, Basis: 'ground-rydberg'),), dmm_objects=(), default_noise_model=None)
[6]:
# Enable the multiple declaration of a channel in a sequence
VirtualAnalog3D = replace(VirtualAnalog3D, reusable_channels=True)
# Creating a square register
reg = Register.square(4, spacing=5)  # 4x4 array with atoms 5 um apart
# Building a sequence with the register and the virtual device
seq = Sequence(reg, VirtualAnalog3D)
# Declare twice the channel "rydberg_global"
seq.declare_channel("ch0", "rydberg_global")
seq.declare_channel("ch1", "rydberg_global")
# Show the declared channels
print(seq.declared_channels)
{'ch0': Rydberg.Global(Max Absolute Detuning: 125.66370614359172 rad/µs, Max Amplitude: 12.566370614359172 rad/µs, Clock period: 4 ns, Minimum pulse duration: 16 ns, Maximum pulse duration: 100000000 ns, Modulation Bandwidth: 8 MHz, Basis: 'ground-rydberg'), 'ch1': Rydberg.Global(Max Absolute Detuning: 125.66370614359172 rad/µs, Max Amplitude: 12.566370614359172 rad/µs, Clock period: 4 ns, Minimum pulse duration: 16 ns, Maximum pulse duration: 100000000 ns, Modulation Bandwidth: 8 MHz, Basis: 'ground-rydberg')}

Building your own virtual device

More generally, the VirtualDevice class is more permissive than the Device class. In fact, some parameters can be left undefined and simpler devices can be built with a VirtualDevice instance. A virtual device only needs a name, a dimension and a rydberg_level to be initialized.

[7]:
BasicVirtualDevice = VirtualDevice(
    name="BasicMockDevice",
    dimensions=2,
    rydberg_level=61,
)
print(BasicVirtualDevice)
VirtualDevice(name='BasicMockDevice', dimensions=2, rydberg_level=61, min_atom_distance=0, max_atom_num=None, max_radial_distance=None, interaction_coeff_xy=None, supports_slm_mask=True, max_layout_filling=0.5, max_sequence_duration=None, max_runs=None, requires_layout=False, reusable_channels=True, channel_ids=(), channel_objects=(), dmm_objects=(DMM.Global(Max Absolute Detuning: None, Max Amplitude: 0, Clock period: 1 ns, Minimum pulse duration: 1 ns, Maximum pulse duration: 100000000 ns, Basis: 'ground-rydberg'),), default_noise_model=None)

Defining the channels of your device

Nevertheless, to perform computations, channels have to be defined. The channels enabled on the device are defined in channel_objects. Their IDs can be defined in channel_ids, but if no IDs are provided, they will be automatically generated as {channeltype}_adressing.
For example, the Rydberg.Global channel is automatically named rydberg_global in the specifications of AnalogDevice.
[8]:
# This basic device can be used for digital quantum computing
DigitalQCVirtualDevice = replace(
    BasicVirtualDevice,
    channel_ids=(
        "ryd_loc",
        "ram_loc",
    ),
    channel_objects=(
        Rydberg.Local(None, None, max_duration=None),
        Raman.Local(None, None, max_duration=None),
    ),
)
print(DigitalQCVirtualDevice)
VirtualDevice(name='BasicMockDevice', dimensions=2, rydberg_level=61, min_atom_distance=0, max_atom_num=None, max_radial_distance=None, interaction_coeff_xy=None, supports_slm_mask=True, max_layout_filling=0.5, max_sequence_duration=None, max_runs=None, requires_layout=False, reusable_channels=True, channel_ids=('ryd_loc', 'ram_loc'), channel_objects=(Rydberg.Local(Max Absolute Detuning: None, Max Amplitude: None, Minimum retarget time: 0 ns, Fixed retarget time: 0 ns, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'ground-rydberg'), Raman.Local(Max Absolute Detuning: None, Max Amplitude: None, Minimum retarget time: 0 ns, Fixed retarget time: 0 ns, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'digital')), dmm_objects=(DMM.Global(Max Absolute Detuning: None, Max Amplitude: 0, Clock period: 1 ns, Minimum pulse duration: 1 ns, Maximum pulse duration: 100000000 ns, Basis: 'ground-rydberg'),), default_noise_model=None)

A built-in example of a virtual device: the MockDevice

Actually, there exists a virtual device having all the channels already implemented, with no constraints on the number of atoms, the distance between them. This virtual device is the MockDevice.

[9]:
MyMockDevice = replace(MockDevice, name="MyMockDevice")
print(MyMockDevice)
VirtualDevice(name='MyMockDevice', dimensions=3, rydberg_level=70, min_atom_distance=0.0, max_atom_num=None, max_radial_distance=None, interaction_coeff_xy=3700.0, supports_slm_mask=True, max_layout_filling=0.5, max_sequence_duration=None, max_runs=None, requires_layout=False, reusable_channels=True, channel_ids=('rydberg_global', 'rydberg_local', 'raman_global', 'raman_local', 'mw_global'), channel_objects=(Rydberg.Global(Max Absolute Detuning: None, Max Amplitude: None, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'ground-rydberg'), Rydberg.Local(Max Absolute Detuning: None, Max Amplitude: None, Minimum retarget time: 0 ns, Fixed retarget time: 0 ns, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'ground-rydberg'), Raman.Global(Max Absolute Detuning: None, Max Amplitude: None, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'digital'), Raman.Local(Max Absolute Detuning: None, Max Amplitude: None, Minimum retarget time: 0 ns, Fixed retarget time: 0 ns, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'digital'), Microwave.Global(Max Absolute Detuning: None, Max Amplitude: None, Clock period: 1 ns, Minimum pulse duration: 1 ns, Basis: 'XY')), dmm_objects=(DMM.Global(Max Absolute Detuning: None, Max Amplitude: 0, Clock period: 1 ns, Minimum pulse duration: 1 ns, Maximum pulse duration: 100000000 ns, Basis: 'ground-rydberg'),), default_noise_model=None)