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.
- multiport_interferometer/__init__.py +7 -0
- multiport_interferometer/bell_class.py +324 -0
- multiport_interferometer/bell_components.py +219 -0
- multiport_interferometer/bell_decomposer.py +213 -0
- multiport_interferometer/bell_models.py +258 -0
- multiport_interferometer/clements_class.py +220 -0
- multiport_interferometer/functions.py +233 -0
- multiport_interferometer/ideal_models.py +77 -0
- multiport_interferometer/sax_models/coupler_50_50_2port.pkl +0 -0
- multiport_interferometer/sax_models/s_arm.pkl +0 -0
- multiport_interferometer/sax_models/s_bend.pkl +0 -0
- multiport_interferometer/sax_models/straight_L10.pkl +0 -0
- multiport_interferometer/sax_models/waveguide_150.pkl +0 -0
- multiport_interferometer-0.0.1.dist-info/METADATA +130 -0
- multiport_interferometer-0.0.1.dist-info/RECORD +18 -0
- multiport_interferometer-0.0.1.dist-info/WHEEL +5 -0
- multiport_interferometer-0.0.1.dist-info/licenses/LICENSE +202 -0
- multiport_interferometer-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|