Skip to content

Rydberg

This simulates the operation of a Rydberg tweezer array, i.e. a line of qubit that might be entangled through a Rydberg blockade. It can be deployed on qlued as described on their documentation. Below you can find the API of the simulator.

Config

In this module we define all the configuration parameters for the Rydberg package.

No simulation is performed here. The entire logic is implemented in the spooler.py module.

BarrierInstruction

Bases: BaseModel

The barrier instruction. As each instruction it requires the

Attributes:

Name Type Description
name Literal['barrier']

The string to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]], Field(min_length=0, max_length=N_MAX_WIRES)]

The wires on which the instruction should be applied so the indices should be between 0 and NUM_WIRES-1

params Annotated[List[float], Field(max_length=0)]

has to be empty

Source code in rydberg/config.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
class BarrierInstruction(BaseModel):
    """
    The barrier instruction. As each instruction it requires the

    Attributes:
        name: The string to identify the instruction
        wires: The wires on which the instruction should be applied
            so the indices should be between 0 and NUM_WIRES-1
        params: has to be empty
    """

    name: Literal["barrier"]
    wires: Annotated[
        List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]],
        Field(min_length=0, max_length=N_MAX_WIRES),
    ]
    params: Annotated[List[float], Field(max_length=0)]

MeasureInstruction

Bases: BaseModel

The measure instruction.

Attributes:

Name Type Description
name Literal['measure']

How to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]], Field(min_length=1, max_length=1)]

Exactly one wire has to be given.

params Annotated[List[float], Field(max_length=0)]

Has to be empty

Source code in rydberg/config.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class MeasureInstruction(BaseModel):
    """
    The measure instruction.

    Attributes:
        name: How to identify the instruction
        wires: Exactly one wire has to be given.
        params: Has to be empty
    """

    name: Literal["measure"]
    wires: Annotated[
        List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]],
        Field(min_length=1, max_length=1),
    ]
    params: Annotated[List[float], Field(max_length=0)]

RlxInstruction

Bases: GateInstruction

The rlx instruction. As each instruction it requires the following attributes

Attributes:

Name Type Description
name Literal['rlx']

The string to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]], Field(min_length=1, max_length=1)]

The wire on which the instruction should be applied so the indices should be between 0 and N_MAX_WIRES-1

params Annotated[List[Annotated[float, Field(ge=0, le=2 * pi)]], Field(min_length=1, max_length=1)]

has to be empty

Source code in rydberg/config.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class RlxInstruction(GateInstruction):
    """
    The rlx instruction. As each instruction it requires the following attributes

    Attributes:
        name: The string to identify the instruction
        wires: The wire on which the instruction should be applied
            so the indices should be between 0 and N_MAX_WIRES-1
        params: has to be empty
    """

    name: Literal["rlx"] = "rlx"
    wires: Annotated[
        List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]],
        Field(min_length=1, max_length=1),
    ]
    params: Annotated[
        List[Annotated[float, Field(ge=0, le=2 * np.pi)]],
        Field(min_length=1, max_length=1),
    ]

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "omega"
    description: str = "Evolution under Rlx"
    # TODO: This should become most likely a type that is then used for the enforcement of the wires.
    coupling_map: List = [[0], [1], [2], [3], [4]]
    qasm_def: str = "gate rlx(omega) {}"

RlzInstruction

Bases: GateInstruction

The rlz instruction. As each instruction it requires the

Attributes:

Name Type Description
name Literal['rlz']

The string to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]], Field(min_length=1, max_length=1)]

The wire on which the instruction should be applied so the indices should be between 0 and N_MAX_WIRES-1

params Annotated[List[Annotated[float, Field(ge=0, le=2 * pi)]], Field(min_length=1, max_length=1)]

has to be empty

Source code in rydberg/config.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class RlzInstruction(GateInstruction):
    """
    The rlz instruction. As each instruction it requires the

    Attributes:
        name: The string to identify the instruction
        wires: The wire on which the instruction should be applied
            so the indices should be between 0 and N_MAX_WIRES-1
        params: has to be empty
    """

    name: Literal["rlz"] = "rlz"
    wires: Annotated[
        List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]],
        Field(min_length=1, max_length=1),
    ]
    params: Annotated[
        List[Annotated[float, Field(ge=0, le=2 * np.pi)]],
        Field(min_length=1, max_length=1),
    ]

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "delta"
    description: str = "Evolution under the Rlz gate"
    # TODO: This should become most likely a type that is then used for the enforcement of the wires.
    coupling_map: List = [[0], [1], [2], [3], [4]]
    qasm_def: str = "gate rlz(delta) {}"

RydbergBlockInstruction

Bases: GateInstruction

The Rydberg blockade instruction. As each instruction it requires the

Attributes:

Name Type Description
name Literal['rydberg_block']

The string to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]], Field(min_length=2, max_length=N_MAX_WIRES)]

The wire on which the instruction should be applied so the indices should be between 0 and N_MAX_WIRES-1

params Annotated[List[Annotated[float, Field(ge=0, le=2 * pi)]], Field(min_length=1, max_length=1)]

has to be empty

