multiport-interferometer 0.0.1__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.
@@ -0,0 +1,213 @@
1
+
2
+ # Portions of this file are derived from Strawberry Fields
3
+ # Copyright 2019 Xanadu Quantum Technologies Inc.
4
+ #
5
+ # Modifications Copyright 2026 Carl Abi Nakad
6
+ #
7
+ # Modified from:
8
+ # https://github.com/XanaduAI/strawberryfields
9
+ #
10
+ # Licensed under the Apache License, Version 2.0.
11
+ # See the LICENSE file for details.
12
+
13
+ import numpy as np
14
+ from collections import defaultdict
15
+
16
+ def _rectangular_compact_init(
17
+ U, rtol=1e-12, atol=1e-12
18
+ ): # pylint: disable=too-many-statements, too-many-branches
19
+ r"""Rectangular decomposition of a unitary with sMZIs and phase-shifters, as given in FIG. 3 and "The Clements Scheme" section of (arXiv:2104.0756).
20
+
21
+ Args:
22
+ U (array): unitary matrix
23
+
24
+ Returns:
25
+ dict: A dictionary containing the following items:
26
+
27
+ * ``m``: the length of the matrix
28
+ * ``phi_ins``: parameter of the phase-shifter at the beginning of the mode
29
+ * ``sigmas``: parameter of the sMZI :math:`\frac{(\theta_1+\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
30
+ * ``deltas``: parameter of the sMZI :math:`\frac{(\theta_1-\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
31
+ * ``zetas``: parameter of the phase-shifter at the middle of the mode
32
+ * ``phi_outs``: parameter of the phase-shifter at the end of the mode
33
+
34
+ """
35
+
36
+ V = U.conj()
37
+ m = U.shape[0]
38
+
39
+ phases = {}
40
+ phases["m"] = m
41
+ phases["phi_ins"] = {} # mode : phi
42
+ phases["deltas"] = {} # (mode, layer) : delta
43
+ phases["sigmas"] = {} # (mode, layer) : sigma
44
+ phases["zetas"] = {} # mode : zeta
45
+ phases["phi_outs"] = {} # mode : phi
46
+
47
+ for j in range(m - 1):
48
+ if j % 2 == 0:
49
+ x = m - 1
50
+ y = j
51
+ phi_j = np.angle(V[x, y + 1]) - np.angle(V[x, y]) # reversed order from paper
52
+ V = V @ P(j, phi_j, m)
53
+ phases["phi_ins"][j] = phi_j
54
+ for k in range(j + 1):
55
+ if V[x, y] == 0:
56
+ delta = 0.5 * np.pi
57
+ else:
58
+ delta = np.arctan2(-abs(V[x, y + 1]), abs(V[x, y]))
59
+ n = j - k
60
+ V_temp = V @ M(n, 0, delta, m)
61
+ sigma = np.angle(V_temp[x - 1, y - 1]) - np.angle(V_temp[x - 1, y])
62
+ V = V @ M(n, sigma, delta, m)
63
+ phases["deltas"][n, k] = delta
64
+ phases["sigmas"][n, k] = sigma
65
+ x -= 1
66
+ y -= 1
67
+ else:
68
+ x = m - j - 1
69
+ y = 0
70
+ phi_j = np.angle(V[x - 1, y]) - np.angle(V[x, y])
71
+ V = P(x, phi_j, m) @ V
72
+ phases["phi_outs"][x] = phi_j
73
+ for k in range(j + 1):
74
+ if V[x, y] == 0.0:
75
+ delta = 0.5 * np.pi
76
+ else:
77
+ delta = np.arctan2(abs(V[x - 1, y]), abs(V[x, y]))
78
+ V_temp = M(x - 1, 0, delta, m) @ V
79
+ n = m + k - j - 2
80
+ if j != k:
81
+ sigma = np.angle(V_temp[x + 1, y + 1]) - np.angle(V_temp[x, y + 1])
82
+ else:
83
+ sigma = 0
84
+ phases["deltas"][n, m - k - 1] = delta
85
+ phases["sigmas"][n, m - k - 1] = sigma
86
+ V = M(n, sigma, delta, m) @ V
87
+ x += 1
88
+ y += 1
89
+
90
+ # these next two lines are just to remove a global phase
91
+ zeta = -np.angle(V[0, 0])
92
+ V = V @ P(0, zeta, m)
93
+ phases["zetas"][0] = zeta
94
+
95
+ for j in range(1, m):
96
+ zeta = np.angle(V[0, 0]) - np.angle(V[j, j])
97
+ V = V @ P(j, zeta, m)
98
+ phases["zetas"][j] = zeta
99
+
100
+ assert np.allclose(V, np.eye(m), rtol=rtol, atol=atol), "decomposition failed"
101
+
102
+ return phases
103
+
104
+ def _absorb_zeta(phases):
105
+ r"""Adjust rectangular decomposition to relocate residual phase-shifters of interferometer to edge-shifters, as given in FIG. 4 and "Relocating residual phase-shifts" section of (arXiv:2104.0756).
106
+
107
+ Args:
108
+ phases (dict): output of _rectangular_compact_init
109
+
110
+ Returns:
111
+ dict: A dictionary containing the following items:
112
+
113
+ * ``m``: the length of the matrix
114
+ * ``phi_ins``: parameter of the phase-shifter at the beginning of the mode
115
+ * ``sigmas``: parameter of the sMZI :math:`\frac{(\theta_1+\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
116
+ * ``deltas``: parameter of the sMZI :math:`\frac{(\theta_1-\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
117
+ * ``phi_edges``: parameters of the edge phase shifters
118
+ * ``phi_outs``: parameter of the phase-shifter at the end of the mode
119
+
120
+ """
121
+ m = phases["m"]
122
+ new_phases = phases.copy()
123
+ del new_phases["zetas"]
124
+ new_phases["phi_edges"] = defaultdict(float) # (mode, layer) : phi
125
+
126
+ if m % 2 == 0:
127
+ new_phases["phi_outs"][0] = phases["zetas"][0]
128
+ for j in range(1, m):
129
+ zeta = phases["zetas"][j]
130
+ layer = m - j
131
+ for mode in range(j, m - 1, 2):
132
+ new_phases["sigmas"][mode, layer] += zeta
133
+ for mode in range(j + 1, m - 1, 2):
134
+ new_phases["sigmas"][mode, layer - 1] -= zeta
135
+ if layer % 2 == 1:
136
+ new_phases["phi_edges"][m - 1, layer] += zeta
137
+ else:
138
+ new_phases["phi_edges"][m - 1, layer - 1] -= zeta
139
+ else:
140
+ for j in range(m):
141
+ zeta = phases["zetas"][j]
142
+ layer = m - j - 1
143
+ for mode in range(j, m - 1, 2):
144
+ new_phases["sigmas"][mode, layer] += zeta
145
+ for mode in range(j + 1, m - 1, 2):
146
+ new_phases["sigmas"][mode, layer - 1] -= zeta
147
+ if layer % 2 == 0:
148
+ new_phases["phi_edges"][m - 1, layer] += zeta
149
+ else:
150
+ new_phases["phi_edges"][m - 1, layer - 1] -= zeta
151
+ return new_phases
152
+
153
+
154
+ def decompose_bell(U, rtol=1e-12, atol=1e-12):
155
+ r"""Rectangular decomposition of a unitary with sMZIs and phase-shifters, as given in FIG. 3+4 and "The Clements Scheme" section of (arXiv:2104.0756).
156
+
157
+ Args:
158
+ U (array): unitary matrix
159
+
160
+ Returns:
161
+ dict: A dictionary containing the following items:
162
+
163
+ * ``m``: the length of the matrix
164
+ * ``phi_ins``: parameter of the phase-shifter at the beginning of the mode
165
+ * ``sigmas``: parameter of the sMZI :math:`\frac{(\theta_1+\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
166
+ * ``deltas``: parameter of the sMZI :math:`\frac{(\theta_1-\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
167
+ * ``phi_edges``: parameters of the edge phase shifters
168
+ * ``phi_outs``: parameter of the phase-shifter at the end of the mode
169
+ """
170
+
171
+ if not U.shape[0] == U.shape[1]:
172
+ raise ValueError("Matrix is not square")
173
+
174
+ if not np.allclose(U @ U.conj().T, np.eye(U.shape[0]), rtol=rtol, atol=atol):
175
+ raise ValueError("The input matrix is not unitary")
176
+
177
+ phases_temp = _rectangular_compact_init(U, rtol=rtol, atol=atol)
178
+ return _absorb_zeta(phases_temp)
179
+
180
+ def M(n, sigma, delta, m):
181
+ r"""The symmetric Mach Zehnder interferometer matrix. (Eq 1 of the paper (arXiv:2104.0756).)
182
+
183
+ Args:
184
+ n (int): the starting mode of sMZI
185
+ sigma (complex): parameter of the sMZI :math:`\frac{(\theta_1+\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
186
+ delta (complex): parameter of the sMZI :math:`\frac{(\theta_1-\theta_2)}{2}`, where :math:`\theta_{1,2}` are the values of the two internal phase-shifts of sMZI
187
+ m (int): the length of the unitary matrix to be decomposed
188
+
189
+ Returns:
190
+ array[complex,complex]: the sMZI matrix between n-th and (n+1)-th mode
191
+ """
192
+ mat = np.identity(m, dtype=np.complex128)
193
+ mat[n, n] = np.exp(1j * sigma) * np.sin(delta)
194
+ mat[n, n + 1] = np.exp(1j * sigma) * np.cos(delta)
195
+ mat[n + 1, n] = np.exp(1j * sigma) * np.cos(delta)
196
+ mat[n + 1, n + 1] = -np.exp(1j * sigma) * np.sin(delta)
197
+ return mat
198
+
199
+
200
+ def P(j, phi, m):
201
+ r"""The phase shifter matrix. (Eq 2 of the paper (arXiv:2104.0756).)
202
+
203
+ Args:
204
+ j (int): the starting mode of phase-shifter
205
+ phi (complex): parameter of the phase-shifter
206
+ m (int): the length of the unitary matrix to be decomposed
207
+
208
+ Returns:
209
+ array[complex,complex]: the phase-shifter matrix on the j-th mode
210
+ """
211
+ mat = np.identity(m, dtype=np.complex128)
212
+ mat[j, j] = np.exp(1j * phi)
213
+ return mat
@@ -0,0 +1,258 @@
1
+ '''
2
+ Script for Sax Models used
3
+ '''
4
+
5
+ import numpy as np
6
+ from pathlib import Path
7
+ import sax
8
+ import pickle
9
+
10
+ MODEL_DIR = Path(__file__).resolve().parent / "sax_models"
11
+
12
+ '''
13
+ calibration values from calibration notebook
14
+ '''
15
+ phase_offset = 3.5650791015113166
16
+ phase_step = -1.54018263
17
+
18
+ '''
19
+ the circuit requires models with following names:
20
+ '''
21
+ def tbu_model(sigma, delta):
22
+ return calibrated_mzi_model(sig=sigma, delt = delta)
23
+ # phase_shifter_model(): has correct name
24
+ def edge_phase_shifter_model(phi):
25
+ return edge_phaser_model(phi)
26
+ #def plain_wvgd_model(): has correct name
27
+
28
+ ############################################################################################################################
29
+ '''
30
+ Loading Sax dicts
31
+ '''
32
+ #############################
33
+ '''
34
+ phi must be in radians
35
+ default wavegudie is the 150um waveguide
36
+ returns sax matrix of phase with total phase= standard waveguide phase + added thermooptic
37
+ '''
38
+ def phaser_model(phi, waveguide='waveguide_150.pkl'):
39
+ filepath = MODEL_DIR / waveguide
40
+ with filepath.open("rb") as f:
41
+ ref_sdict = pickle.load(f)
42
+ ref_phase = np.angle(ref_sdict[('in0','out0')])
43
+ new = np.exp(1j*(ref_phase+phi))
44
+ phaser_model = sax.reciprocal({("in0", "out0"): new})
45
+ return phaser_model
46
+
47
+ def arm_model():
48
+ filepath = MODEL_DIR / 's_arm.pkl'
49
+ with filepath.open("rb") as f:
50
+ arm_model = pickle.load(f)
51
+ return arm_model
52
+
53
+ def coupler_model():
54
+ filepath = MODEL_DIR / "coupler_50_50_2port.pkl"
55
+ with filepath.open("rb") as f:
56
+ sax_sdict = pickle.load(f)
57
+ return sax_sdict
58
+
59
+ def edge_wvgd_model():
60
+ filepath = MODEL_DIR / "straight_L10.pkl"
61
+ with filepath.open("rb") as f:
62
+ wvgd_sdict = pickle.load(f)
63
+ return wvgd_sdict
64
+
65
+ def wvgd_model(waveguide='waveguide_150.pkl'):
66
+ filepath = MODEL_DIR / waveguide
67
+ with filepath.open("rb") as f:
68
+ ref_sdict = pickle.load(f)
69
+ return ref_sdict
70
+
71
+ def s_bend_model():
72
+ filepath = MODEL_DIR / 's_bend.pkl'
73
+ with filepath.open("rb") as f:
74
+ arm_model = pickle.load(f)
75
+ return arm_model
76
+
77
+ ############################################################################################################################
78
+ '''
79
+ Plain waveguide Circuit
80
+ '''
81
+ ##########################
82
+ def plain_wvgd_model():
83
+ total_circ, info = sax.circuit(
84
+ netlist={
85
+ "instances": {
86
+ "lft": "connector",
87
+ "middle": "straight",
88
+ "rgt": "connector",
89
+ },
90
+ "connections": {
91
+ "lft,out0": "middle,in0",
92
+ "middle,out0": "rgt,in0",
93
+ },
94
+ "ports": {
95
+ "in0": "lft,in0",
96
+ "out0": "rgt,out0",
97
+ },
98
+ },
99
+ models={
100
+ "connector": connector_model,
101
+ "straight": wvgd_model,
102
+ },
103
+ )
104
+ return total_circ()
105
+ ############################################################################################################################
106
+ '''
107
+ Phase shifters Circuits
108
+ '''
109
+ ##########################
110
+ def connector_model():
111
+ total_circ, info = sax.circuit(
112
+ netlist={
113
+ "instances": {
114
+ "lft": "sbend",
115
+ "middle": "arm",
116
+ "rgt": "sbend",
117
+ },
118
+ "connections": {
119
+ "lft,out0": "middle,in0",
120
+ "middle,out0": "rgt,in0",
121
+ },
122
+ "ports": {
123
+ "in0": "lft,in0",
124
+ "out0": "rgt,out0",
125
+ },
126
+ },
127
+ models={
128
+ "arm": arm_model,
129
+ "sbend": s_bend_model,
130
+ },
131
+ )
132
+ return total_circ()
133
+
134
+
135
+ '''
136
+ returns the sax circuit matrix for the complete arm-phase_shifter-arm component that is used in the mesh
137
+ '''
138
+ def phase_shifter_model(phi=0):
139
+ total_circ, info = sax.circuit(
140
+ netlist={
141
+ "instances": {
142
+ "lft": "connector",
143
+ "middle": "phaser",
144
+ "rgt": "connector",
145
+ },
146
+ "connections": {
147
+ "lft,out0": "middle,in0",
148
+ "middle,out0": "rgt,in0",
149
+ },
150
+ "ports": {
151
+ "in0": "lft,in0",
152
+ "out0": "rgt,out0",
153
+ },
154
+ },
155
+ models={
156
+ "connector": connector_model,
157
+ "phaser": lambda: phaser_model(phi)
158
+ },
159
+ )
160
+ return total_circ()
161
+
162
+ def edge_phaser_model(phi=0):
163
+ total_circ, info = sax.circuit(
164
+ netlist={
165
+ "instances": {
166
+ "lft": "wvgd",
167
+ 'rgt': 'phaser',
168
+ },
169
+ "connections": {
170
+ "lft,out0": "rgt,in0",
171
+ },
172
+ "ports": {
173
+ "in0": "lft,in0",
174
+ "out0": "rgt,out0",
175
+ },
176
+ },
177
+ models={
178
+ "wvgd": edge_wvgd_model,
179
+ "phaser": lambda: phaser_model(phi)#+2.9604398423257847),
180
+ },
181
+ )
182
+ return total_circ()
183
+
184
+ ########################################################################################################################
185
+ '''
186
+ MZI Model Circuits
187
+ '''
188
+ ##############################
189
+
190
+ def mzi_coupler():
191
+ total_circ, info = sax.circuit(
192
+ netlist={
193
+ "instances":{
194
+ 'tl':"sbend",
195
+ 'bl':'sbend',
196
+ 'tr':'sbend',
197
+ 'br':'sbend',
198
+ 'coupler':'coupler',
199
+ },
200
+ "connections":{
201
+ 'tl,out0':'coupler,in1',
202
+ 'bl,out0':'coupler,in0',
203
+ 'coupler,out1':'tr,in0',
204
+ 'coupler,out0':'br,in0',
205
+ },
206
+ "ports":{
207
+ 'in0':'bl,in0',
208
+ 'in1':'tl,in0',
209
+ 'out0':'br,out0',
210
+ 'out1':'tr,out0',
211
+ },
212
+ },
213
+ models={
214
+ 'sbend':s_bend_model,
215
+ 'coupler':coupler_model,
216
+ },
217
+ )
218
+ return total_circ()
219
+ '''
220
+ returns the mzi smatrix, takes input delta and sigma
221
+ '''
222
+ def mzi_model(sigma=0,delta=0):
223
+ phi1 = sigma-delta
224
+ phi2 = sigma + delta
225
+ total_circ, info = sax.circuit(
226
+ netlist={
227
+ "instances": {
228
+ "lft": "coupler",
229
+ "top": "phaser1",
230
+ "bottom": "phaser2",
231
+ 'rgt': 'coupler',
232
+ },
233
+ "connections": {
234
+ "lft,out0": "bottom,in0",
235
+ 'lft,out1': "top,in0",
236
+ "top,out0": "rgt,in1",
237
+ "bottom,out0":"rgt,in0"
238
+
239
+ },
240
+ "ports": {
241
+ "in0": "lft,in0",
242
+ "in1": "lft,in1",
243
+ "out0": "rgt,out0",
244
+ "out1": "rgt,out1",
245
+ },
246
+ },
247
+ models={
248
+ "coupler": mzi_coupler,
249
+ "phaser1": lambda: phaser_model(phi1),
250
+ "phaser2": lambda: phaser_model(phi2)
251
+ },
252
+ )
253
+ return total_circ()
254
+
255
+ def calibrated_mzi_model(sig=0, delt=0):
256
+ sigma_offset = 2.03765676
257
+ delta_offset = 3.0629742188152735
258
+ return mzi_model(sigma=sig+sigma_offset,delta=delt+delta_offset)
@@ -0,0 +1,220 @@
1
+ '''
2
+ Copyright 2026 Carl Abi Nakad
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ '''
16
+
17
+ import interferometer as itf
18
+ from . import ideal_models as ideal
19
+ import numpy as np
20
+ import sax
21
+
22
+ class Clements:
23
+ def __init__(self, dimension):
24
+ if not isinstance(dimension, int) or dimension < 2:
25
+ raise ValueError("Dimension must be an integer >= 2")
26
+ self.dimension = dimension
27
+ self.schematic_netlist = self.clements_circuit_schematic()
28
+
29
+ def ideal_emulate(self,U):
30
+ if U.shape[0]!=self.dimension:
31
+ raise ValueError(f'Matrix Dimension {U.shape[0]} not compatible with Mesh Dimension {self.dimension}')
32
+ mods = self.clements_models_ideal(U)
33
+ netlist = self.schematic_netlist
34
+ circuit, info = sax.circuit(netlist=netlist, models = mods)
35
+ S = circuit()
36
+ return S
37
+
38
+ def clements_circuit_schematic(self):
39
+ dim = self.dimension
40
+ last = dim-2
41
+ end = dim-1
42
+
43
+ instances = {}
44
+ connections = {}
45
+ ports = {}
46
+
47
+ # creating top boundary connector waveguides
48
+ for j in range(1, dim, 2):
49
+ instances[f"wvgd_top_{j}"] = "wvgd"
50
+
51
+ #create bottom boundary connector waveguides
52
+ if dim % 2 == 0:
53
+ for j in range(1, dim, 2):
54
+ instances[f"wvgd_bottom_{j}"] = "wvgd"
55
+ else:
56
+ for j in range(0, dim, 2):
57
+ instances[f"wvgd_bottom_{j}"] = "wvgd"
58
+
59
+ # creating output edge phase shifters
60
+ for i in range(dim):
61
+ instances[f"phaser_o{i}"] = f"phaser_o{i}"
62
+
63
+ # creating top and bottom mzis
64
+ for i in range(0, dim, 2):
65
+ # top mode mzis
66
+ instances[f"bs_0_{i}"] = f"bs_0_{i}"
67
+ if dim % 2 == 0: # even bottom mode mzis
68
+ instances[f"bs_{last}_{i}"] = f"bs_{last}_{i}"
69
+ else: # odd bottom mode mzis
70
+ j = i + 1
71
+ if j > end:
72
+ continue
73
+ instances[f"bs_{last}_{j}"] = f"bs_{last}_{j}"
74
+
75
+ # create middle mzis
76
+ for i in range(1, last):
77
+ if i % 2 == 0:
78
+ for j in np.arange(0, dim, 2):
79
+ instances[f"bs_{i}_{j}"] = f"bs_{i}_{j}"
80
+ else:
81
+ for j in range(1, dim, 2):
82
+ instances[f"bs_{i}_{j}"] = f"bs_{i}_{j}"
83
+
84
+ # Connecting Mesh
85
+ for i in range(dim-1):
86
+
87
+ # Top boundary Connections
88
+ if i == 0:
89
+ if dim % 2 == 0:
90
+ for j in range(0, dim - 2, 2):
91
+ connections[f"bs_{i}_{j},out0"] = f"wvgd_top_{j+1},in0"
92
+ connections[f"wvgd_top_{j+1},out0"] = f"bs_{i}_{j+2},in0"
93
+ connections[f"bs_{i}_{dim-2},out0"] =f"wvgd_top_{end},in0"
94
+ else:
95
+ for j in range(0, dim - 2, 2):
96
+ if j < last:
97
+ connections[f"bs_{i}_{j},out0"] =f"wvgd_top_{j+1},in0"
98
+ if f"bs_{i}_{j+2}" in instances:
99
+ connections[f"wvgd_top_{j+1},out0"] = f"bs_{i}_{j+2},in0"
100
+
101
+ # Connecting diagonal mzis
102
+ for j in range(dim):
103
+ key = f"bs_{i}_{j}"
104
+ if key not in instances:
105
+ continue
106
+ #up-right connection
107
+ if i > 0:
108
+ upper_right = f"bs_{i-1}_{j+1}"
109
+ if upper_right in instances:
110
+ connections[f"bs_{i}_{j},out0"] = f"bs_{i-1}_{j+1},in1"
111
+ # downward-right connection
112
+ target = f"bs_{i+1}_{j+1}"
113
+ if target in instances:
114
+ connections[f"bs_{i}_{j},out1"] = f"bs_{i+1}_{j+1},in0"
115
+
116
+ # bottom boundary connections
117
+ if i == last:
118
+ if dim % 2 == 0:
119
+ # intermediate bottom boundary connections
120
+ for j in range(1, last, 2):
121
+ connections[f"bs_{last}_{j-1},out1"] = f"wvgd_bottom_{j},in0"
122
+ connections[f"wvgd_bottom_{j},out0"] = f"bs_{last}_{j+1},in1"
123
+
124
+ # final bottom boundary segment
125
+ connections[f"bs_{last}_{last},out1"] = f"wvgd_bottom_{end},in0"
126
+ # bottom output
127
+ connections[f"wvgd_bottom_{end},out0"] = f"phaser_o{end},in0"
128
+ ports[f"out{end}"] = f"phaser_o{end},out0"
129
+ else:
130
+ # first bottom phaser
131
+ connections[f"wvgd_bottom_0,out0"] = f"bs_{last}_1,in1"
132
+
133
+ # last bottom phaser
134
+ connections[f"bs_{last}_{last},out1"] = f"wvgd_bottom_{end},in0"
135
+
136
+ # intermediate bottom phasers
137
+ for j in range(2, last, 2):
138
+ connections[f"bs_{i}_{j-1},out1"] = f"wvgd_bottom_{j},in0"
139
+ connections[f"wvgd_bottom_{j},out0"] = f"bs_{i}_{j+1},in1"
140
+
141
+ # Connecting output phase shifters
142
+ if dim % 2 == 0:
143
+ # top boundary output
144
+ connections[f"wvgd_top_{end},out0"] = "phaser_o0,in0"
145
+ ports["out0"] = "phaser_o0,out0"
146
+
147
+ # middle BS outputs
148
+ for i in range(1, last, 2):
149
+ connections[f"bs_{i}_{end},out0"] = f"phaser_o{i},in0"
150
+ connections[f"bs_{i}_{end},out1"] = f"phaser_o{i+1},in0"
151
+
152
+ ports[f"out{i}"] = f"phaser_o{i},out0"
153
+ ports[f"out{i+1}"] = f"phaser_o{i+1},out0"
154
+
155
+ # Connecting output phase shifters - odd
156
+ else:
157
+ output_col = end
158
+ for i in range(0, dim - 1, 2):
159
+ connections[f"bs_{i}_{output_col},out0"] = f"phaser_o{i},in0"
160
+ connections[f"bs_{i}_{output_col},out1"] = f"phaser_o{i+1},in0"
161
+ ports[f"out{i}"] = f"phaser_o{i},out0"
162
+ ports[f"out{i+1}"] = f"phaser_o{i+1},out0"
163
+ # bottom mode
164
+ connections[f"wvgd_bottom_{end},out0"] = f"phaser_o{end},in0"
165
+ ports[f"out{end}"] = f"phaser_o{end},out0"
166
+
167
+ # input ports
168
+ for i in range(0, dim - 1, 2):
169
+ ports[f"in{i}"] = f"bs_{i}_0,in0"
170
+ ports[f"in{i+1}"] = f"bs_{i}_0,in1"
171
+
172
+ if dim % 2 != 0:
173
+ ports[f"in{end}"] = "wvgd_bottom_0,in0"
174
+ netlist = {}
175
+ netlist['instances'] = instances
176
+ netlist['connections'] = connections
177
+ netlist['ports'] = ports
178
+ return netlist
179
+
180
+ def clements_models_ideal(self,U):
181
+ I = itf.square_decomposition(U)
182
+ dim = U.shape[0]
183
+ theta, phi = self.map_clements_bs(I)
184
+ phases = I.output_phases
185
+ models = {}
186
+ models['wvgd'] = ideal.ideal_waveguide
187
+ for i in range(dim):
188
+ phase = phases[i]
189
+ models[f"phaser_o{i}"] = lambda phase=phase: ideal.ideal_phaseshifter(phase)
190
+ for j in range(dim):
191
+ if (i,j) in phi.keys():
192
+ phi_=phi[(i,j)]
193
+ theta_ = theta[(i,j)]
194
+ models[f"bs_{i}_{j}"] = lambda phi_=phi_, theta_ = theta_: ideal.ideal_clements_tbu_model(theta_, phi_)
195
+ return models
196
+
197
+ def map_clements_bs(self,I):
198
+ theta = {}
199
+ phi = {}
200
+ dim = self.dimension
201
+ mode_tracker = np.zeros(dim, dtype=int)
202
+
203
+ for BS in I.BS_list:
204
+ m1 = BS.mode1 - 1
205
+ m2 = BS.mode2 - 1
206
+
207
+ # Physical column in Clements mesh
208
+ j = max(mode_tracker[m1], mode_tracker[m2])
209
+
210
+ # Physical row = upper mode
211
+ i = m1
212
+
213
+ theta[(i, j)] = BS.theta
214
+ phi[(i, j)] = BS.phi
215
+
216
+ # Both modes have now advanced one column
217
+ mode_tracker[m1] = j + 1
218
+ mode_tracker[m2] = j + 1
219
+
220
+ return theta, phi