Trajectree 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,20 +4,38 @@ import numpy as np
4
4
  from numpy import kron
5
5
 
6
6
  from quimb.tensor import MatrixProductOperator as mpo #type: ignore
7
+ from functools import lru_cache
7
8
 
8
9
  import qutip as qt
9
10
 
10
11
  # Beamsplitter transformation
11
- def create_BS_MPO(site1, site2, theta, total_sites, N, tag = 'BS'):
12
+ def create_BS_MPO(site1, site2, theta, total_sites, N, return_unitary = False, tag = 'BS'):
13
+
14
+ """
15
+ This is a convenience function for legacy implementations. Preferably, don't use this. Only use the generalized mode mixer.
12
16
 
13
- a = qt.destroy(N).full()
14
- a_dag = a.T
15
- I = np.eye(N)
17
+ As can be inferred from the call to the generalized_mode_mixer function, the beamsplitter transformation is equivalent to an ry rotation.
18
+ The specific form is chosen because legacy code used this definition.
19
+ When theta = np.pi/4, the transformation corresponds to:
20
+
21
+ a -> 1/sqrt(2) (c-d)
22
+ b -> 1/sqrt(2) (c+d)
23
+
24
+ """
25
+
26
+ # a = qt.destroy(N).full()
27
+ # a_dag = a.T
28
+ # I = np.eye(N)
16
29
 
17
- # This corresponds to the BS hamiltonian:
30
+ # # This corresponds to the BS hamiltonian:
31
+
32
+ # hamiltonian_BS = -theta * ( kron(I, a_dag)@kron(a, I) - kron(I, a)@kron(a_dag, I) )
33
+ # unitary_BS = expm(hamiltonian_BS)
34
+
35
+ unitary_BS = generalized_mode_mixer_unitary(-2*theta, 0, 0, 0, N)
18
36
 
19
- hamiltonian_BS = -theta * ( kron(I, a_dag)@kron(a, I) - kron(I, a)@kron(a_dag, I) )
20
- unitary_BS = expm(hamiltonian_BS)
37
+ if return_unitary:
38
+ return unitary_BS
21
39
 
22
40
  # print("unitary_BS", unitary_BS)
23
41
 
@@ -26,33 +44,117 @@ def create_BS_MPO(site1, site2, theta, total_sites, N, tag = 'BS'):
26
44
  return BS_MPO
27
45
 
28
46
 
29
- def generalized_mode_mixer(site1, site2, theta, phi, psi, lamda, total_sites, N, tag = 'MM'):
47
+ # def generalized_mode_mixer(site1, site2, theta, phi, psi, lamda, total_sites, N, tag = 'MM'):
48
+ # """
49
+ # Deprticated, do not use!
50
+ # """
30
51
 
31
- a = qt.destroy(N).full()
32
- a_dag = a.T
33
- I = np.eye(N)
52
+ # a = qt.destroy(N).full()
53
+ # a_dag = a.T
54
+ # I = np.eye(N)
34
55
 
35
- # This corresponds to the BS hamiltonian: This is a different difinition from the one in
36
- # create_BS_MPO. This is because of how the generalized beamsplitter is defined in DOI: 10.1088/0034-4885/66/7/203 .
37
- hamiltonian_BS = theta * (kron(a_dag, I)@kron(I, a) + kron(a, I)@kron(I, a_dag))
38
- unitary_BS = expm(-1j * hamiltonian_BS)
56
+ # # This corresponds to the BS hamiltonian: This is a different difinition from the one in
57
+ # # create_BS_MPO. This is because of how the generalized beamsplitter is defined in DOI: 10.1088/0034-4885/66/7/203 .
58
+ # hamiltonian_BS = theta * (kron(a_dag, I)@kron(I, a) + kron(a, I)@kron(I, a_dag))
59
+ # unitary_BS = expm(-1j * hamiltonian_BS)
39
60
 
40
- # print("unitary_BS\n", np.round(unitary_BS, 4))
61
+ # # print("unitary_BS\n", np.round(unitary_BS, 4))
41
62
 
42
- pre_phase_shifter = np.kron(phase_shifter(N, phi[0]/2), phase_shifter(N, phi[1]/2))
43
- post_phase_shifter = np.kron(phase_shifter(N, psi[0]/2), phase_shifter(N, psi[1]/2))
44
- global_phase_shifter = np.kron(phase_shifter(N, lamda[0]/2), phase_shifter(N, lamda[1]/2))
63
+ # pre_phase_shifter = np.kron(phase_shifter(N, phi[0]/2), phase_shifter(N, phi[1]/2))
64
+ # post_phase_shifter = np.kron(phase_shifter(N, psi[0]/2), phase_shifter(N, psi[1]/2))
65
+ # global_phase_shifter = np.kron(phase_shifter(N, lamda[0]/2), phase_shifter(N, lamda[1]/2))
45
66
 
