Trajectree 0.0.4__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.
- trajectree/fock_optics/devices.py +129 -27
- trajectree/fock_optics/light_sources.py +8 -2
- trajectree/fock_optics/measurement.py +73 -37
- trajectree/fock_optics/noise_models.py +112 -2
- trajectree/{optical_quant_info.py → fock_optics/optical_quant_info.py} +15 -5
- trajectree/fock_optics/outputs.py +1 -1
- trajectree/quant_info/circuit.py +251 -0
- trajectree/quant_info/noise_models.py +24 -0
- trajectree/sequence/swap.py +48 -24
- trajectree/trajectory.py +289 -63
- {trajectree-0.0.4.dist-info → trajectree-0.0.5.dist-info}/METADATA +2 -1
- trajectree-0.0.5.dist-info/RECORD +18 -0
- {trajectree-0.0.4.dist-info → trajectree-0.0.5.dist-info}/WHEEL +1 -1
- trajectree-0.0.4.dist-info/RECORD +0 -16
- {trajectree-0.0.4.dist-info → trajectree-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {trajectree-0.0.4.dist-info → trajectree-0.0.5.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
# a = qt.destroy(N).full()
|
|
53
|
+
# a_dag = a.T
|
|
54
|
+
# I = np.eye(N)
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
61
|
+
# # print("unitary_BS\n", np.round(unitary_BS, 4))
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
70
|
+
# # print("generalized_BS\n", np.round(generalized_BS, 4))
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
150
|
-
quantum_channel_list.extend(
|
|
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
|
|
194
|
+
for i in range(len(idler_angles)):
|
|
186
195
|
coincidence_probs = []
|
|
187
196
|
|
|
188
|
-
# rotator_node_1 =
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
134
|
-
#
|
|
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
|
|
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 = ""
|