Trajectree 0.0.3__tar.gz → 0.0.5__tar.gz

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.
Files changed (28) hide show
  1. {trajectree-0.0.3 → trajectree-0.0.5}/PKG-INFO +2 -1
  2. {trajectree-0.0.3 → trajectree-0.0.5}/Trajectree.egg-info/PKG-INFO +2 -1
  3. {trajectree-0.0.3 → trajectree-0.0.5}/Trajectree.egg-info/SOURCES.txt +3 -1
  4. {trajectree-0.0.3 → trajectree-0.0.5}/Trajectree.egg-info/requires.txt +1 -0
  5. {trajectree-0.0.3 → trajectree-0.0.5}/pyproject.toml +4 -4
  6. trajectree-0.0.5/trajectree/fock_optics/devices.py +160 -0
  7. {trajectree-0.0.3 → trajectree-0.0.5}/trajectree/fock_optics/light_sources.py +8 -2
  8. trajectree-0.0.5/trajectree/fock_optics/measurement.py +272 -0
  9. trajectree-0.0.5/trajectree/fock_optics/noise_models.py +151 -0
  10. {trajectree-0.0.3/trajectree → trajectree-0.0.5/trajectree/fock_optics}/optical_quant_info.py +15 -5
  11. {trajectree-0.0.3 → trajectree-0.0.5}/trajectree/fock_optics/outputs.py +1 -1
  12. trajectree-0.0.5/trajectree/quant_info/circuit.py +251 -0
  13. trajectree-0.0.5/trajectree/quant_info/noise_models.py +24 -0
  14. trajectree-0.0.5/trajectree/sequence/swap.py +101 -0
  15. trajectree-0.0.5/trajectree/trajectory.py +437 -0
  16. trajectree-0.0.3/trajectree/fock_optics/devices.py +0 -58
  17. trajectree-0.0.3/trajectree/fock_optics/measurement.py +0 -236
  18. trajectree-0.0.3/trajectree/fock_optics/noise_models.py +0 -41
  19. trajectree-0.0.3/trajectree/sequence/swap.py +0 -77
  20. trajectree-0.0.3/trajectree/trajectory.py +0 -211
  21. {trajectree-0.0.3 → trajectree-0.0.5}/LICENSE +0 -0
  22. {trajectree-0.0.3 → trajectree-0.0.5}/README.md +0 -0
  23. {trajectree-0.0.3 → trajectree-0.0.5}/Trajectree.egg-info/dependency_links.txt +0 -0
  24. {trajectree-0.0.3 → trajectree-0.0.5}/Trajectree.egg-info/top_level.txt +0 -0
  25. {trajectree-0.0.3 → trajectree-0.0.5}/setup.cfg +0 -0
  26. {trajectree-0.0.3 → trajectree-0.0.5}/trajectree/__init__.py +0 -0
  27. {trajectree-0.0.3 → trajectree-0.0.5}/trajectree/experimental/sparse.py +0 -0
  28. {trajectree-0.0.3 → trajectree-0.0.5}/trajectree/fock_optics/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Trajectree
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
5
5
  Author-email: Ansh Singal <asingal@u.northwestern.edu>
6
6
  License-Expression: MIT
@@ -12,6 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: matplotlib
13
13
  Requires-Dist: qutip
14
14
  Requires-Dist: trajectree-quimb
15
+ Requires-Dist: treelib
15
16
  Dynamic: license-file
16
17
 
17
18
  Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Trajectree
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
5
5
  Author-email: Ansh Singal <asingal@u.northwestern.edu>
6
6
  License-Expression: MIT
@@ -12,6 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: matplotlib
13
13
  Requires-Dist: qutip
14
14
  Requires-Dist: trajectree-quimb
15
+ Requires-Dist: treelib
15
16
  Dynamic: license-file
16
17
 
17
18
  Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
@@ -7,13 +7,15 @@ Trajectree.egg-info/dependency_links.txt
7
7
  Trajectree.egg-info/requires.txt
8
8
  Trajectree.egg-info/top_level.txt
9
9
  trajectree/__init__.py
10
- trajectree/optical_quant_info.py
11
10
  trajectree/trajectory.py
12
11
  trajectree/experimental/sparse.py
13
12
  trajectree/fock_optics/devices.py
14
13
  trajectree/fock_optics/light_sources.py
15
14
  trajectree/fock_optics/measurement.py
