interferometer 1.0__tar.gz → 1.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: interferometer
3
- Version: 1.0
3
+ Version: 1.1.1
4
4
  Summary: Algorithms for universal interferometers
5
5
  Author-email: "William R. Clements" <mail@william-clements.com>
6
6
  Project-URL: Homepage, https://github.com/clementsw/interferometer
@@ -13,6 +13,8 @@ Classifier: Topic :: Scientific/Engineering :: Physics
13
13
  Requires-Python: >=3.7
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
+ Requires-Dist: numpy
17
+ Requires-Dist: matplotlib
16
18
 
17
19
  # Interferometer package
18
20
 
@@ -0,0 +1 @@
1
+ from interferometer.main import Interferometer, Beamsplitter, triangle_decomposition, square_decomposition, random_unitary
@@ -0,0 +1,295 @@
1
+ import numpy as np
2
+
3
+
4
+ class Beamsplitter:
5
+ """This class defines a beam splitter
6
+
7
+ The matrix describing the mode transformation is:
8
+
9
+ e^{iphi}*cos(theta) -sin(theta)
10
+ e^{iphi}*sin(theta) cos(theta)
11
+
12
+ Args:
13
+ mode1 (int): the index of the first mode (the first mode is mode 1)
14
+ mode2 (int): the index of the second mode
15
+ theta (float): the beam splitter angle
16
+ phi (float): the beam splitter phase
17
+ """
18
+
19
+ def __init__(self, mode1, mode2, theta, phi):
20
+ self.mode1 = mode1
21
+ self.mode2 = mode2
22
+ self.theta = theta
23
+ self.phi = phi
24
+
25
+ def __repr__(self):
26
+ repr = "\n Beam splitter between modes {} and {}: \n Theta angle: {:.2f} \n Phase: {:.2f}".format(
27
+ self.mode1,
28
+ self.mode2,
29
+ self.theta,
30
+ self.phi
31
+ )
32
+ return repr
33
+
34
+
35
+ class Interferometer:
36
+ """This class defines an interferometer.
37
+
38
+ An interferometer contains an ordered list of variable beam splitters,
39
+ represented here by BS_list. For BS in BS_list, BS[0] and BS[1] correspond to the labels of the two modes
40
+ being interfered (which start at 1). The beam splitters implement the optical transformation defined in equation 1 of:
41
+ Clements, William R., et al. "Optimal design for universal multiport interferometers." Optica 3.12 (2016): 1460-1465.
42
+ This transformation is parametrized by BS[2] (theta) which determines the beam splitter reflectivity,
43
+ and by BS[3] (phi). The interferometer also contains a list of output phases described by output_phases.
44
+ """
45
+
46
+ def __init__(self):
47
+ self.BS_list = []
48
+ self.output_phases = []
49
+
50
+ def add_BS(self, BS):
51
+ """Adds a beam splitter at the output of the current interferometer
52
+
53
+ Args:
54
+ BS (Beamsplitter): a Beamsplitter instance
55
+ """
56
+ self.BS_list.append(BS)
57
+
58
+ def add_phase(self, mode, phase):
59
+ """Use this to manually add a phase shift to a selected mode at the output of the interferometer
60
+
61
+ Args:
62
+ mode (int): the mode index. The first mode is mode 1
63
+ phase (float): the real-valued phase to add
64
+ """
65
+ while mode > np.size(self.output_phases):
66
+ self.output_phases.append(0)
67
+ self.output_phases[mode-1] = phase
68
+
69
+ def count_modes(self):
70
+ """Calculate number of modes involved in the transformation.
71
+
72
+ This is required for calculate_transformation and draw
73
+
74
+ Returns:
75
+ the number of modes in the transformation
76
+ """
77
+ highest_index = max([max([BS.mode1, BS.mode2]) for BS in self.BS_list])
78
+ return highest_index
79
+
80
+ def calculate_transformation(self):
81
+ """Calculate unitary matrix describing the transformation implemented by the interferometer
82
+
83
+ Returns:
84
+ complex-valued 2D numpy array representing the interferometer
85
+ """
86
+ N = int(self.count_modes())
87
+ U = np.eye(N, dtype=np.complex_)
88
+
89
+ for BS in self.BS_list:
90
+ T = np.eye(N, dtype=np.complex_)
91
+ T[BS.mode1 - 1, BS.mode1 - 1] = np.exp(1j * BS.phi) * np.cos(BS.theta)
92
+ T[BS.mode1 - 1, BS.mode2 - 1] = -np.sin(BS.theta)
93
+ T[BS.mode2 - 1, BS.mode1 - 1] = np.exp(1j * BS.phi) * np.sin(BS.theta)
94
+ T[BS.mode2 - 1, BS.mode2 - 1] = np.cos(BS.theta)
95
+ U = np.matmul(T,U)
96
+
97
+ while np.size(self.output_phases) < N: # Autofill for users who don't want to bother with output phases
98
+ self.output_phases.append(0)
99
+
100
+ D = np.diag(np.exp([1j * phase for phase in self.output_phases]))
101
+ U = np.matmul(D,U)
102
+ return U
103
+
104
+ def draw(self, show_plot=True):
105
+ """Use matplotlib to make a drawing of the interferometer
106
+
107
+ Args:
108
+ show_plot (bool): whether to show the generated plot
109
+ """
110
+
111
+ import matplotlib.pyplot as plt
112
+ plt.figure()
113
+ N = self.count_modes()
114
+ mode_tracker = np.zeros(N)
115
+
116
+ for ii in range(N):
117
+ plt.plot((-1, 0), (ii, ii), lw=1, color="blue")
118
+
119
+ for BS in self.BS_list:
120
+ x = np.max([mode_tracker[BS.mode1 - 1], mode_tracker[BS.mode2 - 1]])
121
+ plt.plot((x+0.3, x+1), (N - BS.mode1, N - BS.mode2), lw=1, color="blue")
122
+ plt.plot((x, x+0.3), (N - BS.mode1, N - BS.mode1), lw=1, color="blue")
123
+ plt.plot((x, x+0.3), (N - BS.mode2, N - BS.mode2), lw=1, color="blue")
124
+ plt.plot((x+0.3, x+1), (N - BS.mode2, N - BS.mode1), lw=1, color="blue")
125
+ plt.plot((x+0.4, x+0.9), (N - (BS.mode2 + BS.mode1)/2, N - (BS.mode2 + BS.mode1)/2), lw=1, color="blue")
126
+ reflectivity = "{:2f}".format(np.cos(BS.theta)**2)
127
+ plt.text(x+0.9, N + 0.05 - (BS.mode2 + BS.mode1)/2, reflectivity[0:3], color="green", fontsize=7)
128
+
129
+ plt.plot((x+0.15, x+0.15), (N+0.3-(BS.mode2 + BS.mode1)/2., N+0.7-(BS.mode2 + BS.mode1)/2.), lw=1, color="blue")
130
+ circle = plt.Circle((x+0.15, N+0.5-(BS.mode2 + BS.mode1)/2.), 0.1, fill=False)
131
+ plt.gca().add_patch(circle)
132
+ phase = "{:2f}".format(BS.phi)
133
+ if BS.phi > 0:
134
+ plt.text(x+0.2, N+0.7-(BS.mode2 + BS.mode1)/2., phase[0:3], color="red", fontsize=7)
135
+ else:
136
+ plt.text(x+0.2, N+0.7-(BS.mode2 + BS.mode1)/2., phase[0:4], color="red", fontsize=7)
137
+ if x > mode_tracker[BS.mode1-1]:
138
+ plt.plot((mode_tracker[BS.mode1-1], x), (N-BS.mode1, N-BS.mode1), lw=1, color="blue")
139
+ if x > mode_tracker[BS.mode2-1]:
140
+ plt.plot((mode_tracker[BS.mode2-1], x), (N-BS.mode2, N-BS.mode2), lw=1, color="blue")
141
+ mode_tracker[BS.mode1-1] = x+1
142
+ mode_tracker[BS.mode2-1] = x+1
143
+
144
+ max_x = np.max(mode_tracker)
145
+ for ii in range(N):
146
+ plt.plot((mode_tracker[ii], max_x+1), (N-ii-1, N-ii-1), lw=1, color="blue")
147
+ while np.size(self.output_phases) < N: # Autofill for users who don't want to bother with output phases
148
+ self.output_phases.append(0)
149
+ if self.output_phases[ii] != 0:
150
+ plt.plot((max_x+0.5, max_x+0.5), (N-ii-1.2, N-ii-0.8), lw=1, color="blue")
151
+ circle = plt.Circle((max_x+0.5, N-ii-1), 0.1, fill=False)
152
+ plt.gca().add_patch(circle)
153
+ phase = str(self.output_phases[ii])
154
+ if BS.phi > 0:
155
+ plt.text(max_x+0.6, N-ii-0.8, phase[0:3], color="red", fontsize=7)
156
+ else:
157
+ plt.text(max_x+0.6, N-ii-0.8, phase[0:4], color="red", fontsize=7)
158
+
159
+
160
+ plt.text(max_x/2, -0.7, "green: BS reflectivity", color="green", fontsize=10)
161
+ plt.text(max_x/2, -1.4, "red: phase shift", color="red", fontsize=10)
162
+ plt.text(-1, N-0.3, "Light in", fontsize=10)
163
+ plt.text(max_x+0.5, N-0.3, "Light out", fontsize=10)
164
+ plt.gca().axes.set_ylim([-1.8, N+0.2])
165
+ plt.axis("off")
166
+ if show_plot:
167
+ plt.show()
168
+
169
+
170
+ def triangle_decomposition(U):
171
+ """Returns a triangular mesh of beam splitters implementing matrix U
172
+
173
+ This code implements the decomposition algorithm in:
174
+ Reck, Michael, et al. "Experimental realization of any discrete unitary operator."
175
+ Physical review letters 73.1 (1994): 58.
176
+
177
+ Args:
178
+ U (np.ndarray): complex-valued 2D numpy array representing the interferometer
179
+
180
+ Returns:
181
+ an Interferometer instance
182
+ """
183
+ I = Interferometer()
184
+ N = int(np.sqrt(U.size))
185
+ for ii in range(N-1):
186
+ for jj in range(N-1-ii):
187
+ modes = [N - jj - 1, N - jj]
188
+ theta = custom_arctan(U[ii, N - 1 - jj], U[ii, N - 2 - jj])
189
+ phi = -custom_angle(-U[ii, N - 1 - jj], U[ii, N - 2 - jj])
190
+ invT = np.eye(N, dtype=np.complex_)
191
+ invT[modes[0]-1, modes[0]-1] = np.exp(-1j * phi) * np.cos(theta)
192
+ invT[modes[0]-1, modes[1]-1] = np.exp(-1j * phi) * np.sin(theta)
193
+ invT[modes[1]-1, modes[0]-1] = -np.sin(theta)
194
+ invT[modes[1]-1, modes[1]-1] = np.cos(theta)
195
+ U = np.matmul(U, invT)
196
+ I.BS_list.append(Beamsplitter(modes[0], modes[1], theta, phi))
197
+ phases = np.diag(U)
198
+ I.output_phases = [np.angle(i) for i in phases]
199
+ return I
200
+
201
+
202
+ def square_decomposition(U):
203
+ """Returns a rectangular mesh of beam splitters implementing matrix U
204
+
205
+ This code implements the decomposition algorithm in:
206
+ Clements, William R., et al. "Optimal design for universal multiport interferometers."
207
+ Optica 3.12 (2016): 1460-1465.
208
+
209
+ Returns:
210
+ an Interferometer instance
211
+ """
212
+ I = Interferometer()
213
+ N = int(np.sqrt(U.size))
214
+ left_T = []
215
+ for ii in range(N-1):
216
+ if np.mod(ii, 2) == 0:
217
+ for jj in range(ii+1):
218
+ modes = [ii - jj + 1, ii + 2 - jj]
219
+ theta = custom_arctan(U[N-1-jj, ii-jj], U[N-1-jj, ii-jj+1])
220
+ phi = custom_angle(U[N-1-jj, ii-jj], U[N-1-jj, ii-jj+1])
221
+ invT = np.eye(N, dtype=np.complex_)
222
+ invT[modes[0]-1, modes[0]-1] = np.exp(-1j * phi) * np.cos(theta)
223
+ invT[modes[0]-1, modes[1]-1] = np.exp(-1j * phi) * np.sin(theta)
224
+ invT[modes[1]-1, modes[0]-1] = -np.sin(theta)
225
+ invT[modes[1]-1, modes[1]-1] = np.cos(theta)
226
+ U = np.matmul(U, invT)
227
+ I.BS_list.append(Beamsplitter(modes[0], modes[1], theta, phi))
228
+ else:
229
+ for jj in range(ii+1):
230
+ modes = [N+jj-ii-1, N+jj-ii]
231
+ theta = custom_arctan(U[N+jj-ii-1, jj], U[N+jj-ii-2, jj])
232
+ phi = custom_angle(-U[N+jj-ii-1, jj], U[N+jj-ii-2, jj])
233
+ T = np.eye(N, dtype=np.complex_)
234
+ T[modes[0]-1, modes[0]-1] = np.exp(1j * phi) * np.cos(theta)
235
+ T[modes[0]-1, modes[1]-1] = -np.sin(theta)
236
+ T[modes[1]-1, modes[0]-1] = np.exp(1j * phi) * np.sin(theta)
237
+ T[modes[1]-1, modes[1]-1] = np.cos(theta)
238
+ U = np.matmul(T, U)
239
+ left_T.append(Beamsplitter(modes[0], modes[1], theta, phi))
240
+
241
+ for BS in np.flip(left_T, 0):
242
+ modes = [int(BS.mode1), int(BS.mode2)]
243
+ invT = np.eye(N, dtype=np.complex_)
244
+ invT[modes[0]-1, modes[0]-1] = np.exp(-1j * BS.phi) * np.cos(BS.theta)
245
+ invT[modes[0]-1, modes[1]-1] = np.exp(-1j * BS.phi) * np.sin(BS.theta)
246
+ invT[modes[1]-1, modes[0]-1] = -np.sin(BS.theta)
247
+ invT[modes[1]-1, modes[1]-1] = np.cos(BS.theta)
248
+ U = np.matmul(invT, U)
249
+ theta = custom_arctan(U[modes[1]-1, modes[0]-1], U[modes[1]-1, modes[1]-1])
250
+ phi = custom_angle(U[modes[1]-1, modes[0]-1], U[modes[1]-1, modes[1]-1])
251
+ invT[modes[0]-1, modes[0]-1] = np.exp(-1j * phi) * np.cos(theta)
252
+ invT[modes[0]-1, modes[1]-1] = np.exp(-1j * phi) * np.sin(theta)
253
+ invT[modes[1]-1, modes[0]-1] = -np.sin(theta)
254
+ invT[modes[1]-1, modes[1]-1] = np.cos(theta)
255
+ U = np.matmul(U, invT)
256
+ I.BS_list.append(Beamsplitter(modes[0], modes[1], theta, phi))
257
+ phases = np.diag(U)
258
+ I.output_phases = [np.angle(i) for i in phases]
259
+ return I
260
+
261
+
262
+ def random_unitary(N):
263
+ """Returns a random NxN unitary matrix
264
+
265
+ This code is inspired by Matlab code written by Toby Cubitt:
266
+ http://www.dr-qubit.org/matlab/randU.m
267
+
268
+ Args:
269
+ N (int): dimension of the NxN unitary matrix to generate
270
+
271
+ Returns:
272
+ complex-valued 2D numpy array representing the interferometer
273
+ """
274
+ X = np.zeros([N, N], dtype=np.complex_)
275
+ for ii in range(N):
276
+ for jj in range(N):
277
+ X[ii, jj] = (np.random.normal() + 1j * np.random.normal()) / np.sqrt(2)
278
+
279
+ q, r = np.linalg.qr(X)
280
+ r = np.diag(np.divide(np.diag(r), abs(np.diag(r))))
281
+ U = np.matmul(q, r)
282
+
283
+ return U
284
+
285
+ def custom_arctan(x1, x2):
286
+ if x2 != 0:
287
+ return np.arctan(abs(x1/x2))
288
+ else:
289
+ return np.pi/2
290
+
291
+ def custom_angle(x1, x2):
292
+ if x2 != 0:
293
+ return np.angle(x1/x2)
294
+ else:
295
+ return 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: interferometer
3
- Version: 1.0
3
+ Version: 1.1.1
4
4
  Summary: Algorithms for universal interferometers