Source code in rydberg/config.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class RydbergBlockInstruction(GateInstruction):
    """
    The Rydberg blockade instruction. As each instruction it requires the

    Attributes:
        name: The string to identify the instruction
        wires: The wire on which the instruction should be applied
            so the indices should be between 0 and N_MAX_WIRES-1
        params: has to be empty
    """

    name: Literal["rydberg_block"] = "rydberg_block"
    wires: Annotated[
        List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]],
        Field(min_length=2, max_length=N_MAX_WIRES),
    ]
    params: Annotated[
        List[Annotated[float, Field(ge=0, le=2 * np.pi)]],
        Field(min_length=1, max_length=1),
    ]

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "phi"
    description: str = "Apply the Rydberg blockade over the whole array"
    # TODO: This should become most likely a type that is then used for the enforcement of the wires.
    coupling_map: List = [[0, 1, 2, 3, 4]]
    qasm_def: str = "gate rydberg_block(phi) {}"

RydbergExperiment

Bases: BaseModel

The class that defines the Rydberg experiments. Each of those RydbergExperiments is executed on a RydbergSpooler.

Source code in rydberg/config.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class RydbergExperiment(BaseModel):
    """
    The class that defines the Rydberg experiments. Each of those
    `RydbergExperiment`s is executed on a `RydbergSpooler`.
    """

    wire_order: Literal["interleaved", "sequential"] = "sequential"
    # mypy keeps throwing errors here because it does not understand the type.
    # not sure how to fix it, so we leave it as is for the moment
    # HINT: Annotated does not work
    shots: Annotated[int, Field(gt=0, le=N_MAX_SHOTS)]
    num_wires: Annotated[int, Field(ge=1, le=N_MAX_WIRES)]
    instructions: List[list]
    seed: Optional[int] = None

RydbergFullInstruction

Bases: GateInstruction

The time evolution under the global Hamiltonian. It does not allow for any local control.

Attributes:

Name Type Description
name Literal['rydberg_full']

The string to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]], Field(min_length=2, max_length=N_MAX_WIRES)]

The wire on which the instruction should be applied so the indices should be between 0 and N_MAX_WIRES-1

params Annotated[List[Annotated[float, Field(ge=0, le=5000000.0 * pi)]], Field(min_length=3, max_length=3)]

Define the parameter for RX, RZand RydbergBlock in this order

Source code in rydberg/config.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class RydbergFullInstruction(GateInstruction):
    """
    The time evolution under the global Hamiltonian. It does not allow for any local control.

    Attributes:
        name: The string to identify the instruction
        wires: The wire on which the instruction should be applied
            so the indices should be between 0 and N_MAX_WIRES-1
        params: Define the parameter for `RX`, `RZ`and `RydbergBlock` in this order
    """

    name: Literal["rydberg_full"] = "rydberg_full"
    wires: Annotated[
        List[Annotated[int, Field(ge=0, le=N_MAX_WIRES - 1)]],
        Field(min_length=2, max_length=N_MAX_WIRES),
    ]
    params: Annotated[
        List[Annotated[float, Field(ge=0, le=5e6 * np.pi)]],
        Field(min_length=3, max_length=3),
    ]

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "omega, delta, phi"
    description: str = "Apply the Rydberg and Rabi coupling over the whole array."
    # TODO: This should become most likely a type that is then used for the enforcement of the wires.
    coupling_map: List = [[0, 1, 2, 3, 4]]
    qasm_def: str = "gate rydberg_full(omega, delta, phi) {}"

Simulation code

The module that contains all the necessary logic for the Rydberg simulator. It has to implement the code that is executed for all the instructions that we defined in the config.py file.

gen_circuit(exp_name, json_dict)

The function the creates the instructions for the circuit.

json_dict: The list of instructions for the specific run.

