Trajectree 0.0.0__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.
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: Trajectree
3
+ Version: 0.0.0
4
+ Summary: Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
5
+ Author-email: Ansh Singal <asingal@u.northwestern.edu>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: quimb
13
+ Requires-Dist: numpy
14
+ Requires-Dist: scipy
15
+ Requires-Dist: matplotlib
16
+ Dynamic: license-file
17
+
18
+ Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
@@ -0,0 +1 @@
1
+ Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: Trajectree
3
+ Version: 0.0.0
4
+ Summary: Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
5
+ Author-email: Ansh Singal <asingal@u.northwestern.edu>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: quimb
13
+ Requires-Dist: numpy
14
+ Requires-Dist: scipy
15
+ Requires-Dist: matplotlib
16
+ Dynamic: license-file
17
+
18
+ Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator.
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ Trajectree.egg-info/PKG-INFO
5
+ Trajectree.egg-info/SOURCES.txt
6
+ Trajectree.egg-info/dependency_links.txt
7
+ Trajectree.egg-info/requires.txt
8
+ Trajectree.egg-info/top_level.txt
9
+ trajectree/__init__.py
10
+ trajectree/optical_quant_info.py
11
+ trajectree/trajectory.py
12
+ trajectree/experimental/sparse.py
13
+ trajectree/fock_optics/devices.py
14
+ trajectree/fock_optics/light_sources.py
15
+ trajectree/fock_optics/measurement.py
16
+ trajectree/fock_optics/noise_models.py
17
+ trajectree/fock_optics/outputs.py
18
+ trajectree/fock_optics/utils.py
19
+ trajectree/sequence/swap.py
@@ -0,0 +1,4 @@
1
+ quimb
2
+ numpy
3
+ scipy
4
+ matplotlib
@@ -0,0 +1 @@
1
+ trajectree
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "Trajectree"
7
+ dynamic = ["version"]
8
+ authors = [{ name="Ansh Singal", email="asingal@u.northwestern.edu" },]
9
+ description = "Trajectree is a quantum trajectory theory and tensor network based quantum optics simulator."
10
+ readme = "README.md"
11
+ requires-python = ">=3.9"
12
+ license = "MIT"
13
+ license-files = ["LICEN[CS]E*"]
14
+
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ dependencies=[
21
+ 'quimb',
22
+ 'numpy',
23
+ 'scipy',
24
+ 'matplotlib',
25
+ ]
26
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,115 @@
1
+ from ..fock_optics.measurement import *
2
+ from ..fock_optics.outputs import *
3
+
4
+ import numpy as np
5
+ import scipy.sparse as sp
6
+
7
+ # Support functions:
8
+ def create_op(left_indices, op, right_indices, N):
9
+ if left_indices == 0:
10
+ return sp.kron(op, sp.eye(N**right_indices))
11
+ elif right_indices == 0:
12
+ return sp.kron(sp.eye(N**left_indices), op)
13
+ else:
14
+ out_op = sp.kron(sp.eye(N**left_indices), op)
15
+ return sp.kron(out_op, sp.eye(N**right_indices))
16
+ def _find_mat_exp(mat):
17
+ ans = sp.eye(mat.shape[0])
18
+ intermediate = 1
19
+ for i in range(1, 50+1):
20
+ intermediate *= mat/i
21
+ intermediate.eliminate_zeros()
22
+ ans += intermediate
23
+ return ans
24
+ def read_quantum_state_sparse(sparse_state, N):
25
+ temp_sparse_state = sp.csr_matrix(sparse_state)
26
+ temp_sparse_state.data = np.round(temp_sparse_state.data, 10)
27
+ temp_sparse_state.eliminate_zeros()
28
+ labels = generate_labels(4,N)
29
+ state = temp_sparse_state.nonzero()[0]
30
+ print(f"{len(state)} non-zero elements Corresponding Basis terms:")
31
+ for k in state: print(labels[k],"-",k,"-",temp_sparse_state[k].data)
32
+
33
+ def extend_state_sparse(state):
34
+ return sp.kron(state, state)
35
+ # TMSV_state_dense = extend_state_sparse(TMSV_state)
36
+
37
+ def bell_state_measurement_sparse(TMSV_state_dense, N, efficiency, a_dag, is_dm = False):
38
+ # BSM BS implementation
39
+ BSM_H_0_Mode_op = create_op(2, a_dag, 5, N)
40
+ print(BSM_H_0_Mode_op.shape, len(BSM_H_0_Mode_op.nonzero()[0]), len(BSM_H_0_Mode_op.nonzero()[1]))
41
+ BSM_V_0_Mode_op = create_op(3, a_dag, 4, N)
42
+ BSM_H_1_Mode_op = create_op(6, a_dag, 1, N)
43
+ BSM_V_1_Mode_op = create_op(7, a_dag, 0, N)
44
+
45
+ hamiltonian_BS_H = -np.pi/4 * ( BSM_H_0_Mode_op.T@BSM_H_1_Mode_op - BSM_H_0_Mode_op@BSM_H_1_Mode_op.T )
46
+ unitary_BS_H = _find_mat_exp(hamiltonian_BS_H)
47
+
48
+ hamiltonian_BS_V = -np.pi/4 * ( BSM_V_0_Mode_op.T@BSM_V_1_Mode_op - BSM_V_0_Mode_op@BSM_V_1_Mode_op.T )
49
+ unitary_BS_V = _find_mat_exp(hamiltonian_BS_V)
50
+
51
+
52
+ # BSM povm implementation
53
+ povm_op_1 = sp.csr_matrix(create_threshold_POVM_OP_Dense(efficiency, 1, N))
54
+ povm_op_0 = sp.csr_matrix(create_threshold_POVM_OP_Dense(efficiency, 0, N))
55
+
56
+ BSM_povm = create_op(2, povm_op_1, 0, N)
57
+ BSM_povm = create_op(0, sp.kron(BSM_povm, povm_op_0), 2, N)
58
+ BSM_povm = sp.kron(BSM_povm, sp.kron(povm_op_0, povm_op_1))
59
+
60
+ # print(unitary_BS_V.shape, unitary_BS_H.shape, TMSV_state_dense.shape)
61
+
62
+ if is_dm:
63
+ post_BS_State = unitary_BS_V @ unitary_BS_H @ TMSV_state_dense @ (unitary_BS_V @ unitary_BS_H).conj().T
64
+ post_BSM_State = BSM_povm @ post_BS_State @ BSM_povm.conj().T
65
+ else:
66
+ post_BS_State = unitary_BS_V @ unitary_BS_H @ TMSV_state_dense
67
+ post_BSM_State = BSM_povm @ post_BS_State
68
+
69
+ # post_BSM_State.data = np.round(post_BSM_State.data, 10)
70
+ # post_BSM_State.eliminate_zeros()
71
+
72
+ return post_BSM_State
73
+ # post_BSM_State = bell_state_measurement_sparse(TMSV_state_dense, N, efficiency)
74
+
75
+ def rotate_and_measure_sparse(post_BSM_State, N, efficiency, a_dag):
76
+ # Polarization rotators mode operators
77
+ rotator_H_0_Mode_op = create_op(0, a_dag, 7, N)
78
+ rotator_V_0_Mode_op = create_op(1, a_dag, 6, N)
79
+ rotator_H_1_Mode_op = create_op(4, a_dag, 3, N)
80
+ rotator_V_1_Mode_op = create_op(5, a_dag, 2, N)
81
+
82
+ povm_op_1 = sp.csr_matrix(create_threshold_POVM_OP_Dense(efficiency, 1, N))
83
+
84
+ # polarization analysis detector POVMs
85
+ pol_analyzer_povm = create_op(0, povm_op_1, 3, N)
86
+ pol_analyzer_povm = create_op(0, sp.kron(pol_analyzer_povm, povm_op_1), 3, N)
87
+
88
+ # Applying rotations and measuring
89
+
90
+ signal_angles = np.linspace(0, np.pi, 10)
91
+ # idler_angles = np.linspace(0, np.pi, 20)
92
+ idler_angles = [0]
93
+ coincidence = []
94
+
95
+ for i, idler_angle in enumerate(idler_angles):
96
+ coincidence_probs = []
97
+
98
+ hamiltonian_rotator_1 = -idler_angle * ( rotator_H_1_Mode_op.T@rotator_V_1_Mode_op - rotator_H_1_Mode_op@rotator_V_1_Mode_op.T )
99
+ unitary_rotator_1 = _find_mat_exp(hamiltonian_rotator_1)
100
+ post_idler_detection_state = unitary_rotator_1 @ post_BSM_State
101
+ # post_idler_detection_state = post_BSM_State
102
+
103
+ for j, angle in enumerate(signal_angles):
104
+ # print("idler:", i, "signal:", j)
105
+
106
+ hamiltonian_rotator_0 = -angle * ( rotator_H_0_Mode_op.T@rotator_V_0_Mode_op - rotator_H_0_Mode_op@rotator_V_0_Mode_op.T )
107
+ unitary_rotator_0 = _find_mat_exp(hamiltonian_rotator_0)
108
+ post_rotations_state = unitary_rotator_0 @ post_idler_detection_state
109
+
110
+ measured_state = pol_analyzer_povm @ post_rotations_state
111
+
112
+ coincidence_probs.append(sp.linalg.norm(measured_state)**2)
113
+ coincidence.append(coincidence_probs)
114
+ return coincidence, idler_angles
115
+ # coincidence, idler_angles = rotate_and_measure_sparse(post_BSM_State, N, efficiency)
@@ -0,0 +1,58 @@
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
+
8
+ import qutip as qt
9
+
10
+ # Beamsplitter transformation
11
+ def create_BS_MPO(site1, site2, theta, total_sites, N, tag = 'BS'):
12
+
13
+ a = qt.destroy(N).full()
14
+ a_dag = a.T
15
+ I = np.eye(N)
16
+
17
+ # This corresponds to the BS hamiltonian:
18
+
19
+ hamiltonian_BS = -theta * ( kron(I, a_dag)@kron(a, I) - kron(I, a)@kron(a_dag, I) )
20
+ unitary_BS = expm(hamiltonian_BS)
21
+
22
+ # print("unitary_BS", unitary_BS)
23
+
24
+ BS_MPO = mpo.from_dense(unitary_BS, dims = N, sites = (site1,site2), L=total_sites, tags=tag)
25
+ # BS_MPO = BS_MPO.fill_empty_sites(mode = "full")
26
+ return BS_MPO
27
+
28
+
29
+ def generalized_mode_mixer(site1, site2, theta, phi, psi, lamda, total_sites, N, tag = 'MM'):
30
+
31
+ a = qt.destroy(N).full()
32
+ a_dag = a.T
33
+ I = np.eye(N)
34
+
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)
39
+
40
+ # print("unitary_BS\n", np.round(unitary_BS, 4))
41
+
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))
45
+
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)
48
+
49
+ # print("generalized_BS\n", np.round(generalized_BS, 4))
50
+
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")
53
+ return BS_MPO
54
+
55
+
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)
@@ -0,0 +1,123 @@
1
+ from .utils import create_MPO
2
+ from .devices import create_BS_MPO
3
+
4
+ from scipy import sparse as sp
5
+ from scipy.linalg import expm
6
+
7
+ import numpy as np
8
+ from numpy.linalg import matrix_power
9
+ from numpy import kron, sqrt
10
+
11
+ from quimb.tensor.tensor_arbgeom import tensor_network_apply_op_vec #type: ignore
12
+ from quimb.tensor.tensor_1d_compress import enforce_1d_like #type: ignore
13
+
14
+ import qutip as qt
15
+ from math import factorial
16
+
17
+
18
+ def create_TMSV_OP_Dense(N, mean_photon_num):
19
+ a = qt.destroy(N).full()
20
+ a_dag = a.T
21
+ truncation = (N-1)
22
+
23
+ op = expm(1j * mean_photon_num * (kron(a_dag, a_dag) + kron(a, a)))
24
+
25
+ return op
26
+
27
+
28
+
29
+ ########## Light Source ###########
30
+
31
+ def light_source(vacuum, N, mean_photon_num, num_modes, error_tolerance, TMSV_indices = ((0,2),(5,7)), compress = True, contract = True):
32
+
33
+ psi = vacuum.copy()
34
+ psi.add_tag("L0")
35
+ site_tags = psi.site_tags
36
+
37
+ # Creating TMSV ops:
38
+ TMSV_op_dense = create_TMSV_OP_Dense(N, mean_photon_num)
39
+
40
+ TMSV_MPO_H = create_MPO(site1 = TMSV_indices[0][0], site2 = TMSV_indices[0][1], total_sites = num_modes, op = TMSV_op_dense, N = N, tag = r"$TMSV_H$")
41
+ # TMSV_MPO_H.draw()
42
+ # print("sites present in light_source:", TMSV_MPO_H.sites)
43
+ enforce_1d_like(TMSV_MPO_H, site_tags=site_tags, inplace=True)
44
+ # print("sites present in light_source:", TMSV_MPO_H.sites)
45
+ TMSV_MPO_H.add_tag("L1")
46
+
47
+ TMSV_MPO_V = create_MPO(site1 = TMSV_indices[1][0], site2 = TMSV_indices[1][1], total_sites = num_modes, op = TMSV_op_dense, N = N, tag = r"$TMSV_V$")
48
+ enforce_1d_like(TMSV_MPO_V, site_tags=site_tags, inplace=True)
49
+ TMSV_MPO_V.add_tag("L1")
50
+
51
+ # Creating PBS ops:
52
+ U_PBS_H_Signal = create_BS_MPO(site1 = 2, site2 = 6, theta=np.pi/2, total_sites = num_modes, N = N, tag = r"$PBS_S$")
53
+ enforce_1d_like(U_PBS_H_Signal, site_tags=site_tags, inplace=True)
54
+ U_PBS_H_Signal.add_tag("L1")
55
+
56
+ U_PBS_H_Idler = create_BS_MPO(site1 = 0, site2 = 4, theta=np.pi/2, total_sites = num_modes, N = N, tag = r"$PBS_I$")
57
+ enforce_1d_like(U_PBS_H_Idler, site_tags=site_tags, inplace=True)
58
+ U_PBS_H_Signal.add_tag("L1")
59
+
60
+ # Create entangled state:
61
+ psi = tensor_network_apply_op_vec(TMSV_MPO_H, psi, compress=compress, contract = contract, cutoff = error_tolerance)
62
+ psi = tensor_network_apply_op_vec(TMSV_MPO_V, psi, compress=compress, contract = contract, cutoff = error_tolerance)
63
+ psi = tensor_network_apply_op_vec(U_PBS_H_Idler, psi, compress=compress, contract = contract, cutoff = error_tolerance)
64
+ psi = tensor_network_apply_op_vec(U_PBS_H_Signal, psi, compress=compress, contract = contract, cutoff = error_tolerance)
65
+
66
+ psi.normalize()
67
+
68
+ # print("trace is:", np.linalg.norm(psi.to_dense()))
69
+
70
+ for _ in range(4):
71
+ psi.measure(0, remove = True, renorm = True, inplace = True)
72
+
73
+ # Not used for TN implermentation. Used for validating impelmentation with dense version
74
+ TMSV_state = psi.to_dense()
75
+ TMSV_state = np.reshape(TMSV_state.data, (-1, 1), order = 'C')
76
+ TMSV_state = sp.csr_matrix(TMSV_state)
77
+ TMSV_state.data = np.round(TMSV_state.data, 10)
78
+ TMSV_state.eliminate_zeros()
79
+
80
+ return psi, TMSV_state
81
+
82
+
83
+ # Generate truncation filter MPO
84
+ # TODO: Make a function to renormalize a quantum state. How: find the projection of the quantum state onto itself and calculate the
85
+ # probability. Next, take the square root of this number, divide it by the number nodes in the quantum state and multiply it with
86
+ # all the states in the MPS. For density matrices, simply find the trace directly and do the same thing as the previous example except
87
+ # for not taking the square root. The truncation filter would not work without the renormalization
88
+ def create_truncation_filter_Dense(truncation):
89
+ # This is only the projection operator. The states need to be normalized first.
90
+ N = truncation+1
91
+ vacuum = np.zeros(N**2)
92
+ vacuum[0] = 1
93
+
94
+ a = qt.destroy(N).full()
95
+ a_dag = a.T
96
+ I = np.eye(N)
97
+
98
+ # # debug
99
+ # labels = generate_labels(1,N)
100
+
101
+ op = 0
102
+ for trunc in range(truncation, -1, -1):
103
+ state = kron(matrix_power(a_dag, trunc), I) @ vacuum / sqrt(factorial(trunc) * factorial(0))
104
+ op+=np.outer(state, state)
105
+ coeffs = [trunc+1, 0]
106
+
107
+ # # Debug
108
+ # state_inds = state.nonzero()[0]
109
+ # print("TMSV state:", [labels[i] for i in state_inds], "Val:", state[state_inds[0]])
110
+ # print("coeffs", coeffs)
111
+
112
+ for i in range(trunc):
113
+ coeffs = [coeffs[0]-1, coeffs[1]+1]
114
+ state = kron(a, a_dag) @ state / sqrt((coeffs[0]) * (coeffs[1]))
115
+ op += np.outer(state, state)
116
+
117
+
118
+ # # debug
119
+ # state_inds = state.nonzero()[0]
120
+ # print("TMSV state:", [labels[i] for i in state_inds], "Val:", state[state_inds[0]])
121
+ # print("coeffs", coeffs)
122
+
123
+ return op
@@ -0,0 +1,236 @@
1
+ from .devices import generalized_mode_mixer, create_BS_MPO
2
+ from ..trajectory import quantum_channel
3
+ from .noise_models import single_mode_bosonic_noise_channels
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
+ else:
84
+ dense_op = sqrtm(create_threshold_POVM_OP_Dense(efficiency, outcome, N)).astype(np.complex128)
85
+
86
+ sqrt_POVM_MPOs = []
87
+ for i in sites:
88
+ sqrt_POVM_MPOs.append(mpo.from_dense(dense_op, dims = N, sites = (i,), L=total_sites, tags=tag))
89
+
90
+ return sqrt_POVM_MPOs
91
+
92
+
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):
94
+
95
+ """Perform Bell state measrement or return the MPOs used in the measurement.
96
+ Args:
97
+ psi (mps): The input state to be measured.
98
+ N (int): local Hilbert space dimension
99
+ site_tags (list): The tags for the sites in the MPS.
100
+ num_modes (int): The number of modes in the MPS.
101
+ efficiencies list[float]: The efficiencies of the (pairs of) detectors in the BSM.
102
+ error_tolerance (float): The error tolerance for the tensor network.
103
+ measurements (dict): The sites for the measurements. Default is {1:(2,7), 0:(3,6)}.
104
+ pnr (bool): Whether to use photon number resolving measurement. Default is False.
105
+ 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.
106
+ return_MPOs (bool): Whether to return the MPOs used in the measurement. Default is False.
107
+ compress (bool): Whether to compress the MPS after applying the MPOs. Default is True.
108
+ contract (bool): Whether to contract the MPS after applying the MPOs. Default is True.
109
+
110
+ Returns:
111
+ mps: The measured state after the Bell state measurement.
112
+
113
+ """
114
+
115
+ 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}$")
116
+ enforce_1d_like(U_BS_H, site_tags=site_tags, inplace=True)
117
+ U_BS_H.add_tag("L2")
118
+
119
+ 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}$")
120
+ enforce_1d_like(U_BS_V, site_tags=site_tags, inplace=True)
121
+ U_BS_V.add_tag("L3")
122
+
123
+ # Note that these are not used if using trajectree to implement detector inefficiency.
124
+ 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)
125
+ 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))
126
+
127
+ if return_MPOs:
128
+ returned_MPOs = [U_BS_H, U_BS_V]
129
+ if use_trajectory:
130
+ 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]
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))) # 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))) # 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))) # 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))) # The tuples in this list are defined as (sites, kraus_ops). The sites are the sites where the Kraus ops are applied.
145
+
146
+ 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
+ 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
+
149
+ det_quantum_channels = [quantum_channel(N = N, num_modes = num_modes, formalism = "closed", unitary_MPOs = DET_MPO, name = "DET") for DET_MPO in BSM_POVM_1_OPs]
150
+ quantum_channel_list.extend(det_quantum_channels)
151
+
152
+ return quantum_channel_list
153
+
154
+ returned_MPOs.extend(BSM_POVM_1_OPs) # Collect all the MPOs in a list and return them. The operators are ordered as such:
155
+
156
+ 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]
157
+
158
+ return quantum_channel_list
159
+
160
+ psi = tensor_network_apply_op_vec(U_BS_H, psi, compress=compress, contract = contract, cutoff = error_tolerance)
161
+ psi = tensor_network_apply_op_vec(U_BS_V, psi, compress=compress, contract = contract, cutoff = error_tolerance)
162
+
163
+ for POVM_OP in BSM_POVM_1_OPs:
164
+ POVM_OP.add_tag("L4")
165
+ psi = tensor_network_apply_op_vec(POVM_OP, psi, compress=compress, contract = contract, cutoff = error_tolerance)
166
+
167
+ return psi
168
+
169
+
170
+
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):
172
+ # idler_angles = [0]
173
+ # angles = [np.pi/4]
174
+
175
+ coincidence = []
176
+
177
+ POVM_1_OPs = generate_sqrt_POVM_MPO(sites = measurements[1], outcome = det_outcome, total_sites=num_modes, efficiency=efficiency, N=N, pnr = pnr)
178
+ 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
+
182
+ meas_ops = POVM_1_OPs
183
+ meas_ops.extend(POVM_0_OPs)
184
+
185
+ for i, idler_angle in enumerate(idler_angles):
186
+ coincidence_probs = []
187
+
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
+
194
+
195
+ enforce_1d_like(rotator_node_1, site_tags=site_tags, inplace=True)
196
+ 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.
198
+ idler_rotated_psi = tensor_network_apply_op_vec(rotator_node_1, psi, compress=compress, contract = contract, cutoff = error_tolerance)
199
+
200
+
201
+ for j, angle in enumerate(signal_angles):
202
+ # print("idler:", i, "signal:", j)
203
+
204
+ # 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
+ ##########################
206
+ # 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
+ # 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
+
210
+
211
+ enforce_1d_like(rotator_node_2, site_tags=site_tags, inplace=True)
212
+
213
+ if return_MPOs:
214
+ meas_ops = [rotator_node_1, rotator_node_2] + meas_ops # Collect all the MPOs in a list and return them
215
+ return meas_ops
216
+
217
+ # Rotate and measure:
218
+ rotator_node_2.add_tag("L5")
219
+ rho_rotated = tensor_network_apply_op_vec(rotator_node_2, idler_rotated_psi, compress=compress, contract = contract, cutoff = error_tolerance)
220
+
221
+ # read_quantum_state(psi)
222
+ # read_quantum_state(rho_rotated)
223
+
224
+ for POVM_OP in meas_ops:
225
+ POVM_OP.add_tag("L6")
226
+ rho_rotated = tensor_network_apply_op_vec(POVM_OP, rho_rotated, compress=compress, contract = contract, cutoff = error_tolerance)
227
+
228
+ if draw:
229
+ # only for drawing the TN. Not used otherwise
230
+ fix = {(f"L{j}",f"I{num_modes - i-1}"):(3*j,i+5) for j in range(10) for i in range(10)}
231
+ 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)
232
+ # rho_rotated.draw_tn()
233
+ coincidence_probs.append((rho_rotated.norm())**2)
234
+ coincidence.append(coincidence_probs)
235
+
236
+ return np.array(coincidence)