Download

Download this file as Jupyter notebook: variables.ipynb.

Variables in programs

Variables are a central part of Keysight’s Quantum Control System and represent classical inputs and outputs of quantum programs. Due to the short time scales imposed by quantum decoherence, these variables are processed on field-programmable gate arrays (FPGAs) and application-specific integrated circuits (ASICs) that are integrated into the control hardware. This integration allows for real-time readouts and updates of variables, and enables decision logic based on measurement outcomes.

There are three types of scalars that need to be processed in quantum computations:

  1. Integers, e.g., to describe the classical output of a measurement.

  2. Real numbers, e.g., to describe the amplitude, frequency or duration of waveforms.

  3. Complex numbers, e.g., to describe the integrated result of an analog measurement.

Each of these scalar types can be grouped into arrays. We then want to be able to do the following:

  1. Define the inputs and outputs of programs.

  2. Define variables and use them to define multiple instructions such that whenever the value of the variable changes, it is changed in all instructions that use it.

  3. Define one or more variables and use their values at runtime to determine which instruction to execute.

  4. Define classical processing of arrays, slices thereof, and scalars that are to be performed during the execution of a quantum program.

To support the above, Keysight’s Quantum Control System includes the Scalar and Array classes, which encapsulate a variable name, an underlying scalar dtype, and a value that may be uninitialized. The value can be set on initialization and/or changed afterwards. When the value is set, it will be coerced to the underlying dtype if possible and raise an error if it is not possible.

The default dtype is complex because it is the broadest scalar type.

[2]:
import keysight.qcs as qcs

# define a constant waveform and a delay that both have the same uninitialized duration
duration = qcs.Scalar("duration", dtype=float)

assert duration.name == "duration"
assert duration.dtype is float

const_pulse = qcs.DCWaveform(duration, qcs.ConstantEnvelope(), amplitude=1)
delay = qcs.Delay(duration)

assert const_pulse.duration.value is None
assert delay.duration.value is None

Setting the duration updates both const_pulse and delay:

[3]:
duration.value = 1e-6
assert const_pulse.duration.value == 1e-6
assert delay.duration.value == 1e-6

Setting a variable to an invalid type raises a TypeError and leaves the value unchanged:

[4]:
try:
    duration.value = 1j
except TypeError:
    pass
assert duration.value == 1e-6

We can also create read-only variables that effectively act as constants in an experiment. Trying to set the value of a read-only variable raises a ValueError and leaves it unchanged:

[5]:
read_only = qcs.Scalar("duration", value=1, dtype=int, read_only=True)
try:
    read_only.value = 2
except ValueError:
    pass
assert read_only.value == 1

Arrays

An Array has the same name, dtype, and value properties as a Scalar, but adds a shape property which specifies the number of dimensions and the size of each dimension. The dimensions of an undefined size can be specified as None and will be inferred when the value is set. The shape and value cannot both be passed as arguments as they are redundant and must be specified as keyword arguments to avoid ambiguity.

[6]:
# specify an empty 1D array with two rows
array = qcs.Array("array", shape=(2,))
assert array.value is None

# setting the value will set any unknown sizes
array.value = [1, 2]
assert array.shape == (2,)
assert all(array.value == [1, 2])

We can access individual elements or regularly spaced subsets in an Array using the same syntax as basic NumPy slicing. This slicing syntax extends Python slicing syntax to multi-dimensional arrays.

Note

We only support basic NumPy slicing because it returns a view of the underlying data, whereas advanced NumPy slicing creates copies.

[7]:
# Create a 2x3 array with integer values
array = qcs.Array("array", value=[[1, 2], [3, 4], [4, 5]])

# retrieve the first row
assert (array[0].value == [1, 2]).all()

# retrieve the first column
assert (array[:, 0].value == [1, 3, 4]).all()

Slices are valid even if the value of the Array being sliced is None. Setting the value of the Array will automatically update the value of all its slices.

Note

To avoid potential edge cases, an Array can only be sliced if all elements of its shape are specified.

[8]:
# create an empty array with two rows
array = qcs.Array("array", shape=(2,), dtype=int)

# store the first value in a slice
arslice = array[0]
assert arslice.value is None

# populate the array with values
array.value = [0, 1]

# the slice value is now updated
assert arslice.value == 0

Likewise, setting the value of a slice will also update the Array's value:

[9]:
array = qcs.Array("array", value=[[0, 1]])

# store the first value in a slice
arslice = array[0, 0]
assert arslice.value == 0

# change the value of the slice
arslice.value = 2

# the array value is now updated
assert (array.value == [[2, 1]]).all()

Implicit variables

For convenient and consistent interfaces, we allow scalar arguments to be specified by a number and implicitly cast them to a read-only variable (or constant) with an empty string as its name, where the dtype depends on the class.

[10]:
delay = qcs.Delay(1e-6)
assert delay.duration.value == 1e-6

Dependent variables

Dependent variables can be specified as arithmetic expressions of existing variables.

[11]:
var = qcs.Scalar("var", dtype=int)
result = 0.5 * var + 1

Note that such an expression is not a temporary variable assignment, but rather a stationary definition. The value of result is always set using the value of var.

[12]:
# the value of result is None before var is used
assert result.value is None

# result updates accordingly with var
var.value = 3
assert result.value == 2.5

Expressions can use any combination of scalars, arrays, and slices. Array operations are element-wise.

[13]:
coeff = qcs.Scalar("c", value=0.5, dtype=float)
res1 = qcs.Array("res1", value=[5, 2], dtype=int)
res2 = qcs.Array("res2", value=[2, 1], dtype=int)
res3 = qcs.Array("res3", value=[0, 1, 7], dtype=int)

result = 2 * coeff * res1[0] * res2 + res3[:2] / res2 + 5

assert (result.value == [15, 11]).all()

Dependent variables are convenient as instruction arguments that depend on a common variable.

[14]:
# define an amplitude variable that may update over the course of the program
amp = qcs.Scalar("amp", dtype=float)

# multiple pulses depend on the variable amplitude
pulse1 = qcs.DCWaveform(80e-9, qcs.GaussianEnvelope(), 0.98 * amp)
pulse2 = qcs.DCWaveform(80e-9, qcs.GaussianEnvelope(), 0.90 * amp)

Hashing

Python makes heavy use of hashable collections, such as sets and dictionaries, to enable lookups to be performed with an average-case cost that is independent of the collection’s size. For variables and any classes that contain them as attributes to be able to be inserted in a hashable collection, they need to define hash and equality functions. There are two requirements for a hash function:

  1. The hash must not change over the lifetime of an object.

  2. The hash of two equal objects must be the same.

To satisfy these requirements, we make the hash of a variable independent of the variable’s value so that changing that value does not affect the hash of the variable.

[15]:
var = qcs.Scalar("var")
initial_hash = hash(var)
var.value = 1e-6
assert hash(var) == initial_hash

Download

Download this file as Jupyter notebook: variables.ipynb.

On this page