Source code in rydberg/spooler.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def gen_circuit(exp_name: str, json_dict: ExperimentalInputDict) -> ExperimentDict:
    """The function the creates the instructions for the circuit.

    json_dict: The list of instructions for the specific run.
    """
    ins_list = json_dict.instructions
    n_shots = json_dict.shots
    if json_dict.seed is not None:
        np.random.seed(json_dict.seed)

    n_wires = json_dict.num_wires
    spin_per_wire = 1 / 2 * np.ones(n_wires)

    dim_per_wire = 2 * spin_per_wire + np.ones(n_wires)
    dim_per_wire = dim_per_wire.astype(int)
    dim_hilbert = np.prod(dim_per_wire)

    # we will need a list of local spin operators as their dimension can change
    # on each wire
    lx_list = []
    ly_list = []
    lz_list = []
    nocc_list = []
    spin_length = 1 / 2
    qudit_range = np.arange(spin_length, -(spin_length + 1), -1)
    lx = csc_matrix(
        1
        / 2
        * diags(
            [
                np.sqrt(
                    [
                        (spin_length - m + 1) * (spin_length + m)
                        for m in qudit_range[:-1]
                    ]
                ),
                np.sqrt(
                    [(spin_length + m + 1) * (spin_length - m) for m in qudit_range[1:]]
                ),
            ],
            [-1, 1],
        )
    )
    ly = csc_matrix(
        1
        / (2 * 1j)
        * diags(
            [
                np.sqrt(
                    [
                        (spin_length - m + 1) * (spin_length + m)
                        for m in qudit_range[:-1]
                    ]
                ),
                -1
                * np.sqrt(
                    [(spin_length + m + 1) * (spin_length - m) for m in qudit_range[1:]]
                ),
            ],
            [-1, 1],
        )
    )

    lz = csc_matrix(diags([qudit_range], [0]))
    # nocc = csc_matrix(diags([qudit_range + 1 / 2], [0]))
    nocc = csc_matrix(diags([-qudit_range + 1 / 2], [0]))

    for i1 in np.arange(0, n_wires):
        # let's put together spin matrices
        lx_list.append(op_at_wire(lx, i1, list(dim_per_wire)))
        ly_list.append(op_at_wire(ly, i1, list(dim_per_wire)))
        lz_list.append(op_at_wire(lz, i1, list(dim_per_wire)))
        nocc_list.append(op_at_wire(nocc, i1, list(dim_per_wire)))

    int_matrix = csc_matrix((dim_hilbert, dim_hilbert))
    for i1 in np.arange(0, n_wires):
        for i2 in np.arange(i1 + 1, n_wires):
            int_matrix = (
                int_matrix + nocc_list[i1].dot(nocc_list[i2]) / np.abs(i1 - i2) ** 6
            )

    initial_state = 1j * np.zeros(dim_per_wire[0])
    initial_state[0] = 1 + 1j * 0
    psi = sparse.csc_matrix(initial_state)
    for i1 in np.arange(1, len(dim_per_wire)):
        initial_state = 1j * np.zeros(dim_per_wire[i1])
        initial_state[0] = 1 + 1j * 0
        psi = sparse.kron(psi, initial_state)
    psi = psi.T

    measurement_indices = []
    shots_array = []
    for inst in ins_list:
        if inst.name == "rlx":
            position = inst.wires[0]
            theta = inst.params[0]
            psi = expm_multiply(-1j * theta * lx_list[position], psi)
        if inst.name == "rlz":
            position = inst.wires[0]
            theta = inst.params[0]
            psi = expm_multiply(-1j * theta * lz_list[position], psi)
        if inst.name == "rydberg_block":
            # apply gate on all qubits
            theta = inst.params[0]
            psi = expm_multiply(-1j * theta * int_matrix, psi)
        if inst.name == "rydberg_full":
            omega, delta, phi = inst.params
            u_full = csc_matrix((dim_hilbert, dim_hilbert))
            # first the RX
            for lxi in lx_list:
                u_full = u_full + omega * lxi
            # next the RZ
            for lzi in lz_list:
                u_full = u_full + delta * lzi
            # end the blockade
            u_full = u_full + phi * int_matrix
            psi = expm_multiply(-1j * u_full, psi)
        if inst.name == "measure":
            measurement_indices.append(inst.wires[0])
    if measurement_indices:
        # the following filters out the results for the indices we prefer.
        probs = np.squeeze(abs(psi.toarray()) ** 2)
        result_ind = np.random.choice(dim_hilbert, p=probs, size=n_shots)
        measurements = np.zeros((n_shots, len(measurement_indices)), dtype=int)
        for i1 in range(n_shots):
            observed = np.unravel_index(result_ind[i1], dim_per_wire)
            # TODO these types are messed up for the moment
            # as ususal we add an ignore until this gets back to bite us in the ...
            # but it simply to tough to find out where the typing goes wrong right now.
            observed = np.array(observed)  # type: ignore
            measurements[i1, :] = observed[measurement_indices]  # type: ignore
        shots_array = measurements.tolist()

    exp_sub_dict = create_memory_data(shots_array, exp_name, n_shots, ins_list)
    return exp_sub_dict

op_at_wire(op, pos, dim_per_wire)

Applies an operation onto the wire and provides unitaries on the other wires. Basically this creates the nice tensor products.

Parameters:

Name Type Description Default
op matrix

The operation that should be applied.

required
pos int

The wire onto which the operation should be applied.

required
dim_per_wire int

What is the local Hilbert space of each wire.

required

Returns:

Type Description
csc_matrix

The tensor product matrix.

Source code in rydberg/spooler.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def op_at_wire(op: csc_matrix, pos: int, dim_per_wire: List[int]) -> csc_matrix:
    """
    Applies an operation onto the wire and provides unitaries on the other wires.
    Basically this creates the nice tensor products.

    Args:
        op (matrix): The operation that should be applied.
        pos (int): The wire onto which the operation should be applied.
        dim_per_wire (int): What is the local Hilbert space of each wire.

    Returns:
        The tensor product matrix.
    """
    # There are two cases the first wire can be the identity or not
    if pos == 0:
        res = op
    else:
        res = csc_matrix(identity(dim_per_wire[0]))
    # then loop for the rest
    for i1 in np.arange(1, len(dim_per_wire)):
        temp = csc_matrix(identity(dim_per_wire[i1]))
        if i1 == pos:
            temp = op
        res = sparse.kron(res, temp)

    return res

Comments