5
5
  Author-email: "William R. Clements" <mail@william-clements.com>
6
6
  Project-URL: Homepage, https://github.com/clementsw/interferometer
@@ -13,6 +13,8 @@ Classifier: Topic :: Scientific/Engineering :: Physics
13
13
  Requires-Python: >=3.7
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
+ Requires-Dist: numpy
17
+ Requires-Dist: matplotlib
16
18
 
17
19
  # Interferometer package
18
20
 
@@ -1,8 +1,11 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
+ interferometer/__init__.py
5
+ interferometer/main.py
4
6
  interferometer.egg-info/PKG-INFO
5
7
  interferometer.egg-info/SOURCES.txt
6
8
  interferometer.egg-info/dependency_links.txt
7
9
  interferometer.egg-info/requires.txt
8
- interferometer.egg-info/top_level.txt
10
+ interferometer.egg-info/top_level.txt
11
+ tests/test_interferometer.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "interferometer"
7
- version = "1.0"
7
+ version = "1.1.1"
8
8
  authors = [
9
9
  { name="William R. Clements", email="mail@william-clements.com" },
10
10
  ]
@@ -28,5 +28,5 @@ dependencies = [
28
28
  "Bug Tracker" = "https://github.com/clementsw/interferometer/issues"
29
29
 
30
30
  [tool.setuptools]
31
- py-modules = ["interferometer"]
31
+ packages = ["interferometer"]
32
32
 
@@ -0,0 +1,17 @@
1
+ import numpy as np
2
+ from unittest import TestCase
3
+
4
+ from interferometer import random_unitary, triangle_decomposition, square_decomposition
5
+
6
+ class TestInterferometer(TestCase):
7
+
8
+ def test_triangle_interferometer(self):
9
+ U = random_unitary(5)
10
+ I = triangle_decomposition(U)
11
+ self.assertTrue(abs(np.max(I.calculate_transformation() - U)) < 1e-14)
12
+
13
+ def test_square_interferometer(self):
14
+ U = random_unitary(5)
15
+ I = square_decomposition(U)
16
+ self.assertTrue(abs(np.max(I.calculate_transformation() - U)) < 1e-14)
17
+
File without changes
File without changes
File without changes