Skip to content

Fermionic tweezer

This simulates the hopping of fermions in a fermionic tweezer. It can be used in qiskit-cold-atom as described in this tutorial. Below you can find the API of the simulator.

Config

In this module we define all the configuration parameters for the fermions 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=NUM_WIRES - 1)]], Field(min_length=0, max_length=NUM_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 fermions/config.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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=NUM_WIRES - 1)]],
        Field(min_length=0, max_length=NUM_WIRES),
    ]
    params: Annotated[List[float], Field(max_length=0)]

FermionExperiment

Bases: BaseModel

The class that defines the fermion experiments

Source code in fermions/config.py
149
150
151
152
153
154
155
156
157
158
159
class FermionExperiment(BaseModel):
    """
    The class that defines the fermion experiments
    """

    wire_order: Literal["interleaved"]
    # we use the Annotated notation to make mypy happy with constrained types
    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

HopInstruction

Bases: GateInstruction

The instruction that applies the hopping gate.

Attributes:

Name Type Description
name Literal['fhop']

How to identify the instruction

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

Exactly four wires have to be given.

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

between 0 and 2 pi

coupling_map List

contains all the allowed configurations. Currently not used

Source code in fermions/config.py
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
class HopInstruction(GateInstruction):
    """
    The instruction that applies the hopping gate.

    Attributes:
        name: How to identify the instruction
        wires: Exactly four wires have to be given.
        params: between 0 and 2 pi
        coupling_map: contains all the allowed configurations. Currently not used
    """

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

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "j_i"
    description: str = "hopping of atoms to neighboring tweezers"
    # 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],
        [2, 3, 4, 5],
        [4, 5, 6, 7],
        [0, 1, 2, 3, 4, 5, 6, 7],
    ]

IntInstruction

Bases: GateInstruction

The instruction that applies the interaction gate.

Attributes:

Name Type Description
name Literal['fint']

How to identify the instruction

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

Exactly one wire has to be given.

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

Has to be empty

Source code in fermions/config.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
class IntInstruction(GateInstruction):
    """
    The instruction that applies the interaction gate.

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

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

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "u"
    description: str = "on-site interaction of atoms of opposite spin state"
    # 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, 5, 6, 7]]

LoadMeasureInstruction

Bases: BaseModel

The load or measure instruction.

Attributes:

Name Type Description
name Literal['load', 'measure']

How to identify the instruction

wires Annotated[List[Annotated[int, Field(ge=0, le=NUM_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 fermions/config.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class LoadMeasureInstruction(BaseModel):
    """
    The load or measure instruction.

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

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

PhaseInstruction

Bases: GateInstruction

The instruction that applies the interaction gate.

Attributes:

Name Type Description
name Literal['fphase']

How to identify the instruction

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

Exactly one wire has to be given.

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

Has to be empty