46
- # This construction for the generalized beamsplitter is based on the description in paper DOI: 10.1088/0034-4885/66/7/203
47
- generalized_BS = global_phase_shifter @ (pre_phase_shifter @ unitary_BS @ post_phase_shifter)
67
+ # # This construction for the generalized beamsplitter is based on the description in paper DOI: 10.1088/0034-4885/66/7/203
68
+ # generalized_BS = global_phase_shifter @ (pre_phase_shifter @ unitary_BS @ post_phase_shifter)
48
69
 
49
- # print("generalized_BS\n", np.round(generalized_BS, 4))
70
+ # # print("generalized_BS\n", np.round(generalized_BS, 4))
50
71
 
51
- BS_MPO = mpo.from_dense(generalized_BS, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
52
- # BS_MPO = BS_MPO.fill_empty_sites(mode = "full")
72
+ # BS_MPO = mpo.from_dense(generalized_BS, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
73
+ # # BS_MPO = BS_MPO.fill_empty_sites(mode = "full")
74
+ # return BS_MPO
75
+
76
+
77
+ # def phase_shifter(N, theta):
78
+ # """
79
+ # Depricated, do not use!
80
+ # """
81
+ # diag = [np.exp(1j * theta * i) for i in range(N)]
82
+ # return np.diag(diag, k=0)
83
+
84
+
85
+ def rx(omega, N, return_unitary = False, site1 = None, site2 = None, total_sites = None, tag = 'rx'):
86
+ L_t, L_x, L_y, L_z = generate_angular_momentum_operators(N)
87
+ unitary_rx = expm(-1j * omega * L_x)
88
+ if return_unitary:
89
+ return unitary_rx
90
+ BS_MPO = mpo.from_dense(unitary_rx, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
53
91
  return BS_MPO
54
92
 
93
+ def ry(theta, N, return_unitary = False, site1 = None, site2 = None, total_sites = None, tag = 'ry'):
94
+ L_t, L_x, L_y, L_z = generate_angular_momentum_operators(N)
95
+ unitary_ry = expm(-1j * theta * L_y)
96
+ if return_unitary:
97
+ return unitary_ry
98
+ BS_MPO = mpo.from_dense(unitary_ry, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
99
+ return BS_MPO
100
+
101
+ def rz(phi, N, return_unitary = False, site1 = None, site2 = None, total_sites = None, tag = 'rz'):
102
+ L_t, L_x, L_y, L_z = generate_angular_momentum_operators(N)
103
+ unitary_rz = expm(-1j * phi * L_z)
104
+ if return_unitary:
105
+ return unitary_rz
106
+ BS_MPO = mpo.from_dense(unitary_rz, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
107
+ return BS_MPO
108
+
109
+ def global_phase(lamda, N, return_unitary = False, site1 = None, site2 = None, total_sites = None, tag = 'global_phase'):
110
+ L_t, L_x, L_y, L_z = generate_angular_momentum_operators(N)
111
+ unitary_global_phase = expm(-1j * lamda * L_t)
112
+ if return_unitary:
113
+ return unitary_global_phase
114
+ BS_MPO = mpo.from_dense(unitary_global_phase, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
115
+ return BS_MPO
116
+
117
+ def single_mode_phase(lamda, N):
118
+ a = qt.destroy(N).full()
119
+ a_dag = a.T
120
+ I = np.eye(N)
55
121
 
56
- def phase_shifter(N, theta):
57
- diag = [np.exp(1j * theta * i) for i in range(N)]
58
- return np.diag(diag, k=0)
122
+ unitary_phase = expm(-1j * (lamda/2) * a_dag @ a)
123
+ return unitary_phase
124
+
125
+ @lru_cache(maxsize=32)
126
+ def generate_angular_momentum_operators(N):
127
+ """
128
+ This generates the angular momentum operators for polarization two polarization modes using the Jordan-Schwinger representation.
129
+ See sec. 4.2 in DOI: 10.1088/0034-4885/66/7/203
130
+ """
131
+ a = qt.destroy(N).full()
132
+ a_dag = a.T
133
+ I = np.eye(N)
134
+
135
+ a1 = kron(I, a)
136
+ a2 = kron(a, I)
137
+ a1_dag = a1.T
138
+ a2_dag = a2.T
139
+
140
+ L_t = 0.5 * (a1_dag @ a1 + a2_dag @ a2)
141
+ L_x = 0.5 * (a1_dag @ a2 + a2_dag @ a1)
142
+ L_y = 0.5j * (a2_dag @ a1 - a1_dag @ a2)
143
+ L_z = 0.5 * (a1_dag @ a1 - a2_dag @ a2)
144
+
145
+ return L_t, L_x, L_y, L_z
146
+
147
+ def generalized_mode_mixer_unitary(theta, phi, psi, lamda, N):
148
+ """
149
+ This generates the generalized beamsplitter operator.
150
+ See eq. 4.12 in DOI: 10.1088/0034-4885/66/7/203
151
+ """
152
+ unitary_BS = rz(phi, N, return_unitary=True) @ ry(theta, N, return_unitary=True) @ rz(psi, N, return_unitary=True) @ global_phase(lamda, N, return_unitary=True)
153
+ return unitary_BS
154
+
155
+ def generalized_mode_mixer(site1, site2, theta, phi, psi, lamda, total_sites, N, tag = 'MM'):
156
+ generalized_BS = generalized_mode_mixer_unitary(theta, phi, psi, lamda, N)
157
+
158
+ BS_MPO = mpo.from_dense(generalized_BS, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
159
+ # # BS_MPO = BS_MPO.fill_empty_sites(mode = "full")
160
+ return BS_MPO
@@ -15,12 +15,18 @@ import qutip as qt
15
15
  from math import factorial
16
16
 
17
17
 
18
- def create_TMSV_OP_Dense(N, mean_photon_num):
18
+ def create_TMSV_OP_Dense(N, mean_photon_num, phi = np.pi, theta = np.pi):
19
19
  a = qt.destroy(N).full()
20
20
  a_dag = a.T
21
21
  truncation = (N-1)
22
22
 
23
- op = expm(1j * mean_photon_num * (kron(a_dag, a_dag) + kron(a, a)))
23
+ # We convert the mean photon number to the squeezing parameter chi using the relation in paper: https://doi.org/10.1103/PhysRevA.98.063842
24
+ chi = np.asinh(np.sqrt(mean_photon_num))
25
+
26
+ # op = expm(np.exp(1j * phi) * chi * (kron(a_dag, a_dag) + np.exp(1j * theta) * kron(a, a)))
27
+ # op = np.round(op, 12)
28
+ op = expm(1j* chi * (kron(a_dag, a_dag) + kron(a, a)))
29
+ # op = expm(0.24 * (kron(a_dag, a_dag) + kron(a, a)))
24
30
 
25
31
  return op
26
32
 
@@ -1,6 +1,6 @@
1
- from .devices import generalized_mode_mixer, create_BS_MPO
1
+ from .devices import generalized_mode_mixer, create_BS_MPO, ry
2
2
  from ..trajectory import quantum_channel
3
- from .noise_models import single_mode_bosonic_noise_channels
3
+ from .noise_models import single_mode_bosonic_noise_channels, general_mixed_bs_noise_model, depolarizing_operators
4
4
 
5
5
  from scipy.linalg import sqrtm
6
6
  from scipy import sparse as sp
@@ -79,9 +79,11 @@ def create_PNR_POVM_OP_Dense(eff, outcome, N, debug = False):
79
79
 
80
80
  def generate_sqrt_POVM_MPO(sites, outcome, total_sites, efficiency, N, pnr = False, tag = "POVM"):
81
81
  if pnr:
82
- dense_op = sqrtm(create_PNR_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
82
+ # dense_op = sqrtm(create_PNR_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
83
+ dense_op = (create_PNR_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
83
84
  else:
84
- dense_op = sqrtm(create_threshold_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
85
+ # dense_op = sqrtm(create_threshold_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
86
+ dense_op = (create_threshold_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
85
87
 
86
88
  sqrt_POVM_MPOs = []
87
89
  for i in sites:
@@ -90,7 +92,7 @@ def generate_sqrt_POVM_MPO(sites, outcome, total_sites, efficiency, N, pnr = Fal
90
92
  return sqrt_POVM_MPOs
91
93
 
92
94
 
93
- def bell_state_measurement(psi, N, site_tags, num_modes, efficiencies, dark_counts_gain, error_tolerance, beamsplitters = [[2,6],[3,7]], measurements = {0:(2,7), 1:(3,6)}, pnr = False, det_outcome = 1, use_trajectory = False, return_MPOs = False, compress = True, contract = True):
95
+ def bell_state_measurement(psi, N, site_tags, num_modes, efficiencies, dark_counts, error_tolerance, beamsplitters = [[2,6],[3,7]], measurements = {0:(2,7), 1:(3,6)}, depolarizing_error = False, damping_error = True, pnr = False, det_outcome = 1, use_trajectory = False, return_MPOs = False, compress = True, contract = True):
94
96
 
95
97
  """Perform Bell state measrement or return the MPOs used in the measurement.
96
98
  Args:
@@ -128,28 +130,29 @@ def bell_state_measurement(psi, N, site_tags, num_modes, efficiencies, dark_coun
128
130
  returned_MPOs = [U_BS_H, U_BS_V]
129
131
  if use_trajectory:
130
132
  quantum_channel_list = [quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = BSM_MPO, name = "beam splitter") for BSM_MPO in returned_MPOs]
131
-
132
- damping_kraus_ops_0 = single_mode_bosonic_noise_channels(noise_parameter = 1-efficiencies[0], N = N)
133
- damping_kraus_ops_1 = single_mode_bosonic_noise_channels(noise_parameter = 1-efficiencies[1], N = N)
134
- two_mode_kraus_ops_0 = [sp.kron(op1, op2) for op1 in damping_kraus_ops_0 for op2 in damping_kraus_ops_0]
135
- two_mode_kraus_ops_1 = [sp.kron(op1, op2) for op1 in damping_kraus_ops_1 for op2 in damping_kraus_ops_1]
136
- quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((2,3), two_mode_kraus_ops_0), name = "detector inefficiency")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
137
- quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((6,7), two_mode_kraus_ops_1), name = "detector inefficiency")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
138
-
139
- amplification_kraus_ops_0 = single_mode_bosonic_noise_channels(noise_parameter = dark_counts_gain[0], N = N)
140
- amplification_kraus_ops_1 = single_mode_bosonic_noise_channels(noise_parameter = dark_counts_gain[1], N = N)
141
- two_mode_kraus_ops_0 = [sp.kron(op1, op2) for op1 in amplification_kraus_ops_0 for op2 in amplification_kraus_ops_0]
142
- two_mode_kraus_ops_1 = [sp.kron(op1, op2) for op1 in amplification_kraus_ops_1 for op2 in amplification_kraus_ops_1]
143
- quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((2,3), two_mode_kraus_ops_0), name = "dark counts")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
144
- quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((6,7), two_mode_kraus_ops_1), name = "dark counts")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
133
+
134
+ if damping_error:
135
+ damping_and_loss_channel0 = general_mixed_bs_noise_model(dark_count_rate = dark_counts[0], eta = efficiencies[0], N = N)
136
+ damping_and_loss_channel1 = general_mixed_bs_noise_model(dark_count_rate = dark_counts[1], eta = efficiencies[1], N = N)
137
+ # two_mode_kraus_ops_0 = [sp.kron(op1, op2) for op1 in damping_and_loss_channel0 for op2 in damping_and_loss_channel0]
138
+ # two_mode_kraus_ops_1 = [sp.kron(op1, op2) for op1 in damping_and_loss_channel1 for op2 in damping_and_loss_channel1]
139
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((2,), damping_and_loss_channel0), name = "BSM detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
140
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((3,), damping_and_loss_channel0), name = "BSM detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
141
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((6,), damping_and_loss_channel1), name = "BSM detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
142
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((7,), damping_and_loss_channel1), name = "BSM detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
143
+
144
+ if depolarizing_error:
145
+ depolarizing_channels = depolarizing_operators(depolarizing_probability = 0.5, N = N)
146
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((2,3), depolarizing_channels), name = "BSM depol"))
147
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((6,7), depolarizing_channels), name = "BSM depol"))
145
148
 
146
149
  BSM_POVM_1_OPs = generate_sqrt_POVM_MPO(sites=measurements[1], outcome = det_outcome, total_sites=num_modes, efficiency=1, N=N, pnr = pnr)
147
150
  BSM_POVM_1_OPs.extend(generate_sqrt_POVM_MPO(sites=measurements[0], outcome = 0, total_sites=num_modes, efficiency=1, N=N, pnr = pnr))
148
151
 
149
- det_quantum_channels = [quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = DET_MPO, name = "Det POVM") for DET_MPO in BSM_POVM_1_OPs]
150
- quantum_channel_list.extend(det_quantum_channels)
152
+ expectation_ops = [quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = DET_MPO, expectation = False, name = "Det POVM") for DET_MPO in BSM_POVM_1_OPs]
153
+ # quantum_channel_list.extend(expectation_ops)
151
154
 
152
- return quantum_channel_list
155
+ return quantum_channel_list, expectation_ops
153
156
 
154
157
  returned_MPOs.extend(BSM_POVM_1_OPs) # Collect all the MPOs in a list and return them. The operators are ordered as such:
155
158
 
@@ -168,52 +171,85 @@ def bell_state_measurement(psi, N, site_tags, num_modes, efficiencies, dark_coun
168
171
 
169
172
 
170
173
 
171
- def rotate_and_measure(psi, N, site_tags, num_modes, efficiency, error_tolerance, idler_angles, signal_angles, rotations = {"signal":(4,5), "idler":(0,1)}, measurements = {1:(0,4), 0:(1,5)}, pnr = False, det_outcome = 1, return_MPOs = False, compress = True, contract = True, draw = False):
174
+ def rotate_and_measure(psi, N, site_tags, num_modes, efficiency, error_tolerance, idler_angles, signal_angles, dark_counts = [3e-5,3e-5], rotations = {"signal":(4,5), "idler":(0,1)}, measurements = {1:(0,4), 0:(1,5)}, depolarizing_error = False, damping_error = True, pnr = False, det_outcome = 1, return_MPOs = False, return_quantum_channel = False, compress = True, contract = True, draw = False):
172
175
  # idler_angles = [0]
173
176
  # angles = [np.pi/4]
174
177
 
178
+ # A dict is used when the user wants to rotate around multiple axes of the bloch sphere. Otherwise, if its a list, then
179
+ # we assume all measurements around the y axis.
180
+ # if type(idler_angles) != dict:
181
+ # idler_angles = {"theta": idler_angles, "phi":[0]*len(idler_angles), "psi":[0]*len(idler_angles)}
182
+ # if type(signal_angles) != dict:
183
+ # signal_angles = {"theta": signal_angles, "phi":[0]*len(signal_angles), "psi":[0]*len(signal_angles)}
184
+
185
+
175
186
  coincidence = []
176
187
 
177
188
  POVM_1_OPs = generate_sqrt_POVM_MPO(sites = measurements[1], outcome = det_outcome, total_sites=num_modes, efficiency=efficiency, N=N, pnr = pnr)
178
189
  POVM_0_OPs = generate_sqrt_POVM_MPO(sites = measurements[0], outcome = 0, total_sites=num_modes, efficiency=efficiency, N=N, pnr = pnr)
179
- # POVM_0_OPs = generate_sqrt_POVM_MPO(sites=(0,4), outcome = 0, total_sites=num_modes, efficiency=efficiency, N=N, pnr = pnr)
180
- # enforce_1d_like(POVM_OP, site_tags=site_tags, inplace=True)
181
190
 
182
191
  meas_ops = POVM_1_OPs
183
192
  meas_ops.extend(POVM_0_OPs)
184
193
 
185
- for i, idler_angle in enumerate(idler_angles):
194
+ for i in range(len(idler_angles)):
186
195
  coincidence_probs = []
187
196
 
188
- # rotator_node_1 = create_BS_MPO(site1 = rotations["idler"][0], site2 = rotations["idler"][1], theta=idler_angle, total_sites = num_modes, N = N, tag = r"$Rotator_I$")
189
- ######################
190
- # We make this correction here since the rotator hamiltonian is 1/2(a_v b_h + a_h b_v), which does not show up in the bs unitary, whose function we are reusing to
191
- # rotate the state.
192
- rotator_node_1 = generalized_mode_mixer(site1 = rotations["idler"][0], site2 = rotations["idler"][1], theta = -idler_angle/2, phi = [0,0], psi = [0,0], lamda = [0,0], total_sites = num_modes, N = N, tag = 'MM')
193
-
197
+ # rotator_node_1 = generalized_mode_mixer(site1 = rotations["idler"][0], site2 = rotations["idler"][1], theta = idler_angles[i], phi = 0, psi = 0, lamda = 0, total_sites = num_modes, N = N, tag = 'MM')
198
+ rotator_node_1 = ry(idler_angles[i], N, site1 = rotations["idler"][0], site2 = rotations["idler"][1], total_sites = num_modes, tag = 'ry')
194
199
 
195
200
  enforce_1d_like(rotator_node_1, site_tags=site_tags, inplace=True)
196
201
  rotator_node_1.add_tag("L5")
197
- if not return_MPOs: # If the user wants the MPOs, we don't need to apply the rotator to the state.
202
+ if not return_MPOs and not return_quantum_channel: # If the user wants the MPOs, we don't need to apply the rotator to the state.
198
203
  idler_rotated_psi = tensor_network_apply_op_vec(rotator_node_1, psi, compress=compress, contract = contract, cutoff = error_tolerance)
199
204
 
200
205
 
201
- for j, angle in enumerate(signal_angles):
206
+ for j in range(len(signal_angles)):
202
207
  # print("idler:", i, "signal:", j)
203
208
 
204
209
  # rotator_node_2 = create_BS_MPO(site1 = rotations["signal"][0], site2 = rotations["signal"][1], theta=angle, total_sites = num_modes, N = N, tag = r"$Rotator_S$")
205
210
  ##########################
206
211
  # We make this correction here since the rotator hamiltonian is 1/2(a_v b_h + a_h b_v), which does not show up in the bs unitary, whose function we are reusing to
207
212
  # rotate the state.
208
- rotator_node_2 = generalized_mode_mixer(site1 = rotations["signal"][0], site2 = rotations["signal"][1], theta = -angle/2, phi = [0,0], psi = [0,0], lamda = [0,0], total_sites = num_modes, N = N, tag = 'MM')
209
-
213
+ # rotator_node_2 = generalized_mode_mixer(site1 = rotations["signal"][0], site2 = rotations["signal"][1], theta = signal_angles[j], phi = 0, psi = 0, lamda = 0, total_sites = num_modes, N = N, tag = 'MM')
214
+ rotator_node_2 = ry(signal_angles[j], N, site1 = rotations["signal"][0], site2 = rotations["signal"][1], total_sites = num_modes, tag = 'ry')
215
+ # print("checling node2 unitarity:", sp.csr_array(np.round(rotator_node_2.to_dense() @ rotator_node_2.to_dense().T.conj() - np.eye(N**2), 5)))
210
216
 
211
217
  enforce_1d_like(rotator_node_2, site_tags=site_tags, inplace=True)
212
218
 
213
219
  if return_MPOs:
214
220
  meas_ops = [rotator_node_1, rotator_node_2] + meas_ops # Collect all the MPOs in a list and return them
215
221
  return meas_ops
216
-
222
+
223
+ if return_quantum_channel:
224
+ quantum_channel_list = []
225
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = rotator_node_1, name = "Idler Rotator"))
226
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = rotator_node_2, name = "Signal Rotator"))
227
+
228
+ if damping_error:
229
+ damping_and_loss_channel0 = general_mixed_bs_noise_model(dark_count_rate = dark_counts[0], eta = efficiency, N = N)
230
+ damping_and_loss_channel1 = general_mixed_bs_noise_model(dark_count_rate = dark_counts[1], eta = efficiency, N = N)
231
+ # two_mode_kraus_ops_0 = [sp.kron(op1, op2) for op1 in damping_and_loss_channel0 for op2 in damping_and_loss_channel0]
232
+ # two_mode_kraus_ops_1 = [sp.kron(op1, op2) for op1 in damping_and_loss_channel1 for op2 in damping_and_loss_channel1]
233
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((rotations["idler"][0],), damping_and_loss_channel0), name = "PA detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
234
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((rotations["idler"][1],), damping_and_loss_channel0), name = "PA detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
235
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((rotations["signal"][0],), damping_and_loss_channel1), name = "PA detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
236
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((rotations["signal"][1],), damping_and_loss_channel1), name = "PA detector")) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
237
+
238
+ if depolarizing_error:
239
+ depolarizing_channels = depolarizing_operators(depolarizing_probability = 0.5, N = N)
240
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((0,1), depolarizing_channels), name = "BSM depol"))
241
+ quantum_channel_list.append(quantum_channel(N = N, num_modes = num_modes, formalism = "kraus", kraus_ops_tuple = ((4,5), depolarizing_channels), name = "BSM depol"))
242
+
243
+ POVM_1_OPs = generate_sqrt_POVM_MPO(sites=measurements[1], outcome = det_outcome, total_sites=num_modes, efficiency=1, N=N, pnr = pnr)
244
+ POVM_1_OPs.extend(generate_sqrt_POVM_MPO(sites=measurements[0], outcome = 0, total_sites=num_modes, efficiency=1, N=N, pnr = pnr))
245
+
246
+ expectation_ops = [quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = DET_MPO, expectation = False, name = "Det POVM") for DET_MPO in POVM_1_OPs]
247
+ # quantum_channel_list.extend(expectation_ops)
248
+
249
+
250
+ return quantum_channel_list, expectation_ops
251
+
252
+
217
253
  # Rotate and measure:
218
254
  rotator_node_2.add_tag("L5")
219
255
  rho_rotated = tensor_network_apply_op_vec(rotator_node_2, idler_rotated_psi, compress=compress, contract = contract, cutoff = error_tolerance)
@@ -1,14 +1,19 @@
1
1
  from scipy import sparse as sp
2
2
  from scipy.linalg import expm
3
+ from .devices import rx, ry, rz, global_phase
3
4
 
4
5
  import numpy as np
5
-
6
+ from numpy import sqrt
6
7
  import qutip as qt
7
8
  from math import factorial
9
+ from functools import lru_cache
10
+
8
11
 
9
12
  def single_mode_bosonic_noise_channels(noise_parameter, N):
10
13
  """This function produces the Kraus operatorsd for the single mode bosonic noise channels. This includes pure loss and
11
14
  pure gain channels. The pure gain channel is simply the transpose of the pure loss channel.
15
+
16
+ This implementation is based on the definitions in the paper: https://doi.org/10.1103/PhysRevA.97.032346
12
17
 
13
18
  Args:
14
19
  noise_parameter (float): The noise parameter, (loss for pure loss and gain for pure gain channels). For the pure loss channel, this
@@ -38,4 +43,109 @@ def single_mode_bosonic_noise_channels(noise_parameter, N):
38
43
  for l in range(N):
39
44
  kraus_ops[l] = kraus_ops[l].T.conjugate()
40
45
 
41
- return kraus_ops
46
+ return kraus_ops
47
+
48
+ def _nck(n,k):
49
+ """Compute the binomial coefficient "n choose k"."""
50
+ if k < 0 or k > n:
51
+ return 0
52
+ return factorial(n) / (factorial(k) * factorial(n - k))
53
+
54
+ def general_coherent_bs_noise_model(bath_parameter, theta, N, bath_type='coherent'):
55
+ """This function produces the Kraus operators for a general beamsplitter noise model with a pure coherent bath.
56
+
57
+ Args:
58
+ bath_parameter (float): The parameter of the bath (thermal or coherent).
59
+ theta (float): The beamsplitter transmissivity.
60
+ N (int): local Hilbert space dimension being considered.
61
+ """
62
+ a = qt.destroy(N).full()
63
+ a_dag = qt.create(N).full()
64
+ n = a_dag @ a
65
+
66
+ basis = lambda i: qt.states.basis(N, i).full()
67
+ if bath_type == 'coherent':
68
+ bath_state = lambda n: np.sqrt(np.exp(-np.abs(bath_parameter)**2) / factorial(n)) * bath_parameter**n
69
+
70
+ kraus_ops = []
71
+
72
+ for k in range(N):
73
+ kraus_op = 0
74
+ for n in range(N):
75
+ for q in range(N):
76
+ coeff = 0
77
+ for r_2 in range(n):
78
+ coeff += bath_state(n) * np.sqrt(_nck(q, q+n-(k+r_2)) * _nck(n, r_2)) * (1j)**(n-r_2) * np.cos(theta)**(k+2*r_2-n) * np.sin(theta)**(q+2*n - k - 2*r_2)
79
+ kraus_op += coeff * basis(q+n-k) @ basis(q).T
80
+ kraus_ops.append(kraus_op)
81
+
82
+ return kraus_ops
83
+
84
+
85
+ def general_mixed_bs_noise_model(dark_count_rate, eta, N):
86
+ """This function produces the Kraus operators for a general beamsplitter noise model with mixed thermal bath.
87
+
88
+ Args:
89
+ dark_count_rate (float): Rate of detector dark counts per second
90
+ eta (float): The beamsplitter transmissivity.
91
+ N (int): local Hilbert space dimension being considered.
92
+ """
93
+ a = qt.destroy(N).full()
94
+ a_dag = qt.create(N).full()
95
+ n = a_dag @ a
96
+
97
+ basis = lambda i: qt.states.basis(N, i).full()
98
+ r = np.atanh(np.sqrt(dark_count_rate/(1-eta+eta*dark_count_rate))) # We can also calulate the mean photon number of the fictitious thermal bath as sinh(r)**2
99
+
100
+ kraus_ops = []
101
+
102
+ theta = np.arcsin(np.sqrt(eta))
103
+
104
+ for n in range(N):
105
+ bath_state = np.sqrt(np.cosh(r)**(-2) * np.tanh(r)**(2*n))
106
+ for k in range(N):
107
+ kraus_op = 0
108
+ # flag = False
109
+ for q in range(N):
110
+ if q+n-k < N and q+n-k >= 0:
111
+ # flag = True
112
+ coeff = 0
113
+ for r_2 in range(n+1):
114
+ coeff += bath_state * np.sqrt(_nck(q, q+n-(k+r_2)) * _nck(n, r_2)) * (-1)**(n-r_2) * np.cos(theta)**(k+2*r_2-n) * np.sin(theta)**(q+2*n - k - 2*r_2)
115
+ # try:
116
+ kraus_op += coeff * sp.csr_array((basis(q+n-k) @ basis(q).T))
117
+ # except:
118
+ # raise ValueError(f"Basis {q+n-k} is not possible for N={N}")
119
+ # if flag:
120
+ kraus_ops.append(kraus_op)
121
+
122
+ return kraus_ops
123
+
124
+
125
+
126
+ @lru_cache(maxsize=100)
127
+ def depolarizing_operators(depolarizing_probability, N, bias = (1/3, 1/3, 1/3)):
128
+ ops = []
129
+ ops.append(sqrt(1-depolarizing_probability) * sp.eye(N**2, format="csc"))
130
+ ops.append(sqrt(depolarizing_probability * bias[0]) * sp.csr_matrix(rx(np.pi, N, return_unitary=True)))
131
+ ops.append(sqrt(depolarizing_probability * bias[1]) * sp.csr_matrix(ry(np.pi, N, return_unitary=True)))
132
+ ops.append(sqrt(depolarizing_probability * bias[2]) * sp.csr_matrix(rz(np.pi, N, return_unitary=True)))
133
+ return ops
134
+
135
+ def two_qubit_depolarizing_channel(depolarizing_probability, N):
136
+ """This function produces the Kraus operators for the two qubit depolarizing channel.
137
+
138
+ Args:
139
+ depolarizing_probability (float): The depolarizing probability.
140
+ N (int): local Hilbert space dimension being considered.
141
+ """
142
+ single_qubit_ops = [sp.csr_matrix(global_phase(0, N, return_unitary = True)), sp.csr_matrix(rx(np.pi, N, return_unitary=True)), sp.csr_matrix(ry(np.pi, N, return_unitary=True)), sp.csr_matrix(rz(np.pi, N, return_unitary=True))]
143
+ ops = []
144
+ ops.append(sqrt(1-(15/16)*depolarizing_probability) * sp.eye(N**4, format="csc"))
145
+ for i in range(4):
146
+ for j in range(4):
147
+ if i == 0 and j == 0:
148
+ continue
149
+ else:
150
+ ops.append(sqrt(depolarizing_probability/16) * sp.kron(single_qubit_ops[i], single_qubit_ops[j]))
151
+ return ops
@@ -4,7 +4,7 @@ from trajectree.fock_optics.light_sources import *
4
4
  from trajectree.fock_optics.devices import *
5
5
  from trajectree.trajectory import *
6
6
 
7
- from trajectree.protocols.swap import perform_swapping_simulation
7
+ from trajectree.sequence.swap import perform_swapping_simulation
8
8
 
9
9
  import numpy as np
10
10
 
@@ -127,11 +127,21 @@ def CNOT(psi_control_modes, psi_target_modes, psi_control, psi_target, N, mean_p
127
127
  return psi
128
128
 
129
129
 
130
- def H(psi, sites, N, error_tolerance):
131
- # TODO: This function does not work for N > 2.
130
+ def H(psi, sites, N, error_tolerance, return_unitary = False, tag = 'H'):
132
131
  # This definition is based on the paper: https://arxiv.org/pdf/quant-ph/9706022
133
- H = generalized_mode_mixer(sites[0], sites[1], -np.pi/4, [0,-np.pi], [0,-np.pi], [0,0], psi.L, N)
134
- # H = generalized_mode_mixer(0, 1, np.pi/4, 0, 0, 0, 2, N)
132
+
133
+ # First, we implement a beamsplitter. We found this beamsplitter configuration using trial and error to match the Hadamard transformation.
134
+ unitary_H = generalized_mode_mixer_unitary(np.pi/2, np.pi/2, -np.pi/2, 2*np.pi, N)
135
+ # Next, we implement the -pi/2 (single mode) phase shifters on the |V> mode, before and after the beamsplitter.
136
+ # Note that although it appears as if we are applying the phase shift on the |H> arm and not on the |V> arm. However, that is not true.
137
+ # In the photonic representation we are using, the |0H1V> = |1> and |1H0V> = |0>, but if you look at the matrix itself, the |0H1V> state
138
+ # comes first and the |1H0V> state comes second. So, according to the matrix representation, the two modes are reversed. Hence, the different phase configuration.
139
+ unitary_H = np.kron(single_mode_phase(-np.pi, N), np.eye(N)) @ unitary_H @ np.kron(single_mode_phase(-np.pi, N), np.eye(N))
140
+
141
+ if return_unitary:
142
+ return unitary_H
143
+
144
+ H = mpo.from_dense(unitary_H, dims = N, sites = (sites[0],sites[1]), L=psi.L, tags=tag)
135
145
  enforce_1d_like(H, site_tags=psi.site_tags, inplace=True)
136
146
  psi = tensor_network_apply_op_vec(H, psi, compress=True, contract = True, cutoff = error_tolerance)
137
147
  return psi
@@ -10,7 +10,7 @@ def generate_labels(num_systems, N):
10
10
  labels = []
11
11
  state_labels = []
12
12
  for i in range(dim):
13
- state_labels.append(f"{i//N}H{i%N}V")
13
+ state_labels.append(f"{i%N}H{i//N}V")
14
14
  # print("sates:", self.state_labels)
15
15
  for i in range(dim**num_systems):
16
16
  new_label = ""