16
15
  trajectree/fock_optics/noise_models.py
16
+ trajectree/fock_optics/optical_quant_info.py
17
17
  trajectree/fock_optics/outputs.py
18
18
  trajectree/fock_optics/utils.py
19
+ trajectree/quant_info/circuit.py
20
+ trajectree/quant_info/noise_models.py
19
21
  trajectree/sequence/swap.py
@@ -1,3 +1,4 @@
1
1
  matplotlib
2
2
  qutip
3
3
  trajectree-quimb
4
+ treelib
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "Trajectree"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  authors = [{ name="Ansh Singal", email="asingal@u.northwestern.edu" },]
9
9
  description = "Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator."
10
10
  readme = "README.md"
@@ -20,6 +20,6 @@ classifiers = [
20
20
  dependencies=[
21
21
  'matplotlib',
22
22
  'qutip',
23
- 'trajectree-quimb'
24
- ]
25
-
23
+ 'trajectree-quimb',
24
+ 'treelib',
25
+ ]
@@ -0,0 +1,160 @@
1
+ from scipy.linalg import expm
2
+
3
+ import numpy as np
4
+ from numpy import kron
5
+
6
+ from quimb.tensor import MatrixProductOperator as mpo #type: ignore
7
+ from functools import lru_cache
8
+
9
+ import qutip as qt
10
+
11
+ # Beamsplitter transformation
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.
16
+
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)
29
+
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)
36
+
37
+ if return_unitary:
38
+ return unitary_BS
39
+
40
+ # print("unitary_BS", unitary_BS)
41
+
42
+ BS_MPO = mpo.from_dense(unitary_BS, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
43
+ # BS_MPO = BS_MPO.fill_empty_sites(mode = "full")
44
+ return BS_MPO
45
+
46
+
47
+ # def generalized_mode_mixer(site1, site2, theta, phi, psi, lamda, total_sites, N, tag = 'MM'):
48
+ # """
49
+ # Deprticated, do not use!
50
+ # """
51
+
52
+ # a = qt.destroy(N).full()
53
+ # a_dag = a.T
54
+ # I = np.eye(N)
55
+
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)
60
+
61
+ # # print("unitary_BS\n", np.round(unitary_BS, 4))
62
+
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))
66
+
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)
69
+
70
+ # # print("generalized_BS\n", np.round(generalized_BS, 4))
71
+
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)
91
+ return BS_MPO
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)
121
+
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
 