Source code in fermions/config.py
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
class PhaseInstruction(GateInstruction):
    """
    The instruction that applies the interaction gate.

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

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

    # a string that is sent over to the config dict and that is necessary for compatibility with QISKIT.
    parameters: str = "mu_i"
    description: str = (
        "Applying a local phase to tweezers through an external potential"
    )
    # 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, 5], [6, 7], [0, 1, 2, 3, 4, 5, 6, 7]]

Simulation code

The module that contains all the necessary logic for the fermions.

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 fermions/spooler.py
 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
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)
    tweezer_len = 4  # length of the tweezer array
    n_states = 2 ** (2 * tweezer_len)

    # create all the raising and lowering operators
    lattice_length = 2 * tweezer_len
    lowering_op_list = []
    for i in range(lattice_length):
        lowering_op_list.append(jordan_wigner_transform(i, lattice_length))

    number_operators = []
    for i in range(lattice_length):
        number_operators.append(lowering_op_list[i].T.conj().dot(lowering_op_list[i]))
    # interaction Hamiltonian
    h_int = 0 * number_operators[0]
    for ii in range(tweezer_len):
        spindown_ind = 2 * ii
        spinup_ind = 2 * ii + 1
        h_int += number_operators[spindown_ind].dot(number_operators[spinup_ind])

    # work our way through the instructions
    psi = 1j * np.zeros(n_states)
    psi[0] = 1
    measurement_indices = []
    shots_array = []
    # pylint: disable=C0200
    # Fix this pylint issue whenever you have time, but be careful !
    for i in range(len(ins_list)):
        inst = ins_list[i]
        if inst.name == "load":
            latt_ind = inst.wires[0]
            psi = np.dot(lowering_op_list[latt_ind].T, psi)
        if inst.name == "fhop":
            # the first two indices are the starting points
            # the other two indices are the end points
            latt_ind = inst.wires
            theta = inst.params[0]
            # couple
            h_hop = lowering_op_list[latt_ind[0]].T.dot(lowering_op_list[latt_ind[2]])
            h_hop += lowering_op_list[latt_ind[2]].T.dot(lowering_op_list[latt_ind[0]])
            h_hop += lowering_op_list[latt_ind[1]].T.dot(lowering_op_list[latt_ind[3]])
            h_hop += lowering_op_list[latt_ind[3]].T.dot(lowering_op_list[latt_ind[1]])
            u_hop = expm(-1j * theta * h_hop)
            psi = np.dot(u_hop, psi)
        if inst.name == "fint":
            # the first two indices are the starting points
            # the other two indices are the end points
            theta = inst.params[0]
            u_int = expm(-1j * theta * h_int)
            psi = np.dot(u_int, psi)
        if inst.name == "fphase":
            # the first two indices are the starting points
            # the other two indices are the end points
            h_phase = 0 * number_operators[0]
            for ii in inst.wires:
                h_phase += number_operators[ii]
            theta = inst.params[0]
            u_phase = expm(-1j * theta * h_phase)
            psi = np.dot(u_phase, psi)
        if inst.name == "measure":
            measurement_indices.append(inst.wires[0])

    # only give back the needed measurments
    if measurement_indices:
        probs = np.abs(psi) ** 2
        result_inds = np.random.choice(np.arange(n_states), p=probs, size=n_shots)

        measurements = np.zeros((n_shots, len(measurement_indices)), dtype=int)
        for jj in range(n_shots):
            result = np.zeros(n_states)
            result[result_inds[jj]] = 1

            for ii, ind in enumerate(measurement_indices):
                observed = number_operators[ind].dot(result)
                observed = observed.dot(result)
                measurements[jj, ii] = int(observed)
        shots_array = measurements.tolist()

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

jordan_wigner_transform(j, lattice_length)

Builds up the fermionic operators in a 1D lattice. For details see : https://arxiv.org/abs/0705.1928

Parameters:

Name Type Description Default
j

site index

required
lattice_length

how many sites does the lattice have ?

required

Returns:

Name Type Description
psi_x ndarray

the field operator of creating a fermion on size j

Source code in fermions/spooler.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def jordan_wigner_transform(j: int, lattice_length: int) -> np.ndarray:
    """
    Builds up the fermionic operators in a 1D lattice.
    For details see : https://arxiv.org/abs/0705.1928

    Args:
        j : site index
        lattice_length :  how many sites does the lattice have ?

    Returns:
        psi_x: the field operator of creating a fermion on size j
    """
    p_arr = np.array([[0, 1], [0, 0]])
    z_arr = np.array([[1, 0], [0, -1]])
    id_arr = np.eye(2)
    operators = []
    for dummy in range(j):
        operators.append(z_arr)
    operators.append(p_arr)
    for dummy in range(lattice_length - j - 1):
        operators.append(id_arr)
    return nested_kronecker_product(operators)

nested_kronecker_product(a)

putting together a large operator from a list of matrices.

Provide an example here.

Parameters:

Name Type Description Default
a list

A list of matrices that can connected.

required

Returns:

Name Type Description
array ndarray

An matrix that operates on the connected Hilbert space.

Source code in fermions/spooler.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def nested_kronecker_product(a: list) -> np.ndarray:
    """putting together a large operator from a list of matrices.

    Provide an example here.

    Args:
        a (list): A list of matrices that can connected.

    Returns:
        array: An matrix that operates on the connected Hilbert space.
    """
    if len(a) == 2:
        return np.kron(a[0], a[1])
    else:
        return np.kron(a[0], nested_kronecker_product(a[1:]))

Comments