@@ -0,0 +1,272 @@
1
+ from .devices import generalized_mode_mixer, create_BS_MPO, ry
2
+ from ..trajectory import quantum_channel
3
+ from .noise_models import single_mode_bosonic_noise_channels, general_mixed_bs_noise_model, depolarizing_operators
4
+
5
+ from scipy.linalg import sqrtm
6
+ from scipy import sparse as sp
7
+
8
+ import numpy as np
9
+ from numpy.linalg import matrix_power
10
+ from numpy import sqrt
11
+
12
+ from quimb.tensor import MatrixProductOperator as mpo #type: ignore
13
+ from quimb.tensor.tensor_arbgeom import tensor_network_apply_op_vec #type: ignore
14
+ from quimb.tensor.tensor_1d_compress import enforce_1d_like #type: ignore
15
+
16
+ import qutip as qt
17
+ from math import factorial
18
+
19
+ from functools import lru_cache
20
+
21
+
22
+ # This is the actual function that generates the POVM operator.
23
+ def create_threshold_POVM_OP_Dense(efficiency, outcome, N):
24
+ a = qt.destroy(N).full()
25
+ a_dag = a.T
26
+ create0 = a_dag * sqrt(efficiency)
27
+ destroy0 = a * sqrt(efficiency)
28
+ series_elem_list = [((-1)**i) * matrix_power(create0, (i+1)) @ matrix_power(destroy0, (i+1)) / factorial(i+1) for i in range(N-1)] # (-1)^i * a_dag^(i+1) @ a^(i+1) / (i+1)! = (-1)^(i+2) * a_dag^(i+1) @ a^(i+1) / (i+1)! since goes from 0->n
29
+ # print(series_elem_list[0])
30
+ dense_op = sum(series_elem_list)
31
+
32
+ if outcome == 0:
33
+ dense_op = np.eye(dense_op.shape[0]) - dense_op
34
+ # print(sqrtm(dense_op))
35
+ return dense_op
36
+
37
+ @lru_cache(maxsize=20)
38
+ def factorial(x):
39
+ n = 1
40
+ for i in range(2, x+1):
41
+ n *= i
42
+ return n
43
+
44
+ @lru_cache(maxsize=20)
45
+ def comb(n, k):
46
+ return factorial(n) / (factorial(k) * factorial(n - k))
47
+
48
+ @lru_cache(maxsize=20)
49
+ def projector(n, N):
50
+ state = np.zeros(N)
51
+ state[n] = 1
52
+ return np.outer(state, state)
53
+
54
+ # Testing stuff out here.
55
+ def create_PNR_POVM_OP_Dense(eff, outcome, N, debug = False):
56
+ a_dag = qt.create(N).full()
57
+ vacuum = np.zeros(N)
58
+ vacuum[0] = 1
59
+
60
+ @lru_cache(maxsize=20)
61
+ def create_povm_list(eff, N):
62
+ povms = []
63
+ # m is the outcome here
64
+ for m in range(N-1):
65
+ op = 0
66
+ for n in range(m, N):
67
+ op += comb(n,m) * eff**m * (1-eff)**(n-m) * projector(n, N)
68
+ povms.append(op)
69
+
70
+ povms.append(np.eye(N) - sum(povms))
71
+ return povms
72
+
73
+ povms = create_povm_list(eff, N)
74
+ if debug:
75
+ return povms[outcome], povms
76
+ return povms[outcome]
77
+
78
+
79
+
80
+ def generate_sqrt_POVM_MPO(sites, outcome, total_sites, efficiency, N, pnr = False, tag = "POVM"):
81
+ if pnr:
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)
84
+ else:
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)
87
+
88
+ sqrt_POVM_MPOs = []
89
+ for i in sites:
90
+ sqrt_POVM_MPOs.append(mpo.from_dense(dense_op, dims = N, sites = (i,), L=total_sites, tags=tag))
91
+
92
+ return sqrt_POVM_MPOs
93
+
94
+
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):
96
+
97
+ """Perform Bell state measrement or return the MPOs used in the measurement.
98
+ Args:
99
+ psi (mps): The input state to be measured.
100
+ N (int): local Hilbert space dimension
101
+ site_tags (list): The tags for the sites in the MPS.
102
+ num_modes (int): The number of modes in the MPS.
103
+ efficiencies list[float]: The efficiencies of the (pairs of) detectors in the BSM.
104
+ error_tolerance (float): The error tolerance for the tensor network.
105
+ measurements (dict): The sites for the measurements. Default is {1:(2,7), 0:(3,6)}.
106
+ pnr (bool): Whether to use photon number resolving measurement. Default is False.
107
+ pnr_outcome (int): The outcome for the photon number resolving measurement. Default is 1. When not using PNR, this can be anything other than 1 since threshold detectors don't distinguish between photon numbers.
108
+ return_MPOs (bool): Whether to return the MPOs used in the measurement. Default is False.
109
+ compress (bool): Whether to compress the MPS after applying the MPOs. Default is True.
110
+ contract (bool): Whether to contract the MPS after applying the MPOs. Default is True.
111
+
112
+ Returns:
113
+ mps: The measured state after the Bell state measurement.
114
+
115
+ """
116
+
117
+ U_BS_H = create_BS_MPO(site1 = beamsplitters[0][0], site2 = beamsplitters[0][1], theta=np.pi/4, total_sites = num_modes, N = N, tag = r"$U_{BS_H}$")
118
+ enforce_1d_like(U_BS_H, site_tags=site_tags, inplace=True)
119
+ U_BS_H.add_tag("L2")
120
+
121
+ U_BS_V = create_BS_MPO(site1 = beamsplitters[1][0], site2 = beamsplitters[1][1], theta=np.pi/4, total_sites = num_modes, N = N, tag = r"$U_{BS_V}$")
122
+ enforce_1d_like(U_BS_V, site_tags=site_tags, inplace=True)
123
+ U_BS_V.add_tag("L3")
124
+
125
+ # Note that these are not used if using trajectree to implement detector inefficiency.
126
+ BSM_POVM_1_OPs = generate_sqrt_POVM_MPO(sites=measurements[1], outcome = det_outcome, total_sites=num_modes, efficiency=efficiencies[0], N=N, pnr = pnr)
127
+ BSM_POVM_1_OPs.extend(generate_sqrt_POVM_MPO(sites=measurements[0], outcome = 0, total_sites=num_modes, efficiency=efficiencies[1], N=N, pnr = pnr))
128
+
129
+ if return_MPOs:
130
+ returned_MPOs = [U_BS_H, U_BS_V]
131
+ if use_trajectory:
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]
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"))
148
+
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)
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))
151
+
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)
154
+
155
+ return quantum_channel_list, expectation_ops
156
+
157
+ returned_MPOs.extend(BSM_POVM_1_OPs) # Collect all the MPOs in a list and return them. The operators are ordered as such:
158
+
159
+ quantum_channel_list = [quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = BSM_MPO, name = "BSM") for BSM_MPO in returned_MPOs]
160
+
161
+ return quantum_channel_list
162
+
163
+ psi = tensor_network_apply_op_vec(U_BS_H, psi, compress=compress, contract = contract, cutoff = error_tolerance)
164
+ psi = tensor_network_apply_op_vec(U_BS_V, psi, compress=compress, contract = contract, cutoff = error_tolerance)
165
+
166
+ for POVM_OP in BSM_POVM_1_OPs:
167
+ POVM_OP.add_tag("L4")
168
+ psi = tensor_network_apply_op_vec(POVM_OP, psi, compress=compress, contract = contract, cutoff = error_tolerance)
169
+
170
+ return psi
171
+
172
+
173
+
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):
175
+ # idler_angles = [0]
176
+ # angles = [np.pi/4]
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
+
186
+ coincidence = []
187
+
188
+ POVM_1_OPs = generate_sqrt_POVM_MPO(sites = measurements[1], outcome = det_outcome, total_sites=num_modes, efficiency=efficiency, N=N, pnr = pnr)
189
+ POVM_0_OPs = generate_sqrt_POVM_MPO(sites = measurements[0], outcome = 0, total_sites=num_modes, efficiency=efficiency, N=N, pnr = pnr)
190
+
191
+ meas_ops = POVM_1_OPs
192
+ meas_ops.extend(POVM_0_OPs)
193
+
194
+ for i in range(len(idler_angles)):
195
+ coincidence_probs = []
196
+
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')
199
+
200
+ enforce_1d_like(rotator_node_1, site_tags=site_tags, inplace=True)
201
+ rotator_node_1.add_tag("L5")
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.
203
+ idler_rotated_psi = tensor_network_apply_op_vec(rotator_node_1, psi, compress=compress, contract = contract, cutoff = error_tolerance)
204
+
205
+
206
+ for j in range(len(signal_angles)):
207
+ # print("idler:", i, "signal:", j)
208
+
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$")
210
+ ##########################
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
212
+ # rotate the state.
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)))
216
+
217
+ enforce_1d_like(rotator_node_2, site_tags=site_tags, inplace=True)
218
+
219
+ if return_MPOs:
220
+ meas_ops = [rotator_node_1, rotator_node_2] + meas_ops # Collect all the MPOs in a list and return them
221
+ return meas_ops
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
+
253
+ # Rotate and measure:
254
+ rotator_node_2.add_tag("L5")
255
+ rho_rotated = tensor_network_apply_op_vec(rotator_node_2, idler_rotated_psi, compress=compress, contract = contract, cutoff = error_tolerance)
256
+
257
+ # read_quantum_state(psi)
258
+ # read_quantum_state(rho_rotated)
259
+
260
+ for POVM_OP in meas_ops:
261
+ POVM_OP.add_tag("L6")
262
+ rho_rotated = tensor_network_apply_op_vec(POVM_OP, rho_rotated, compress=compress, contract = contract, cutoff = error_tolerance)
263
+
264
+ if draw:
265
+ # only for drawing the TN. Not used otherwise
266
+ fix = {(f"L{j}",f"I{num_modes - i-1}"):(3*j,i+5) for j in range(10) for i in range(10)}
267
+ rho_rotated.draw(color = [r'$HH+VV$', r'$U_{BS_H}$', r"$U_{BS_V}$", 'POVM', r'$Rotator_I$', r'$Rotator_S$'], title = "Polarization entanglement swapping MPS", fix = fix, show_inds = True, show_tags = False)
268
+ # rho_rotated.draw_tn()
269
+ coincidence_probs.append((rho_rotated.norm())**2)
270
+ coincidence.append(coincidence_probs)
271
+
272
+ return np.array(coincidence)