differometor 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.
- differometor/__init__.py +2 -0
- differometor/build.py +1229 -0
- differometor/components.py +512 -0
- differometor/setups.py +582 -0
- differometor/simulate.py +392 -0
- differometor/utils.py +45 -0
- differometor-0.0.1.dist-info/METADATA +231 -0
- differometor-0.0.1.dist-info/RECORD +11 -0
- differometor-0.0.1.dist-info/WHEEL +5 -0
- differometor-0.0.1.dist-info/licenses/LICENSE +21 -0
- differometor-0.0.1.dist-info/top_level.txt +1 -0
differometor/build.py
ADDED
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import jax.numpy as jnp
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from differometor.setups import Setup
|
|
5
|
+
from differometor.components import nothing_matrix, directional_beamsplitter_matrix, mirror_matrix, beamsplitter_matrix, laser_np, space, F, DEFAULT_REFRACTIVE_INDEX, DEFAULT_PROPERTIES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
LINKING_FUNCTIONS = []
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_instructions(
|
|
12
|
+
instructions,
|
|
13
|
+
node,
|
|
14
|
+
function_input_indices,
|
|
15
|
+
output_column_indices,
|
|
16
|
+
system_matrix_row_indices,
|
|
17
|
+
system_matrix_column_indices,
|
|
18
|
+
function_indices,
|
|
19
|
+
output_row_indices=None,
|
|
20
|
+
carrier_indices=None,
|
|
21
|
+
carrier_row_indices=None,
|
|
22
|
+
carrier_column_indices=None,
|
|
23
|
+
system_size_for_sidebands=None
|
|
24
|
+
):
|
|
25
|
+
function_input_indices_shape = 7
|
|
26
|
+
if function_input_indices.shape[1] < function_input_indices_shape:
|
|
27
|
+
# add placeholder values
|
|
28
|
+
function_input_indices = np.concatenate([function_input_indices, np.zeros((function_input_indices.shape[0], function_input_indices_shape - function_input_indices.shape[1]))], axis=1)
|
|
29
|
+
|
|
30
|
+
assert function_input_indices.shape[1] == function_input_indices_shape, f"Function input indices have to be standardized to length {function_input_indices_shape}."
|
|
31
|
+
assert len(output_column_indices) == len(system_matrix_row_indices), "Output column indices have to match the length of system matrix row indices."
|
|
32
|
+
|
|
33
|
+
# often these are partially the same as system matrix indices, so make them
|
|
34
|
+
# independent copies to avoid side effects from updates in the system matrix indices
|
|
35
|
+
if carrier_indices is not None:
|
|
36
|
+
carrier_indices = carrier_indices.copy()
|
|
37
|
+
carrier_row_indices = carrier_row_indices.copy()
|
|
38
|
+
carrier_column_indices = carrier_column_indices.copy()
|
|
39
|
+
|
|
40
|
+
if system_size_for_sidebands is not None:
|
|
41
|
+
# We have two sidebands in signal and noise matrices. It looks like this:
|
|
42
|
+
#
|
|
43
|
+
# sideband_1, 0, signal
|
|
44
|
+
# 0, sideband_2, signal
|
|
45
|
+
# signal, signal, signal
|
|
46
|
+
#
|
|
47
|
+
# All system matrix indices that are below system_size in either row or column must get
|
|
48
|
+
# duplicated. While doing that we also have to update the corresponding output indices and
|
|
49
|
+
# the carrier indices.
|
|
50
|
+
|
|
51
|
+
# There are entries which need to be updated, but the column stays the same (e.g. signal connectors)
|
|
52
|
+
only_row_mask = (system_matrix_row_indices < system_size_for_sidebands) & (system_matrix_column_indices >= system_size_for_sidebands)
|
|
53
|
+
# There are entries which need to be updated, but the row stays the same (e.g. force entries for optomechanics)
|
|
54
|
+
only_column_mask = (system_matrix_column_indices < system_size_for_sidebands) & (system_matrix_row_indices >= system_size_for_sidebands)
|
|
55
|
+
# There are entries where both row and column need to be updated (e.g. mirror entries)
|
|
56
|
+
row_and_column_mask = (system_matrix_row_indices < system_size_for_sidebands) & (system_matrix_column_indices < system_size_for_sidebands)
|
|
57
|
+
# pure signal entries still need to be shifted
|
|
58
|
+
pure_signal_mask = (system_matrix_row_indices >= system_size_for_sidebands) & (system_matrix_column_indices >= system_size_for_sidebands)
|
|
59
|
+
|
|
60
|
+
# as carrier part of signal matrix gets duplicated, signal entries need to be relocated
|
|
61
|
+
system_matrix_column_indices[pure_signal_mask] += system_size_for_sidebands
|
|
62
|
+
system_matrix_row_indices[pure_signal_mask] += system_size_for_sidebands
|
|
63
|
+
system_matrix_column_indices[only_row_mask] += system_size_for_sidebands
|
|
64
|
+
system_matrix_row_indices[only_column_mask] += system_size_for_sidebands
|
|
65
|
+
|
|
66
|
+
new_system_matrix_row_indices = system_matrix_row_indices.copy()
|
|
67
|
+
new_system_matrix_column_indices = system_matrix_column_indices.copy()
|
|
68
|
+
new_output_column_indices = output_column_indices.copy()
|
|
69
|
+
new_output_row_indices = output_row_indices.copy() if output_row_indices is not None else None
|
|
70
|
+
|
|
71
|
+
if only_row_mask.any():
|
|
72
|
+
# add new row entries for the second sideband (e.g. signal connectors)
|
|
73
|
+
new_system_matrix_row_indices = np.concatenate([new_system_matrix_row_indices, system_matrix_row_indices[only_row_mask] + system_size_for_sidebands])
|
|
74
|
+
# we still need to duplicate the column indices, but it stays the same column
|
|
75
|
+
new_system_matrix_column_indices = np.concatenate([new_system_matrix_column_indices, system_matrix_column_indices[only_row_mask]])
|
|
76
|
+
new_output_column_indices = np.concatenate([new_output_column_indices, output_column_indices[only_row_mask]])
|
|
77
|
+
if output_row_indices is not None:
|
|
78
|
+
new_output_row_indices = np.concatenate([new_output_row_indices, output_row_indices[only_row_mask]])
|
|
79
|
+
|
|
80
|
+
if only_column_mask.any():
|
|
81
|
+
# add new column entries for the second sideband (e.g. force entries for optomechanics)
|
|
82
|
+
new_system_matrix_column_indices = np.concatenate([new_system_matrix_column_indices, system_matrix_column_indices[only_column_mask] + system_size_for_sidebands])
|
|
83
|
+
new_system_matrix_row_indices = np.concatenate([new_system_matrix_row_indices, system_matrix_row_indices[only_column_mask]])
|
|
84
|
+
new_output_column_indices = np.concatenate([new_output_column_indices, output_column_indices[only_column_mask]])
|
|
85
|
+
if output_row_indices is not None:
|
|
86
|
+
new_output_row_indices = np.concatenate([new_output_row_indices, output_row_indices[only_column_mask]])
|
|
87
|
+
|
|
88
|
+
if row_and_column_mask.any():
|
|
89
|
+
# add new column and row entries for the second sideband (e.g. mirror entries)
|
|
90
|
+
new_system_matrix_column_indices = np.concatenate([new_system_matrix_column_indices, system_matrix_column_indices[row_and_column_mask] + system_size_for_sidebands])
|
|
91
|
+
new_system_matrix_row_indices = np.concatenate([new_system_matrix_row_indices, system_matrix_row_indices[row_and_column_mask] + system_size_for_sidebands])
|
|
92
|
+
new_output_column_indices = np.concatenate([new_output_column_indices, output_column_indices[row_and_column_mask]])
|
|
93
|
+
if output_row_indices is not None:
|
|
94
|
+
new_output_row_indices = np.concatenate([new_output_row_indices, output_row_indices[row_and_column_mask]])
|
|
95
|
+
|
|
96
|
+
output_column_indices = new_output_column_indices
|
|
97
|
+
output_row_indices = new_output_row_indices
|
|
98
|
+
system_matrix_row_indices = new_system_matrix_row_indices
|
|
99
|
+
system_matrix_column_indices = new_system_matrix_column_indices
|
|
100
|
+
|
|
101
|
+
# do the same for carrier_indices
|
|
102
|
+
if carrier_indices is not None:
|
|
103
|
+
only_row_mask = (carrier_row_indices < system_size_for_sidebands) & (carrier_column_indices >= system_size_for_sidebands)
|
|
104
|
+
only_column_mask = (carrier_column_indices < system_size_for_sidebands) & (carrier_row_indices >= system_size_for_sidebands)
|
|
105
|
+
row_and_column_mask = (carrier_row_indices < system_size_for_sidebands) & (carrier_column_indices < system_size_for_sidebands)
|
|
106
|
+
pure_signal_mask = (carrier_row_indices >= system_size_for_sidebands) & (carrier_column_indices >= system_size_for_sidebands)
|
|
107
|
+
|
|
108
|
+
carrier_column_indices[pure_signal_mask] += system_size_for_sidebands
|
|
109
|
+
carrier_row_indices[pure_signal_mask] += system_size_for_sidebands
|
|
110
|
+
carrier_column_indices[only_row_mask] += system_size_for_sidebands
|
|
111
|
+
carrier_row_indices[only_column_mask] += system_size_for_sidebands
|
|
112
|
+
|
|
113
|
+
new_carrier_row_indices = carrier_row_indices.copy()
|
|
114
|
+
new_carrier_column_indices = carrier_column_indices.copy()
|
|
115
|
+
new_carrier_indices = carrier_indices.copy()
|
|
116
|
+
|
|
117
|
+
if only_row_mask.any():
|
|
118
|
+
new_carrier_row_indices = np.concatenate([new_carrier_row_indices, carrier_row_indices[only_row_mask] + system_size_for_sidebands])
|
|
119
|
+
new_carrier_column_indices = np.concatenate([new_carrier_column_indices, carrier_column_indices[only_row_mask]])
|
|
120
|
+
new_carrier_indices = np.concatenate([new_carrier_indices, carrier_indices[only_row_mask]])
|
|
121
|
+
|
|
122
|
+
if only_column_mask.any():
|
|
123
|
+
new_carrier_column_indices = np.concatenate([new_carrier_column_indices, carrier_column_indices[only_column_mask] + system_size_for_sidebands])
|
|
124
|
+
new_carrier_row_indices = np.concatenate([new_carrier_row_indices, carrier_row_indices[only_column_mask]])
|
|
125
|
+
new_carrier_indices = np.concatenate([new_carrier_indices, carrier_indices[only_column_mask]])
|
|
126
|
+
|
|
127
|
+
if row_and_column_mask.any():
|
|
128
|
+
new_carrier_column_indices = np.concatenate([new_carrier_column_indices, carrier_column_indices[row_and_column_mask] + system_size_for_sidebands])
|
|
129
|
+
new_carrier_row_indices = np.concatenate([new_carrier_row_indices, carrier_row_indices[row_and_column_mask] + system_size_for_sidebands])
|
|
130
|
+
new_carrier_indices = np.concatenate([new_carrier_indices, carrier_indices[row_and_column_mask]])
|
|
131
|
+
|
|
132
|
+
carrier_indices = new_carrier_indices
|
|
133
|
+
carrier_row_indices = new_carrier_row_indices
|
|
134
|
+
carrier_column_indices = new_carrier_column_indices
|
|
135
|
+
|
|
136
|
+
assert function_input_indices.shape[1] == function_input_indices_shape, f"Function input indices have to be standardized to length {function_input_indices_shape}."
|
|
137
|
+
assert len(output_column_indices) == len(system_matrix_row_indices), "Output column indices have to match the length of system matrix row indices."
|
|
138
|
+
|
|
139
|
+
instructions[node] = {
|
|
140
|
+
# The indices of the parameters in the parameter vector that are necessary to calculate
|
|
141
|
+
# new values. The length of this always has to be standardized (3 in this case). In case
|
|
142
|
+
# of multiple functions, this has to be an (N, 3) matrix.
|
|
143
|
+
"function_input_indices": function_input_indices,
|
|
144
|
+
# output_column_indices determines what outputs of the FUNCTION_INDICES function are
|
|
145
|
+
# used to update the system matrix. Indices here relate to the output of the function
|
|
146
|
+
# determined by the indices in "function_indices". The length of this array has to match
|
|
147
|
+
# the length of system_matrix_row_indices. In case of multiple functions, this is just an
|
|
148
|
+
# array of N output indices which index the (N, 3) output matrix.
|
|
149
|
+
"output_column_indices": output_column_indices,
|
|
150
|
+
# system_matrix_indices determine where to place the outputs in the system matrix. Indices
|
|
151
|
+
# here relate to the system matrix or signal matrix.
|
|
152
|
+
"system_matrix_row_indices": system_matrix_row_indices,
|
|
153
|
+
"system_matrix_column_indices": system_matrix_column_indices,
|
|
154
|
+
# The index of the function used to process function_inputs and produce outputs that get
|
|
155
|
+
# placed in the system matrix. Indices here relate to the FUNCTION_LIST in components.py
|
|
156
|
+
# In case of multiple functions, this is just an array of function indices.
|
|
157
|
+
"function_indices": function_indices
|
|
158
|
+
}
|
|
159
|
+
if output_row_indices is not None:
|
|
160
|
+
instructions[node]["output_row_indices"] = output_row_indices
|
|
161
|
+
# The following indices are necessary because we often need entries of the carrier solution to
|
|
162
|
+
# calculate entries of the signal system. As the entries to be calculated are potentially
|
|
163
|
+
# distributed over the signal matrix, we need to use indexing to select and distribute the carrier
|
|
164
|
+
# solution entries accordingly.
|
|
165
|
+
if carrier_indices is not None:
|
|
166
|
+
# indicates which entries of the solution of the carrier system are needed
|
|
167
|
+
instructions[node]["carrier_indices"] = carrier_indices
|
|
168
|
+
# indicates where the entries of the solution of the carrier system have to be multiplied into
|
|
169
|
+
instructions[node]["carrier_row_indices"] = carrier_row_indices
|
|
170
|
+
instructions[node]["carrier_column_indices"] = carrier_column_indices
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parameter_linking(
|
|
174
|
+
new_parameters,
|
|
175
|
+
indices_to_link,
|
|
176
|
+
linked_names,
|
|
177
|
+
linking_function_indices,
|
|
178
|
+
parameters
|
|
179
|
+
):
|
|
180
|
+
for new_parameter in new_parameters:
|
|
181
|
+
if type(new_parameter) is not tuple:
|
|
182
|
+
parameters.append(new_parameter)
|
|
183
|
+
else:
|
|
184
|
+
assert type(new_parameter[0]) is str, "Linked parameter name has to be a string."
|
|
185
|
+
assert callable(new_parameter[1]), "Linked parameter value has to be callable."
|
|
186
|
+
# dummy value for now
|
|
187
|
+
parameters.append(0)
|
|
188
|
+
# add the function to the list of functions to be called
|
|
189
|
+
LINKING_FUNCTIONS.append(new_parameter[1])
|
|
190
|
+
linking_function_indices.append(len(LINKING_FUNCTIONS) - 1)
|
|
191
|
+
indices_to_link.append(len(parameters) - 1)
|
|
192
|
+
linked_names.append(new_parameter[0])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def build(setup: Setup):
|
|
196
|
+
"""
|
|
197
|
+
Takes a setup graph and returns the system matrix as well as instructions for the
|
|
198
|
+
parameter vector.
|
|
199
|
+
"""
|
|
200
|
+
system_size = 0
|
|
201
|
+
signal_size = 0
|
|
202
|
+
matrix_positions = {}
|
|
203
|
+
carrier_instructions = {}
|
|
204
|
+
signal_instructions = {}
|
|
205
|
+
# carrier frequency is always the first parameter, refractive index always the second
|
|
206
|
+
# 0 is the default value for alpha for mirrors and qhd phases and is always the third parameter
|
|
207
|
+
# This needs to be this way because these parameters are needed throughout the build
|
|
208
|
+
# process
|
|
209
|
+
parameters = [F, DEFAULT_REFRACTIVE_INDEX, 0]
|
|
210
|
+
parameter_names = ["", "", ""]
|
|
211
|
+
signal_components = []
|
|
212
|
+
space_to_signals = defaultdict(list)
|
|
213
|
+
laser_to_signals = defaultdict(list)
|
|
214
|
+
parameter_positions = {}
|
|
215
|
+
noise_instructions = {}
|
|
216
|
+
all_ports = set([])
|
|
217
|
+
used_ports = set([])
|
|
218
|
+
quantum_detector_number = 0
|
|
219
|
+
free_masses = []
|
|
220
|
+
signal_frequency_position = 0
|
|
221
|
+
surfaces_to_refractive_index_parameter_position = defaultdict(dict)
|
|
222
|
+
mirror_indices = []
|
|
223
|
+
beamsplitter_indices = []
|
|
224
|
+
isolator_indices = []
|
|
225
|
+
indices_to_link = []
|
|
226
|
+
linked_names = []
|
|
227
|
+
linking_function_indices = []
|
|
228
|
+
|
|
229
|
+
def load_defaults(data, component=None):
|
|
230
|
+
"""
|
|
231
|
+
Loads default properties for missing properties.
|
|
232
|
+
"""
|
|
233
|
+
if component is None:
|
|
234
|
+
component = data["component"]
|
|
235
|
+
if "properties" not in data:
|
|
236
|
+
data["properties"] = DEFAULT_PROPERTIES[component].copy()
|
|
237
|
+
else:
|
|
238
|
+
default_dict = DEFAULT_PROPERTIES[component].copy()
|
|
239
|
+
default_dict.update(data["properties"])
|
|
240
|
+
data["properties"] = default_dict
|
|
241
|
+
|
|
242
|
+
# First edge loop to calculate the carrier matrix size from spaces
|
|
243
|
+
for (source, target, data) in setup.edges(data=True):
|
|
244
|
+
load_defaults(data, "space")
|
|
245
|
+
|
|
246
|
+
# define standard values for source and target port for edges
|
|
247
|
+
if "source_port" not in data:
|
|
248
|
+
data["source_port"] = "right"
|
|
249
|
+
if "target_port" not in data:
|
|
250
|
+
data["target_port"] = "left"
|
|
251
|
+
|
|
252
|
+
source_node = setup.nodes[source]
|
|
253
|
+
target_node = setup.nodes[target]
|
|
254
|
+
|
|
255
|
+
# add parameters of spaces here already, because they will be needed by surfaces as well
|
|
256
|
+
parameter_positions[f"{source}_{target}"] = len(parameters)
|
|
257
|
+
parameter_linking([data["properties"]["length"], data["properties"]["refractive_index"]],
|
|
258
|
+
indices_to_link,
|
|
259
|
+
linked_names,
|
|
260
|
+
linking_function_indices,
|
|
261
|
+
parameters)
|
|
262
|
+
parameter_names += [f"{source}_{target}_length", f"{source}_{target}_refractive_index"]
|
|
263
|
+
|
|
264
|
+
# here we save the refractive indices of the spaces at each surface.
|
|
265
|
+
if source_node["component"] == "mirror":
|
|
266
|
+
surfaces_to_refractive_index_parameter_position[source][data["source_port"]] = len(parameters) - 1
|
|
267
|
+
if target_node["component"] == "mirror":
|
|
268
|
+
surfaces_to_refractive_index_parameter_position[target][data["target_port"]] = len(parameters) - 1
|
|
269
|
+
|
|
270
|
+
# A beamsplitter must have the same refractive
|
|
271
|
+
# index on left & top and right & bottom respectively. See finesse > components > beamsplitter.py >
|
|
272
|
+
# Beamsplitter > refractive_index_1 and refractive_index_2
|
|
273
|
+
port_mapping = {
|
|
274
|
+
"right": "right",
|
|
275
|
+
"left": "left",
|
|
276
|
+
"top": "left",
|
|
277
|
+
"bottom": "right"
|
|
278
|
+
}
|
|
279
|
+
if source_node["component"] == "beamsplitter":
|
|
280
|
+
port = port_mapping[data["source_port"]]
|
|
281
|
+
if port in surfaces_to_refractive_index_parameter_position[source] and parameters[surfaces_to_refractive_index_parameter_position[source][port]] != parameters[-1]:
|
|
282
|
+
raise ValueError("Beamsplitter has to have the same refractive index on left & top and right & bottom respectively.")
|
|
283
|
+
surfaces_to_refractive_index_parameter_position[source][port] = len(parameters) - 1
|
|
284
|
+
if target_node["component"] == "beamsplitter":
|
|
285
|
+
port = port_mapping[data["target_port"]]
|
|
286
|
+
if port in surfaces_to_refractive_index_parameter_position[target] and parameters[surfaces_to_refractive_index_parameter_position[target][port]] != parameters[-1]:
|
|
287
|
+
raise ValueError("Beamsplitter has to have the same refractive index on left & top and right & bottom respectively.")
|
|
288
|
+
surfaces_to_refractive_index_parameter_position[target][port] = len(parameters) - 1
|
|
289
|
+
|
|
290
|
+
# The following two for loops only affect lasers and detectors that were connected
|
|
291
|
+
# via space (edge) and without an explicit target.
|
|
292
|
+
if source_node["component"] in ["laser", "squeezer"]:
|
|
293
|
+
matrix_positions[f"{source}_{target}_source"] = system_size
|
|
294
|
+
# space adds two rows to system matrix
|
|
295
|
+
system_size += 2
|
|
296
|
+
# collect ports to later filter unused ports
|
|
297
|
+
all_ports.add(f"{source}_{target}.left")
|
|
298
|
+
# Laser needs a target, so add that if laser was connected via space (edge)
|
|
299
|
+
source_node["target"] = f"{source}_{target}_source"
|
|
300
|
+
elif source_node["component"] in ["detector", "qnoised"]:
|
|
301
|
+
raise ValueError("Detectors can only be targets of edges.")
|
|
302
|
+
|
|
303
|
+
if target_node["component"] in ["detector", "qnoised"]:
|
|
304
|
+
matrix_positions[f"{source}_{target}_target"] = system_size
|
|
305
|
+
# space adds two rows to system matrix
|
|
306
|
+
system_size += 2
|
|
307
|
+
# collect ports to later filter unused ports
|
|
308
|
+
all_ports.add(f"{source}_{target}.right")
|
|
309
|
+
# Detector needs a target, so add that if detector was connected via space (edge)
|
|
310
|
+
target_node["target"] = f"{source}_{target}_target"
|
|
311
|
+
# Update default direction of in to out
|
|
312
|
+
target_node["direction"] = "out"
|
|
313
|
+
elif target_node["component"] in ["laser", "squeezer"]:
|
|
314
|
+
raise ValueError("Lasers can only be sources of edges.")
|
|
315
|
+
|
|
316
|
+
# First node loop to calculate the carrier matrix and signal sizes from
|
|
317
|
+
# components with submatrix and signal components
|
|
318
|
+
for node, data in setup.nodes(data=True):
|
|
319
|
+
load_defaults(data)
|
|
320
|
+
|
|
321
|
+
# all components that have a submatrix
|
|
322
|
+
if data["component"] in MATRIX_SIZES:
|
|
323
|
+
# Calculate matrix dimensions and identify the position of the matrix components
|
|
324
|
+
# (e.g. mirror, beamsplitter) along the diagonal of the system matrix
|
|
325
|
+
matrix_size = MATRIX_SIZES[data["component"]]
|
|
326
|
+
matrix_positions[node] = system_size
|
|
327
|
+
if data["component"] == "mirror":
|
|
328
|
+
surface_indices = mirror_indices
|
|
329
|
+
elif data["component"] == "beamsplitter":
|
|
330
|
+
surface_indices = beamsplitter_indices
|
|
331
|
+
elif data["component"] == "directional_beamsplitter":
|
|
332
|
+
surface_indices = isolator_indices
|
|
333
|
+
surface_indices.extend(range(system_size, system_size + matrix_size))
|
|
334
|
+
system_size += matrix_size
|
|
335
|
+
# collect ports to later filter unused ports
|
|
336
|
+
if data["component"] in ["mirror"]:
|
|
337
|
+
all_ports.add(node + '.left')
|
|
338
|
+
all_ports.add(node + '.right')
|
|
339
|
+
if data["component"] in ["beamsplitter"]:
|
|
340
|
+
all_ports.add(node + '.top')
|
|
341
|
+
all_ports.add(node + '.bottom')
|
|
342
|
+
all_ports.add(node + '.left')
|
|
343
|
+
all_ports.add(node + '.right')
|
|
344
|
+
|
|
345
|
+
if data["component"] in ["laser", "squeezer", "detector", "qnoised"]:
|
|
346
|
+
# define standard values for port and direction for lasers and detectors
|
|
347
|
+
if "port" not in data:
|
|
348
|
+
data["port"] = "left"
|
|
349
|
+
if "direction" not in data:
|
|
350
|
+
data["direction"] = "in"
|
|
351
|
+
|
|
352
|
+
if data["component"] == "squeezer":
|
|
353
|
+
# To be able to change the squeezing angle and db, we need to mark it as a signal component
|
|
354
|
+
# which is used later in the pair_to_arrays function
|
|
355
|
+
signal_components.append(node)
|
|
356
|
+
|
|
357
|
+
if data["component"] in ["qnoised", "qhd"]:
|
|
358
|
+
if not "auxiliary" in data or ("auxiliary" in data and not data["auxiliary"]):
|
|
359
|
+
quantum_detector_number += 1
|
|
360
|
+
|
|
361
|
+
if data["component"] == "signal":
|
|
362
|
+
signal_components.append(node)
|
|
363
|
+
matrix_positions[node] = signal_size
|
|
364
|
+
# we cannot directly apply the modulations because we don't know where the target
|
|
365
|
+
# components will end up in the system matrix. Thus we note the corresponding components
|
|
366
|
+
# to apply the modulations later.
|
|
367
|
+
try:
|
|
368
|
+
target_component = setup.nodes[data["target"]]["component"]
|
|
369
|
+
if target_component == 'laser':
|
|
370
|
+
laser_to_signals[data["target"]].append(node)
|
|
371
|
+
except KeyError:
|
|
372
|
+
# target is space, because spaces dont have a component key
|
|
373
|
+
space_to_signals[data["target"]].append(node)
|
|
374
|
+
signal_size += 1
|
|
375
|
+
|
|
376
|
+
if data["component"] == "free_mass":
|
|
377
|
+
signal_components.append(node)
|
|
378
|
+
matrix_positions[node] = signal_size
|
|
379
|
+
signal_size += 2
|
|
380
|
+
free_masses.append(node)
|
|
381
|
+
|
|
382
|
+
if data["component"] == "frequency":
|
|
383
|
+
signal_components.append(node)
|
|
384
|
+
# add signal frequency as parameter and note its position
|
|
385
|
+
signal_frequency_position = len(parameters)
|
|
386
|
+
parameters.append(data["properties"]["frequency"])
|
|
387
|
+
parameter_names.append(f"{node}_frequency")
|
|
388
|
+
|
|
389
|
+
detectors = {}
|
|
390
|
+
# + 1 for the right hand side input vector
|
|
391
|
+
carrier_matrix = np.zeros((system_size, system_size + 1), dtype=complex)
|
|
392
|
+
eye = np.eye(system_size, dtype=complex)
|
|
393
|
+
carrier_matrix[:, :system_size] = eye
|
|
394
|
+
|
|
395
|
+
# this gets introduced to avoid conditionals in the JAX simulation part. If there is no signal we
|
|
396
|
+
# would have to introduce conditionals, but if we just add this dummy signal, we can avoid them.
|
|
397
|
+
if signal_size == 0:
|
|
398
|
+
signal_size = 1
|
|
399
|
+
# signal matrix is system_size (carrier matrix size) + signal_size (number of signals)
|
|
400
|
+
# + 1 for rhs, * 2 for two sidebands
|
|
401
|
+
# TODO: Maybe we should make the handling of the second sideband optional, as it is not always needed.
|
|
402
|
+
signal_matrix = np.zeros((system_size * 2 + signal_size, system_size * 2 + signal_size + 1), dtype=complex)
|
|
403
|
+
eye = np.eye(system_size * 2 + signal_size, dtype=complex)
|
|
404
|
+
signal_matrix[:, :system_size * 2 + signal_size] = eye
|
|
405
|
+
|
|
406
|
+
noise_matrix = np.zeros(signal_matrix[:, :-1].shape)
|
|
407
|
+
noise_detectors = {}
|
|
408
|
+
# refer to https://doi.org/10.5281/zenodo.821380 appendix D about quantum noise
|
|
409
|
+
noise_selection_vectors = np.zeros((quantum_detector_number, 1, system_size * 2 + signal_size), dtype=complex)
|
|
410
|
+
noise_detector_count = 0
|
|
411
|
+
|
|
412
|
+
detector_indices = []
|
|
413
|
+
# collect qhd phase parameter indices relative to parameter array
|
|
414
|
+
qhd_parameter_indices = []
|
|
415
|
+
# collect placing indices relative to noise_selection_vectors
|
|
416
|
+
qhd_placing_indices = []
|
|
417
|
+
|
|
418
|
+
# Identify indices of lasers and detectors in input and output vectors and fill the system matrix
|
|
419
|
+
# Second node loop necessary because detector and laser indices depend on the target component whose
|
|
420
|
+
# position in the system matrix is only known after the first node loop.
|
|
421
|
+
for node, data in setup.nodes(data=True):
|
|
422
|
+
### SIGNALS ###
|
|
423
|
+
# Even so signals don't depend on the position of other components, we still need to know the final
|
|
424
|
+
# system size to set them.
|
|
425
|
+
if data["component"] == "signal":
|
|
426
|
+
signal_index = matrix_positions[node]
|
|
427
|
+
# This adds the signals parameters and sets indices for the rhs entry of the signal field
|
|
428
|
+
parameter_linking([data["properties"]["amplitude"], data["properties"]["phase"]],
|
|
429
|
+
indices_to_link,
|
|
430
|
+
linked_names,
|
|
431
|
+
linking_function_indices,
|
|
432
|
+
parameters)
|
|
433
|
+
parameter_names += [f"{node}_amplitude", f"{node}_phase"]
|
|
434
|
+
set_instructions(signal_instructions,
|
|
435
|
+
node,
|
|
436
|
+
# amplitude, phase
|
|
437
|
+
function_input_indices=np.array([[len(parameters)-2, len(parameters)-1]]),
|
|
438
|
+
output_column_indices=np.array([0]),
|
|
439
|
+
# place it in the row of the signal (doesn't need to get duplicated for sidebands)
|
|
440
|
+
# * 2 because we need to take into account the second sideband and we don't do
|
|
441
|
+
# any index updating in set_instructions in this case
|
|
442
|
+
system_matrix_row_indices=np.array([system_size * 2 + signal_index]),
|
|
443
|
+
# signal right-hand-side is the last column of the signal matrix
|
|
444
|
+
system_matrix_column_indices=np.array([-1]),
|
|
445
|
+
function_indices=[FUNCTION_INDICES["signal"]])
|
|
446
|
+
|
|
447
|
+
### LASERS AND DETECTORS ###
|
|
448
|
+
if data["component"] in ["laser", "detector", "qnoised", "squeezer"]:
|
|
449
|
+
# target_index describes the position of the target to which the laser or detector is connected
|
|
450
|
+
try:
|
|
451
|
+
target_index = matrix_positions[data["target"]]
|
|
452
|
+
except KeyError:
|
|
453
|
+
# connected to space, so matrix instructions will only contain source or target index set
|
|
454
|
+
# in first edge loop
|
|
455
|
+
target_index = matrix_positions[data["target"] + ("_source" if data["port"] == "left" else "_target")]
|
|
456
|
+
# port_offset is the offset of a port (e.g. input / output) within the submatrix of the target component
|
|
457
|
+
try:
|
|
458
|
+
port_offset = PORT_DICTS[setup.nodes[data["target"]]["component"]][data["port"]]
|
|
459
|
+
except KeyError:
|
|
460
|
+
# space (always 0 as there is only 1 port)
|
|
461
|
+
port_offset = 0
|
|
462
|
+
# direction_offset is the offset of an input / output within a port
|
|
463
|
+
if data["direction"] not in ["in", "out"]:
|
|
464
|
+
raise ValueError("Direction has to be either in or out.")
|
|
465
|
+
|
|
466
|
+
target_is_matrix_component = False
|
|
467
|
+
try:
|
|
468
|
+
target_is_matrix_component = setup.nodes[data["target"]]["component"] in MATRIX_SIZES
|
|
469
|
+
except KeyError:
|
|
470
|
+
pass
|
|
471
|
+
if not target_is_matrix_component:
|
|
472
|
+
# In this case, laser or detector is connected to a space and the directions
|
|
473
|
+
# are reversed to conform with Finesse.
|
|
474
|
+
direction_offset = 1 if data["direction"] == "in" else 0
|
|
475
|
+
else:
|
|
476
|
+
# In this case, laser or detector is connected to a component and
|
|
477
|
+
# input should come before output.
|
|
478
|
+
direction_offset = 0 if data["direction"] == "in" else 1
|
|
479
|
+
|
|
480
|
+
# component_index is the position of the laser or detector in the system matrix
|
|
481
|
+
component_index = target_index + port_offset + direction_offset
|
|
482
|
+
matrix_positions[node] = component_index
|
|
483
|
+
if data["component"] == "detector":
|
|
484
|
+
try:
|
|
485
|
+
sideband = data["sideband"]
|
|
486
|
+
except KeyError:
|
|
487
|
+
sideband = "upper"
|
|
488
|
+
if sideband == "lower":
|
|
489
|
+
component_index += system_size
|
|
490
|
+
detectors[node] = component_index
|
|
491
|
+
detector_indices.append(component_index)
|
|
492
|
+
if data["component"] == "qnoised":
|
|
493
|
+
if not "auxiliary" in data or ("auxiliary" in data and not data["auxiliary"]):
|
|
494
|
+
noise_detectors[node] = noise_detector_count
|
|
495
|
+
# Here we set the entry that gets multiplied with the carrier solution from the detector position
|
|
496
|
+
# see finesse > detectors > compute > quantum.pyx > QND0Workspace > fill_selection_vector()
|
|
497
|
+
# upper sideband
|
|
498
|
+
noise_selection_vectors[noise_detector_count, 0, component_index] = np.sqrt(2)
|
|
499
|
+
# lower sideband
|
|
500
|
+
noise_selection_vectors[noise_detector_count, 0, component_index + system_size] = np.sqrt(2)
|
|
501
|
+
noise_detector_count += 1
|
|
502
|
+
if data["component"] == "laser":
|
|
503
|
+
# Mark target port as used
|
|
504
|
+
target_port = data["target"].replace('_source', '').replace('_target', '') + '.' + data["port"]
|
|
505
|
+
used_ports.add(target_port)
|
|
506
|
+
# fill right-hand-side with laser field
|
|
507
|
+
carrier_matrix[component_index, system_size] = laser_np(**data["properties"])
|
|
508
|
+
parameter_linking([data["properties"]["power"], data["properties"]["phase"]],
|
|
509
|
+
indices_to_link,
|
|
510
|
+
linked_names,
|
|
511
|
+
linking_function_indices,
|
|
512
|
+
parameters)
|
|
513
|
+
parameter_names += [f"{node}_power", f"{node}_phase"]
|
|
514
|
+
set_instructions(carrier_instructions,
|
|
515
|
+
node,
|
|
516
|
+
# power, phase
|
|
517
|
+
function_input_indices=np.array([[len(parameters)-2, len(parameters)-1]]),
|
|
518
|
+
output_column_indices=np.array([0]),
|
|
519
|
+
system_matrix_row_indices=np.array([component_index]),
|
|
520
|
+
# carrier right-hand-side is the last column of the carrier matrix
|
|
521
|
+
system_matrix_column_indices=np.array([-1]),
|
|
522
|
+
function_indices=[FUNCTION_INDICES["laser"]])
|
|
523
|
+
# lasers are a source of quantum noise, so they get an entry in the noise matrix at their position
|
|
524
|
+
set_instructions(noise_instructions,
|
|
525
|
+
node,
|
|
526
|
+
# placeholder
|
|
527
|
+
function_input_indices=np.array([[0]]),
|
|
528
|
+
output_column_indices=np.array([0]),
|
|
529
|
+
system_matrix_row_indices=np.array([component_index]),
|
|
530
|
+
system_matrix_column_indices=np.array([component_index]),
|
|
531
|
+
function_indices=[FUNCTION_INDICES["vacuum_quantum_noise"]],
|
|
532
|
+
system_size_for_sidebands=system_size)
|
|
533
|
+
if data["component"] == "squeezer":
|
|
534
|
+
# Mark target port as used
|
|
535
|
+
target_port = data["target"].replace('_source', '').replace('_target', '') + '.' + data["port"]
|
|
536
|
+
used_ports.add(target_port)
|
|
537
|
+
parameter_linking([data["properties"]["db"], data["properties"]["angle"]],
|
|
538
|
+
indices_to_link,
|
|
539
|
+
linked_names,
|
|
540
|
+
linking_function_indices,
|
|
541
|
+
parameters)
|
|
542
|
+
parameter_names += [f"{node}_db", f"{node}_angle"]
|
|
543
|
+
set_instructions(noise_instructions,
|
|
544
|
+
node,
|
|
545
|
+
# db, angle
|
|
546
|
+
function_input_indices=np.array([[len(parameters)-2, len(parameters)-1]]),
|
|
547
|
+
output_column_indices=np.array([0, 1, 2, 3]),
|
|
548
|
+
system_matrix_row_indices=np.array([component_index, component_index, component_index + system_size, component_index + system_size]),
|
|
549
|
+
system_matrix_column_indices=np.array([component_index, component_index + system_size, component_index + system_size, component_index]),
|
|
550
|
+
function_indices=[FUNCTION_INDICES["squeezer"]])
|
|
551
|
+
if data["component"] == "qhd":
|
|
552
|
+
parameter_linking([data["properties"]["phase"]],
|
|
553
|
+
indices_to_link,
|
|
554
|
+
linked_names,
|
|
555
|
+
linking_function_indices,
|
|
556
|
+
parameters)
|
|
557
|
+
parameter_names += [f"{node}_phase"]
|
|
558
|
+
detector1_index = matrix_positions[data["detector1"]]
|
|
559
|
+
detector2_index = matrix_positions[data["detector2"]]
|
|
560
|
+
# the carrier field of the second detector gets rotated by the specified phase in upper and lower sideband
|
|
561
|
+
qhd_parameter_indices.extend([len(parameters) - 1, len(parameters) - 1])
|
|
562
|
+
qhd_placing_indices.extend([[noise_detector_count, 0, detector2_index],
|
|
563
|
+
[noise_detector_count, 0, detector2_index + system_size]])
|
|
564
|
+
# upper sideband
|
|
565
|
+
noise_selection_vectors[noise_detector_count, 0, detector1_index] = np.sqrt(2)
|
|
566
|
+
noise_selection_vectors[noise_detector_count, 0, detector2_index] = np.sqrt(2)
|
|
567
|
+
# lower sideband
|
|
568
|
+
noise_selection_vectors[noise_detector_count, 0, detector1_index + system_size] = np.sqrt(2)
|
|
569
|
+
noise_selection_vectors[noise_detector_count, 0, detector2_index + system_size] = np.sqrt(2)
|
|
570
|
+
noise_detector_count += 1
|
|
571
|
+
|
|
572
|
+
### NOTHING ###
|
|
573
|
+
if data["component"] == "nothing":
|
|
574
|
+
nothing_index = matrix_positions[node]
|
|
575
|
+
matrix = nothing_matrix()
|
|
576
|
+
# initialize the carrier matrix with the nothing matrix
|
|
577
|
+
carrier_matrix[nothing_index:nothing_index + matrix.shape[0], nothing_index:nothing_index + matrix.shape[1]] = matrix
|
|
578
|
+
|
|
579
|
+
### DIRECTIONAL BEAMSPLITTER ###
|
|
580
|
+
if data["component"] == "directional_beamsplitter":
|
|
581
|
+
directional_beamsplitter_index = matrix_positions[node]
|
|
582
|
+
matrix = directional_beamsplitter_matrix()
|
|
583
|
+
carrier_matrix[directional_beamsplitter_index:directional_beamsplitter_index + matrix.shape[0], directional_beamsplitter_index:directional_beamsplitter_index + matrix.shape[1]] = matrix
|
|
584
|
+
|
|
585
|
+
### MIRRORS AND BEAMSPLITTERS ###
|
|
586
|
+
if data["component"] in ["mirror", "beamsplitter"]:
|
|
587
|
+
component_index = matrix_positions[node]
|
|
588
|
+
parameter_positions[node] = len(parameters)
|
|
589
|
+
if data["component"] == "mirror":
|
|
590
|
+
refractive_index_left_position = surfaces_to_refractive_index_parameter_position[node].get("left", 1)
|
|
591
|
+
refractive_index_right_position = surfaces_to_refractive_index_parameter_position[node].get("right", 1)
|
|
592
|
+
matrix = mirror_matrix(data["properties"]["loss"],
|
|
593
|
+
data["properties"]["reflectivity"],
|
|
594
|
+
data["properties"]["tuning"],
|
|
595
|
+
F,
|
|
596
|
+
parameters[refractive_index_left_position],
|
|
597
|
+
parameters[refractive_index_right_position])
|
|
598
|
+
parameter_linking([data["properties"]["loss"], data["properties"]["reflectivity"], data["properties"]["tuning"]],
|
|
599
|
+
indices_to_link,
|
|
600
|
+
linked_names,
|
|
601
|
+
linking_function_indices,
|
|
602
|
+
parameters)
|
|
603
|
+
parameter_names += [f"{node}_loss", f"{node}_reflectivity", f"{node}_tuning"]
|
|
604
|
+
loss_position = len(parameters) - 3
|
|
605
|
+
# loss, reflectivity, tuning, carrier_frequency, 2 refractive indices, 2 is the parameter position of 0 as default for alpha
|
|
606
|
+
function_input_indices = np.array([[len(parameters)-3, len(parameters)-2, len(parameters)-1, 0, refractive_index_left_position, refractive_index_right_position, 2]])
|
|
607
|
+
# parameter position of frequency changes because of sideband
|
|
608
|
+
signal_function_input_indices = np.array([[len(parameters)-3, len(parameters)-2, len(parameters)-1, signal_frequency_position, refractive_index_left_position, refractive_index_right_position, 2]])
|
|
609
|
+
# These indices pick out reflectivity_entry, transmissivity_entry and reflectivity_entry_minus
|
|
610
|
+
# in the order necessary for the mirror submatrix (indices are relative to the output of
|
|
611
|
+
# the reflectivity_transmissivity_tuning function in components.py)
|
|
612
|
+
output_column_indices = np.array([0, 1, 1, 2])
|
|
613
|
+
# These are now the indices of the matrix entries where the reflectivity and transmissivity
|
|
614
|
+
# entries will be placed. See the mirror_matrix function in components.py to understand
|
|
615
|
+
# the indices.
|
|
616
|
+
system_matrix_row_indices = component_index + np.array([1, 1, 3, 3])
|
|
617
|
+
system_matrix_column_indices = component_index + np.array([0, 2, 0, 2])
|
|
618
|
+
noise_output_column_indices = np.array([0, 0])
|
|
619
|
+
noise_system_matrix_indices = component_index + np.array([1, 3]) # output 1 and output 2
|
|
620
|
+
elif data["component"] == "beamsplitter":
|
|
621
|
+
refractive_index_left_position = surfaces_to_refractive_index_parameter_position[node].get("left", 1)
|
|
622
|
+
refractive_index_right_position = surfaces_to_refractive_index_parameter_position[node].get("right", 1)
|
|
623
|
+
matrix = beamsplitter_matrix(data["properties"]["loss"],
|
|
624
|
+
data["properties"]["reflectivity"],
|
|
625
|
+
data["properties"]["tuning"],
|
|
626
|
+
F,
|
|
627
|
+
parameters[refractive_index_left_position],
|
|
628
|
+
parameters[refractive_index_right_position],
|
|
629
|
+
data["properties"]["alpha"])
|
|
630
|
+
parameter_linking([data["properties"]["loss"], data["properties"]["reflectivity"], data["properties"]["tuning"], data["properties"]["alpha"]],
|
|
631
|
+
indices_to_link,
|
|
632
|
+
linked_names,
|
|
633
|
+
linking_function_indices,
|
|
634
|
+
parameters)
|
|
635
|
+
parameter_names += [f"{node}_loss", f"{node}_reflectivity", f"{node}_tuning", f"{node}_alpha"]
|
|
636
|
+
loss_position = len(parameters) - 4
|
|
637
|
+
# loss, reflectivity, tuning, carrier_frequency, 2 refractive indices, alpha
|
|
638
|
+
function_input_indices = np.array([[len(parameters)-4, len(parameters)-3, len(parameters)-2, 0, refractive_index_left_position, refractive_index_right_position, len(parameters)-1]])
|
|
639
|
+
# parameter position of frequency changes because of sideband
|
|
640
|
+
signal_function_input_indices = np.array([[len(parameters)-4, len(parameters)-3, len(parameters)-2, signal_frequency_position, refractive_index_left_position, refractive_index_right_position, len(parameters)-1]])
|
|
641
|
+
# These indices pick out reflectivity_entry, transmissivity_entry and reflectivity_entry_minus
|
|
642
|
+
# in the order necessary for the beamsplitter submatrix (indices are relative to the output of
|
|
643
|
+
# the reflectivity_transmissivity_tuning function in components.py)
|
|
644
|
+
output_column_indices = np.array([0, 1, 0, 1, 1, 2, 1, 2])
|
|
645
|
+
# These are now the indices of the matrix entries where the reflectivity and transmissivity
|
|
646
|
+
# entries will be placed. See the beamsplitter_matrix function in components.py to understand
|
|
647
|
+
# the indices.
|
|
648
|
+
system_matrix_row_indices = component_index + np.array([1, 1, 3, 3, 5, 5, 7, 7])
|
|
649
|
+
system_matrix_column_indices = component_index + np.array([2, 4, 0, 6, 0, 6, 2, 4])
|
|
650
|
+
noise_output_column_indices = np.array([0, 0, 0, 0])
|
|
651
|
+
noise_system_matrix_indices = component_index + np.array([1, 3, 5, 7]) # outputs 1, 2, 3 and 4
|
|
652
|
+
# initialize the carrier matrix with the mirror or beamsplitter matrix
|
|
653
|
+
carrier_matrix[component_index:component_index + matrix.shape[0], component_index:component_index + matrix.shape[1]] = matrix
|
|
654
|
+
set_instructions(carrier_instructions,
|
|
655
|
+
node,
|
|
656
|
+
function_input_indices=function_input_indices,
|
|
657
|
+
output_column_indices=output_column_indices,
|
|
658
|
+
system_matrix_row_indices=system_matrix_row_indices,
|
|
659
|
+
system_matrix_column_indices=system_matrix_column_indices,
|
|
660
|
+
function_indices=[FUNCTION_INDICES['surface']])
|
|
661
|
+
# Surface submatrix also has to change in signal run if e.g. tuning changes
|
|
662
|
+
set_instructions(signal_instructions,
|
|
663
|
+
node,
|
|
664
|
+
function_input_indices=signal_function_input_indices,
|
|
665
|
+
output_column_indices=output_column_indices,
|
|
666
|
+
system_matrix_row_indices=system_matrix_row_indices,
|
|
667
|
+
system_matrix_column_indices=system_matrix_column_indices,
|
|
668
|
+
function_indices=[FUNCTION_INDICES['surface']],
|
|
669
|
+
system_size_for_sidebands=system_size)
|
|
670
|
+
# losses are a source of quantum noise, so they get an entry in the noise matrix at their position
|
|
671
|
+
set_instructions(noise_instructions,
|
|
672
|
+
node,
|
|
673
|
+
# loss
|
|
674
|
+
function_input_indices = np.array([[loss_position]]),
|
|
675
|
+
output_column_indices = noise_output_column_indices,
|
|
676
|
+
system_matrix_row_indices = noise_system_matrix_indices,
|
|
677
|
+
system_matrix_column_indices = noise_system_matrix_indices,
|
|
678
|
+
function_indices=[FUNCTION_INDICES["loss_quantum_noise"]],
|
|
679
|
+
system_size_for_sidebands=system_size)
|
|
680
|
+
|
|
681
|
+
# Apply potential laser field modulations. This can only be done here, because both positions of laser
|
|
682
|
+
# and signal need to be known, which were only fixed in the second node loop.
|
|
683
|
+
for node in laser_to_signals:
|
|
684
|
+
laser_index = matrix_positions[node]
|
|
685
|
+
# update signal instructions with the corresponding system matrix row indices
|
|
686
|
+
signal_matrix_indices = []
|
|
687
|
+
function_indices = []
|
|
688
|
+
for signal in laser_to_signals[node]:
|
|
689
|
+
signal_index = matrix_positions[signal]
|
|
690
|
+
# system_size * 2 for the two sidebands, then *2 for upper and lower sideband
|
|
691
|
+
signal_matrix_indices += [system_size * 2 + signal_index] * 2
|
|
692
|
+
target_property = setup.nodes[signal]["target_property"]
|
|
693
|
+
if target_property == "amplitude":
|
|
694
|
+
function_indices.extend([FUNCTION_INDICES["laser_amplitude_modulation"], FUNCTION_INDICES["laser_amplitude_modulation"]])
|
|
695
|
+
elif target_property == "frequency":
|
|
696
|
+
function_indices.extend([FUNCTION_INDICES["laser_frequency_modulation"], FUNCTION_INDICES["laser_frequency_modulation_lower"]])
|
|
697
|
+
else:
|
|
698
|
+
raise ValueError("Target property for laser modulation has to be either amplitude or frequency.")
|
|
699
|
+
laser_signal_number = len(laser_to_signals[node])
|
|
700
|
+
system_matrix_row_indices = np.array([laser_index, laser_index + system_size] * laser_signal_number)
|
|
701
|
+
system_matrix_column_indices = np.array(signal_matrix_indices)
|
|
702
|
+
# Here we set the instructions to fill signal connectors in the signal matrix. These
|
|
703
|
+
# are located in the column of the respective signal and in the row of the respective laser.
|
|
704
|
+
set_instructions(signal_instructions,
|
|
705
|
+
node,
|
|
706
|
+
# frequency
|
|
707
|
+
function_input_indices=np.tile(np.array([[signal_frequency_position],
|
|
708
|
+
[signal_frequency_position]]), (laser_signal_number, 1)),
|
|
709
|
+
# for each laser modulation we take the first output of the corresponding function
|
|
710
|
+
output_column_indices=np.array([0, 0] * laser_signal_number),
|
|
711
|
+
# e.g. for laser_signal_number = 1: [0, 1], for 2: [0, 1, 2, 3]
|
|
712
|
+
output_row_indices=np.arange(laser_signal_number * 2),
|
|
713
|
+
system_matrix_row_indices=system_matrix_row_indices,
|
|
714
|
+
system_matrix_column_indices=system_matrix_column_indices,
|
|
715
|
+
function_indices=function_indices,
|
|
716
|
+
carrier_indices=np.array([laser_index, laser_index] * laser_signal_number),
|
|
717
|
+
carrier_row_indices=system_matrix_row_indices,
|
|
718
|
+
carrier_column_indices=system_matrix_column_indices)
|
|
719
|
+
|
|
720
|
+
# set entries for free masses. This can only be done here, as we need all parameters from
|
|
721
|
+
# the mirrors and beamsplitters.
|
|
722
|
+
for node in free_masses:
|
|
723
|
+
data = setup.nodes[node]
|
|
724
|
+
try:
|
|
725
|
+
component_index = matrix_positions[data["target"]]
|
|
726
|
+
parameter_position = parameter_positions[data["target"]]
|
|
727
|
+
target_component = setup.nodes[data["target"]]["component"]
|
|
728
|
+
except KeyError:
|
|
729
|
+
raise ValueError("Free mass has to be connected to either a mirror or a beamsplitter.")
|
|
730
|
+
if target_component == "mirror":
|
|
731
|
+
# f is at index, z is at index + 1
|
|
732
|
+
free_mass_index = matrix_positions[node] + system_size
|
|
733
|
+
parameter_linking([data["properties"]["mass"]],
|
|
734
|
+
indices_to_link,
|
|
735
|
+
linked_names,
|
|
736
|
+
linking_function_indices,
|
|
737
|
+
parameters)
|
|
738
|
+
parameter_names += [f"{node}_mass"]
|
|
739
|
+
# f_to_z connector, 4 x mirror to force connector, 2 x z to mirror connector
|
|
740
|
+
system_matrix_row_indices = np.array([free_mass_index + 1, free_mass_index, free_mass_index, free_mass_index, free_mass_index, component_index + 1, component_index + 3])
|
|
741
|
+
system_matrix_column_indices = np.array([free_mass_index, component_index, component_index + 1, component_index + 2, component_index + 3, free_mass_index + 1, free_mass_index + 1])
|
|
742
|
+
refractive_index_left_position = surfaces_to_refractive_index_parameter_position[data["target"]].get("left", 1)
|
|
743
|
+
refractive_index_right_position = surfaces_to_refractive_index_parameter_position[data["target"]].get("right", 1)
|
|
744
|
+
# fill coupling entries from f to z, fill coupling entries from mirrors to f, fill coupling entries from z to mirror outputs
|
|
745
|
+
# See finesse > components > modal > mirror.pyx > single_z_mechanical_frequency_signal_calc to understand
|
|
746
|
+
# why we need which entries. Also finesse > components > mechanical.pyx > FreeMass > fill
|
|
747
|
+
set_instructions(signal_instructions,
|
|
748
|
+
node,
|
|
749
|
+
function_input_indices=np.array([# signal_frequency, mass, the zeros at the end are placeholder values which are not used.
|
|
750
|
+
[signal_frequency_position, len(parameters)-1, 0, 0, 0, 0, 0],
|
|
751
|
+
# 2 is position of 0 as default value for alpha, rest are placeholder values
|
|
752
|
+
[2, 0, 0, 0, 0, 0, 0],
|
|
753
|
+
# 2 is position of 0 as default value for alpha, refractive_index_left, refractive_index_right, rest are placeholder values
|
|
754
|
+
[2, refractive_index_left_position, refractive_index_right_position, 0, 0, 0, 0],
|
|
755
|
+
# signal_frequency, tuning, reflectivity, loss, refractive_index, 2 is the position of the default 0 for alpha set at the beginning, last 0 is placeholder value
|
|
756
|
+
[signal_frequency_position, parameter_position + 2, parameter_position + 1, parameter_position, refractive_index_left_position, 2, 0],
|
|
757
|
+
[signal_frequency_position, parameter_position + 2, parameter_position + 1, parameter_position, refractive_index_left_position, refractive_index_right_position, 2],
|
|
758
|
+
]),
|
|
759
|
+
output_row_indices=np.array([0, 1, 1, 2, 2, 3, 4]),
|
|
760
|
+
output_column_indices=np.array([0] * 7),
|
|
761
|
+
system_matrix_row_indices=system_matrix_row_indices,
|
|
762
|
+
system_matrix_column_indices=system_matrix_column_indices,
|
|
763
|
+
function_indices=[FUNCTION_INDICES["f_to_z"],
|
|
764
|
+
FUNCTION_INDICES["optical_to_mechanical_left"],
|
|
765
|
+
FUNCTION_INDICES["optical_to_mechanical_right"],
|
|
766
|
+
FUNCTION_INDICES["mechanical_to_optical_left"],
|
|
767
|
+
FUNCTION_INDICES["mechanical_to_optical_right"]],
|
|
768
|
+
# 4 mirror fields, 2 mirror inputs
|
|
769
|
+
carrier_indices=np.array([component_index, component_index + 1, component_index + 2, component_index + 3, component_index, component_index + 2]),
|
|
770
|
+
carrier_row_indices=system_matrix_row_indices[1:],
|
|
771
|
+
carrier_column_indices=system_matrix_column_indices[1:],
|
|
772
|
+
system_size_for_sidebands=system_size)
|
|
773
|
+
elif target_component == "beamsplitter":
|
|
774
|
+
# f is at index, z is at index + 1
|
|
775
|
+
free_mass_index = matrix_positions[node] + system_size
|
|
776
|
+
# TODO: mass is not yet optimizable
|
|
777
|
+
parameter_linking([data["properties"]["mass"]],
|
|
778
|
+
indices_to_link,
|
|
779
|
+
linked_names,
|
|
780
|
+
linking_function_indices,
|
|
781
|
+
parameters)
|
|
782
|
+
parameter_names += [f"{node}_mass"]
|
|
783
|
+
# f_to_z connector, 8 x beamsplitter to force connector, 4 x z to mirror connector (beamsplitter outputs)
|
|
784
|
+
system_matrix_row_indices = np.array([free_mass_index + 1, free_mass_index, free_mass_index, free_mass_index, free_mass_index, free_mass_index, free_mass_index, free_mass_index, free_mass_index, component_index + 1, component_index + 3, component_index + 5, component_index + 7])
|
|
785
|
+
system_matrix_column_indices = np.array([free_mass_index, component_index, component_index + 1, component_index + 2, component_index + 3, component_index + 4, component_index + 5, component_index + 6, component_index + 7, free_mass_index + 1, free_mass_index + 1, free_mass_index + 1, free_mass_index + 1])
|
|
786
|
+
refractive_index_left_position = surfaces_to_refractive_index_parameter_position[data["target"]].get("left", 1)
|
|
787
|
+
refractive_index_right_position = surfaces_to_refractive_index_parameter_position[data["target"]].get("right", 1)
|
|
788
|
+
# fill coupling entries from f to z, fill coupling entries from mirrors to f, fill coupling entries from z to mirror outputs
|
|
789
|
+
# See finesse > components > modal > beamsplitter.pyx > single_z_mechanical_frequency_signal_calc to understand
|
|
790
|
+
# why we need which entries. Also finesse > components > mechanical.pyx > FreeMass > fill
|
|
791
|
+
set_instructions(signal_instructions,
|
|
792
|
+
node,
|
|
793
|
+
function_input_indices=np.array([# signal_frequency, mass, the last two indices are placeholder values which are not used.
|
|
794
|
+
[signal_frequency_position, len(parameters)-1, 0, 0, 0, 0, 0],
|
|
795
|
+
# alpha, rest are placeholder values
|
|
796
|
+
[parameter_position + 3, 0, 0, 0, 0, 0, 0],
|
|
797
|
+
# alpha, refractive_index_left, refractive_index_right
|
|
798
|
+
[parameter_position + 3, refractive_index_left_position, refractive_index_right_position, 0, 0, 0, 0],
|
|
799
|
+
# signal_frequency, tuning, reflectivity, loss, refractive_index, alpha
|
|
800
|
+
[signal_frequency_position, parameter_position + 2, parameter_position + 1, parameter_position, refractive_index_left_position, parameter_position + 3, 0],
|
|
801
|
+
[signal_frequency_position, parameter_position + 2, parameter_position + 1, parameter_position, refractive_index_left_position, refractive_index_right_position, parameter_position + 3],
|
|
802
|
+
]),
|
|
803
|
+
output_row_indices=np.array([0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4]),
|
|
804
|
+
output_column_indices=np.array([0] * 13),
|
|
805
|
+
system_matrix_row_indices=system_matrix_row_indices,
|
|
806
|
+
system_matrix_column_indices=system_matrix_column_indices,
|
|
807
|
+
function_indices=[FUNCTION_INDICES["f_to_z"],
|
|
808
|
+
FUNCTION_INDICES["optical_to_mechanical_left"],
|
|
809
|
+
FUNCTION_INDICES["optical_to_mechanical_right"],
|
|
810
|
+
FUNCTION_INDICES["mechanical_to_optical_left"],
|
|
811
|
+
FUNCTION_INDICES["mechanical_to_optical_right"]],
|
|
812
|
+
# 8 beamsplitter fields, 4 beamsplitter inputs (top input for left output, left input for top output, bottom input for right output, right input for bottom output)
|
|
813
|
+
carrier_indices=np.array([component_index, component_index + 1, component_index + 2, component_index + 3, component_index + 4, component_index + 5, component_index + 6, component_index + 7, component_index + 2, component_index, component_index + 6, component_index + 4]),
|
|
814
|
+
carrier_row_indices=system_matrix_row_indices[1:],
|
|
815
|
+
carrier_column_indices=system_matrix_column_indices[1:],
|
|
816
|
+
system_size_for_sidebands=system_size)
|
|
817
|
+
|
|
818
|
+
# Identify the indices of the connector entries in the system matrix and fill the system matrix
|
|
819
|
+
for (source, target, data) in setup.edges(data=True):
|
|
820
|
+
source_node = setup.nodes[source]
|
|
821
|
+
target_node = setup.nodes[target]
|
|
822
|
+
|
|
823
|
+
# Mark source and target ports as used
|
|
824
|
+
if source_node["component"] in MATRIX_SIZES:
|
|
825
|
+
used_ports.add(source + '.' + data["source_port"])
|
|
826
|
+
if target_node["component"] in MATRIX_SIZES:
|
|
827
|
+
used_ports.add(target + '.' + data["target_port"])
|
|
828
|
+
|
|
829
|
+
source_index = matrix_positions[source]
|
|
830
|
+
target_index = matrix_positions[target]
|
|
831
|
+
try:
|
|
832
|
+
source_port_index = PORT_DICTS[setup.nodes[source]["component"]][data['source_port']]
|
|
833
|
+
source_input_index = source_index + source_port_index
|
|
834
|
+
source_output_index = source_index + source_port_index + 1
|
|
835
|
+
except KeyError:
|
|
836
|
+
# laser is at input index (See couplings.excalidraw)
|
|
837
|
+
source_input_index = source_index - 1
|
|
838
|
+
source_output_index = source_index
|
|
839
|
+
try:
|
|
840
|
+
target_port_index = PORT_DICTS[setup.nodes[target]["component"]][data['target_port']]
|
|
841
|
+
target_input_index = target_index + target_port_index
|
|
842
|
+
target_output_index = target_index + target_port_index + 1
|
|
843
|
+
except KeyError:
|
|
844
|
+
# detector is at output index (See couplings.excalidraw)
|
|
845
|
+
target_input_index = target_index + 1
|
|
846
|
+
target_output_index = target_index
|
|
847
|
+
|
|
848
|
+
# We use the full space function in case F changes at some point
|
|
849
|
+
space_entry = space(jnp.array([F, data["properties"]["length"], data["properties"]["refractive_index"]]))[0]
|
|
850
|
+
# initialize carrier matrix with space entries
|
|
851
|
+
carrier_matrix[source_input_index, target_output_index] = space_entry
|
|
852
|
+
carrier_matrix[target_input_index, source_output_index] = space_entry
|
|
853
|
+
|
|
854
|
+
# parameters have been added in first edge loop already as they are needed by surfaces
|
|
855
|
+
parameter_position = parameter_positions[f"{source}_{target}"]
|
|
856
|
+
set_instructions(carrier_instructions,
|
|
857
|
+
f"{source}_{target}",
|
|
858
|
+
# carrier frequency, length, refractive_index
|
|
859
|
+
function_input_indices=np.array([[0, parameter_position, parameter_position + 1]]),
|
|
860
|
+
output_column_indices=np.array([0, 0]),
|
|
861
|
+
system_matrix_row_indices=np.array([source_input_index, target_input_index]),
|
|
862
|
+
system_matrix_column_indices=np.array([target_output_index, source_output_index]),
|
|
863
|
+
function_indices=[FUNCTION_INDICES['space']])
|
|
864
|
+
|
|
865
|
+
# all spaces need to be updated in the signal run because the frequency changes for the sidebands.
|
|
866
|
+
# Lower sideband is handled manually here because so far the negative frequency is only important
|
|
867
|
+
# for these space entries.
|
|
868
|
+
set_instructions(signal_instructions,
|
|
869
|
+
f"{source}_{target}",
|
|
870
|
+
# signal_freqency, length, refractive_index
|
|
871
|
+
function_input_indices=np.array([[signal_frequency_position, parameter_position, parameter_position + 1],
|
|
872
|
+
[signal_frequency_position, parameter_position, parameter_position + 1]]),
|
|
873
|
+
output_row_indices=np.array([0, 0, 1, 1]),
|
|
874
|
+
output_column_indices=np.array([0, 0, 0, 0]),
|
|
875
|
+
# 2 upper sideband entries, 2 lower sideband entries
|
|
876
|
+
system_matrix_row_indices=np.array([source_input_index, target_input_index, source_input_index + system_size, target_input_index + system_size]),
|
|
877
|
+
system_matrix_column_indices=np.array([target_output_index, source_output_index, target_output_index + system_size, source_output_index + system_size]),
|
|
878
|
+
function_indices=[FUNCTION_INDICES['space'], FUNCTION_INDICES["space_lower"]])
|
|
879
|
+
|
|
880
|
+
# Only now we can set the signal instructions for the strain signals acting on the respective spaces
|
|
881
|
+
# because we didn't know where these spaces would end up in the system matrix before.
|
|
882
|
+
if f"{source}_{target}" in space_to_signals:
|
|
883
|
+
# update signal instructions with the corresponding system matrix row indices
|
|
884
|
+
signal_matrix_indices = []
|
|
885
|
+
for signal in space_to_signals[f"{source}_{target}"]:
|
|
886
|
+
signal_index = matrix_positions[signal]
|
|
887
|
+
# system_size * 2 for the two sidebands, then *4 because 2 for upper and two for lower sideband
|
|
888
|
+
signal_matrix_indices += [system_size * 2 + signal_index] * 4
|
|
889
|
+
space_signal_number = len(space_to_signals[f"{source}_{target}"])
|
|
890
|
+
system_matrix_row_indices=np.array([source_input_index, target_input_index, source_input_index + system_size, target_input_index + system_size] * space_signal_number)
|
|
891
|
+
system_matrix_column_indices=np.array(signal_matrix_indices)
|
|
892
|
+
# Here we set the instructions to fill signal connectors in the signal matrix. Signal connectors are
|
|
893
|
+
# located in the column of the respective signal and in the row of the respective space.
|
|
894
|
+
set_instructions(signal_instructions,
|
|
895
|
+
# different key than in the space_signal case, because we don't want to override
|
|
896
|
+
# the space_signal case and the keys are not needed any more, so we can choose any
|
|
897
|
+
f"{source}_{target}_modulation",
|
|
898
|
+
# frequency, length, refractive index
|
|
899
|
+
function_input_indices=np.tile(np.array([[signal_frequency_position, parameter_position, parameter_position + 1],
|
|
900
|
+
[signal_frequency_position, parameter_position, parameter_position + 1]]), (space_signal_number, 1)),
|
|
901
|
+
# 2 signal entries * 2 sidebands for each signal
|
|
902
|
+
output_column_indices=np.array([0, 0, 0, 0] * space_signal_number),
|
|
903
|
+
# e.g. for space_signal_number = 1: [0, 0, 1, 1], for 2: [0, 0, 1, 1, 2, 2, 3, 3], ...
|
|
904
|
+
output_row_indices=np.repeat(np.arange(space_signal_number * 2), 2),
|
|
905
|
+
# two signal connector entries for each signal
|
|
906
|
+
system_matrix_row_indices=system_matrix_row_indices,
|
|
907
|
+
system_matrix_column_indices=system_matrix_column_indices,
|
|
908
|
+
function_indices=[FUNCTION_INDICES["space_modulation"], FUNCTION_INDICES["space_modulation_lower"]] * space_signal_number,
|
|
909
|
+
carrier_indices=np.array([source_input_index, target_input_index, source_input_index, target_input_index] * space_signal_number),
|
|
910
|
+
carrier_row_indices=system_matrix_row_indices,
|
|
911
|
+
carrier_column_indices=system_matrix_column_indices)
|
|
912
|
+
|
|
913
|
+
# insert carrier matrix into signal matrix for first sideband
|
|
914
|
+
signal_matrix[:system_size, :system_size] = carrier_matrix[:, :system_size]
|
|
915
|
+
# insert carrier matrix into signal matrix for second sideband
|
|
916
|
+
signal_matrix[system_size:-signal_size, system_size:-signal_size-1] = carrier_matrix[:, :system_size]
|
|
917
|
+
|
|
918
|
+
# filter out unused ports and add vacuum noise to the noise matrix
|
|
919
|
+
for node_port in all_ports - used_ports:
|
|
920
|
+
node, port = node_port.split('.')
|
|
921
|
+
try:
|
|
922
|
+
port_offset = PORT_DICTS[setup.nodes[node]["component"]][port]
|
|
923
|
+
except KeyError:
|
|
924
|
+
# space
|
|
925
|
+
port_offset = 0
|
|
926
|
+
node_index = matrix_positions[node]
|
|
927
|
+
port_index = node_index + port_offset
|
|
928
|
+
# unused ports are a source of quantum noise, so they get an entry in the noise matrix at their position
|
|
929
|
+
set_instructions(noise_instructions,
|
|
930
|
+
node_port,
|
|
931
|
+
# placeholder
|
|
932
|
+
function_input_indices=np.array([[0]]),
|
|
933
|
+
output_column_indices=np.array([0]),
|
|
934
|
+
system_matrix_row_indices=np.array([port_index]),
|
|
935
|
+
system_matrix_column_indices=np.array([port_index]),
|
|
936
|
+
function_indices=[FUNCTION_INDICES["vacuum_quantum_noise"]],
|
|
937
|
+
system_size_for_sidebands=system_size)
|
|
938
|
+
|
|
939
|
+
# convert linked_names to indices
|
|
940
|
+
linked_indices = [parameter_names.index(name) for name in linked_names]
|
|
941
|
+
if len(linked_indices) == 0:
|
|
942
|
+
linked_indices = [0]
|
|
943
|
+
indices_to_link = [0]
|
|
944
|
+
LINKING_FUNCTIONS.append(lambda x: x)
|
|
945
|
+
linking_function_indices = [0]
|
|
946
|
+
|
|
947
|
+
return (
|
|
948
|
+
# instructions
|
|
949
|
+
(carrier_instructions,
|
|
950
|
+
signal_instructions,
|
|
951
|
+
noise_instructions,
|
|
952
|
+
signal_components,
|
|
953
|
+
parameter_names),
|
|
954
|
+
# matrices
|
|
955
|
+
(jnp.array([parameters], dtype=complex),
|
|
956
|
+
jnp.array(carrier_matrix),
|
|
957
|
+
jnp.array(signal_matrix),
|
|
958
|
+
jnp.array(noise_matrix, dtype=complex),
|
|
959
|
+
jnp.array(noise_selection_vectors, dtype=complex),
|
|
960
|
+
jnp.array(qhd_parameter_indices, dtype=int),
|
|
961
|
+
jnp.array(qhd_placing_indices, dtype=int),
|
|
962
|
+
jnp.array(linked_indices, dtype=int),
|
|
963
|
+
jnp.array(linking_function_indices, dtype=int),
|
|
964
|
+
jnp.array(indices_to_link, dtype=int)),
|
|
965
|
+
# metadata
|
|
966
|
+
(jnp.array(detector_indices, dtype=int),
|
|
967
|
+
jnp.array(mirror_indices, dtype=int),
|
|
968
|
+
jnp.array(beamsplitter_indices, dtype=int),
|
|
969
|
+
jnp.array(isolator_indices, dtype=int)),
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def pairs_to_arrays(
|
|
974
|
+
parameter_instructions: dict,
|
|
975
|
+
signal_instructions: dict,
|
|
976
|
+
noise_instructions: dict,
|
|
977
|
+
signal_components: list,
|
|
978
|
+
parameter_names: list,
|
|
979
|
+
optimization_pairs: list = None,
|
|
980
|
+
changing_pairs: list = None
|
|
981
|
+
):
|
|
982
|
+
# this will be used by both carrier and signal solver
|
|
983
|
+
optimized_parameter_indices = []
|
|
984
|
+
# optimization_value_indices is introduced to allow for multiple optimized parameters that all use the same value
|
|
985
|
+
optimization_value_indices = []
|
|
986
|
+
|
|
987
|
+
carrier_components = []
|
|
988
|
+
carrier_changing_parameter_indices = []
|
|
989
|
+
carrier_arrays = defaultdict(list)
|
|
990
|
+
|
|
991
|
+
signal_changing_parameter_indices = []
|
|
992
|
+
signal_arrays = defaultdict(list)
|
|
993
|
+
|
|
994
|
+
noise_components = []
|
|
995
|
+
noise_arrays = defaultdict(list)
|
|
996
|
+
|
|
997
|
+
if optimization_pairs is None:
|
|
998
|
+
optimization_pairs = []
|
|
999
|
+
if changing_pairs is None:
|
|
1000
|
+
changing_pairs = []
|
|
1001
|
+
|
|
1002
|
+
signal_component_number = 0
|
|
1003
|
+
# if there is a signal, all spaces and signal fields have to be updated after the carrier has been solved.
|
|
1004
|
+
# This is why (different from the carrier components) we have to iterate through all of them.
|
|
1005
|
+
|
|
1006
|
+
for instruction in signal_instructions.values():
|
|
1007
|
+
# frequency only has instructions to update optimized_parameter_indices and nothing else
|
|
1008
|
+
# as it gets updated together with all the other entries.
|
|
1009
|
+
try:
|
|
1010
|
+
instruction["function_indices"]
|
|
1011
|
+
except KeyError:
|
|
1012
|
+
continue
|
|
1013
|
+
signal_arrays["function_input_indices"].append(instruction["function_input_indices"])
|
|
1014
|
+
signal_arrays["function_indices"].extend(instruction["function_indices"])
|
|
1015
|
+
signal_arrays["output_column_indices"].append(instruction["output_column_indices"])
|
|
1016
|
+
if "output_row_indices" in instruction:
|
|
1017
|
+
signal_arrays["output_row_indices"].append(instruction["output_row_indices"].flatten() + signal_component_number)
|
|
1018
|
+
individual_rows = len(np.unique(instruction["output_row_indices"]))
|
|
1019
|
+
signal_component_number += individual_rows
|
|
1020
|
+
else:
|
|
1021
|
+
signal_arrays["output_row_indices"].append(np.ones(len(instruction["output_column_indices"])) * signal_component_number)
|
|
1022
|
+
signal_component_number += 1
|
|
1023
|
+
signal_arrays["system_matrix_row_indices"].append(instruction["system_matrix_row_indices"])
|
|
1024
|
+
signal_arrays["system_matrix_column_indices"].append(instruction["system_matrix_column_indices"])
|
|
1025
|
+
if "carrier_indices" in instruction:
|
|
1026
|
+
signal_arrays["carrier_indices"].append(instruction["carrier_indices"])
|
|
1027
|
+
signal_arrays["carrier_row_indices"].append(instruction["carrier_row_indices"])
|
|
1028
|
+
signal_arrays["carrier_column_indices"].append(instruction["carrier_column_indices"])
|
|
1029
|
+
|
|
1030
|
+
def append_information(instructions, arrays, component, components=None):
|
|
1031
|
+
component_function_input_indices = instructions[component]["function_input_indices"]
|
|
1032
|
+
# each component has functions that act on certain parameters. If a component is already in the list,
|
|
1033
|
+
# then all its parameters already get processed by the respective functions.
|
|
1034
|
+
if component in components:
|
|
1035
|
+
return None
|
|
1036
|
+
else:
|
|
1037
|
+
components.append(component)
|
|
1038
|
+
arrays["function_input_indices"].append(component_function_input_indices)
|
|
1039
|
+
arrays["function_indices"].extend(instructions[component]["function_indices"])
|
|
1040
|
+
arrays["output_column_indices"].append(instructions[component]["output_column_indices"])
|
|
1041
|
+
# the number of components equals the number of function calls and therefore the number of rows in the output.
|
|
1042
|
+
# all the outputs are in the same row
|
|
1043
|
+
arrays["output_row_indices"].append(np.ones(len(instructions[component]["output_column_indices"])) * (len(components) - 1))
|
|
1044
|
+
arrays["system_matrix_row_indices"].append(instructions[component]["system_matrix_row_indices"])
|
|
1045
|
+
arrays["system_matrix_column_indices"].append(instructions[component]["system_matrix_column_indices"])
|
|
1046
|
+
|
|
1047
|
+
# compile noise arrays
|
|
1048
|
+
for component, instruction in noise_instructions.items():
|
|
1049
|
+
append_information(noise_instructions, noise_arrays, component, noise_components)
|
|
1050
|
+
|
|
1051
|
+
# if changing or optimized components are from signal solver, we only have to update the parameter indices
|
|
1052
|
+
# because all entries will be updated anyway.
|
|
1053
|
+
for ix, optimization_pair in enumerate(optimization_pairs):
|
|
1054
|
+
if not isinstance(optimization_pair[0], list):
|
|
1055
|
+
optimized_component, optimized_parameter = optimization_pair
|
|
1056
|
+
try:
|
|
1057
|
+
optimized_parameter_indices.append(parameter_names.index(f"{optimized_component}_{optimized_parameter}"))
|
|
1058
|
+
optimization_value_indices.append(ix)
|
|
1059
|
+
except ValueError:
|
|
1060
|
+
pass
|
|
1061
|
+
else:
|
|
1062
|
+
# allow for multiple optimized parameters that all use the same value
|
|
1063
|
+
for optimized_component, optimized_parameter in optimization_pair:
|
|
1064
|
+
try:
|
|
1065
|
+
optimized_parameter_indices.append(parameter_names.index(f"{optimized_component}_{optimized_parameter}"))
|
|
1066
|
+
optimization_value_indices.append(ix)
|
|
1067
|
+
except ValueError:
|
|
1068
|
+
pass
|
|
1069
|
+
|
|
1070
|
+
if optimized_component not in signal_components and optimized_component in parameter_instructions:
|
|
1071
|
+
append_information(parameter_instructions, carrier_arrays, optimized_component, carrier_components)
|
|
1072
|
+
|
|
1073
|
+
# multiple changing parameters can be updated at the same time (e.g. loss and reflectivity)
|
|
1074
|
+
for changing_component, changing_parameter in changing_pairs:
|
|
1075
|
+
if changing_component in signal_components:
|
|
1076
|
+
signal_changing_parameter_indices.append(parameter_names.index(f"{changing_component}_{changing_parameter}"))
|
|
1077
|
+
else:
|
|
1078
|
+
carrier_changing_parameter_indices.append(parameter_names.index(f"{changing_component}_{changing_parameter}"))
|
|
1079
|
+
append_information(parameter_instructions, carrier_arrays, changing_component, carrier_components)
|
|
1080
|
+
|
|
1081
|
+
def combine_arrays(arrays, signal: bool = False):
|
|
1082
|
+
if len(arrays["function_input_indices"]) == 0:
|
|
1083
|
+
# In case there are no carrier or signal matrix entries to change, these parameters get set to
|
|
1084
|
+
# dummy values that replace the 1 at position [0, 0] of the respective matrix with 1 (do nothing).
|
|
1085
|
+
# function_input_indices, function_indices, output_indices, system_matrix_indices
|
|
1086
|
+
return jnp.array([[0, 0, 0]]), jnp.array([6]), jnp.array([[0], [0]]), jnp.array([[0], [0]])
|
|
1087
|
+
else:
|
|
1088
|
+
function_input_indices = np.vstack(arrays["function_input_indices"])
|
|
1089
|
+
output_indices = np.stack((np.concatenate(arrays["output_row_indices"]), np.concatenate(arrays["output_column_indices"])))
|
|
1090
|
+
system_matrix_indices = np.stack((np.concatenate(arrays["system_matrix_row_indices"]), np.concatenate(arrays["system_matrix_column_indices"])))
|
|
1091
|
+
if signal:
|
|
1092
|
+
# dummy values
|
|
1093
|
+
carrier_solution_indices = np.array([0])
|
|
1094
|
+
# place in the first row of the last column of the system matrix which
|
|
1095
|
+
# should be guaranteed to be empty as it is the rhs of the signal system
|
|
1096
|
+
# which can only have non-zero entries in the last rows where the signals
|
|
1097
|
+
# live
|
|
1098
|
+
carrier_solution_placing_indices = np.array([[0], [-1]])
|
|
1099
|
+
if "carrier_indices" in arrays:
|
|
1100
|
+
carrier_solution_indices = np.concatenate(arrays["carrier_indices"]),
|
|
1101
|
+
carrier_solution_placing_indices = np.stack((np.concatenate(arrays["carrier_row_indices"]), np.concatenate(arrays["carrier_column_indices"])))
|
|
1102
|
+
return (jnp.array(function_input_indices, dtype=int),
|
|
1103
|
+
jnp.array(arrays["function_indices"], dtype=int),
|
|
1104
|
+
jnp.array(output_indices, dtype=int),
|
|
1105
|
+
jnp.array(system_matrix_indices, dtype=int),
|
|
1106
|
+
jnp.array(carrier_solution_indices, dtype=int),
|
|
1107
|
+
jnp.array(carrier_solution_placing_indices, dtype=int))
|
|
1108
|
+
return (jnp.array(function_input_indices, dtype=int),
|
|
1109
|
+
jnp.array(arrays["function_indices"], dtype=int),
|
|
1110
|
+
jnp.array(output_indices, dtype=int),
|
|
1111
|
+
jnp.array(system_matrix_indices, dtype=int))
|
|
1112
|
+
|
|
1113
|
+
return (
|
|
1114
|
+
# arrays to prepare
|
|
1115
|
+
(jnp.array(optimized_parameter_indices, dtype=int),
|
|
1116
|
+
jnp.array(optimization_value_indices, dtype=int),
|
|
1117
|
+
jnp.array(carrier_changing_parameter_indices, dtype=int),
|
|
1118
|
+
jnp.array(signal_changing_parameter_indices, dtype=int)),
|
|
1119
|
+
# carrier_arrays
|
|
1120
|
+
combine_arrays(carrier_arrays),
|
|
1121
|
+
# signal_arrays
|
|
1122
|
+
combine_arrays(signal_arrays, signal=True),
|
|
1123
|
+
# noise_arrays
|
|
1124
|
+
combine_arrays(noise_arrays)
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def prepare_arrays(
|
|
1129
|
+
optimized_parameter_indices,
|
|
1130
|
+
optimized_value_indices,
|
|
1131
|
+
carrier_changing_parameter_indices,
|
|
1132
|
+
signal_changing_parameter_indices,
|
|
1133
|
+
carrier_changing_values,
|
|
1134
|
+
parameters
|
|
1135
|
+
):
|
|
1136
|
+
optimized_parameters = None
|
|
1137
|
+
|
|
1138
|
+
# setup dummy arrays in case any array is empty to still comply with the vectorization scheme later
|
|
1139
|
+
# parameters has shape (1, N) where N is the number of parameters
|
|
1140
|
+
if len(optimized_parameter_indices) == 0:
|
|
1141
|
+
optimized_parameter_indices = jnp.array([0])
|
|
1142
|
+
optimized_parameters = jnp.array(parameters[0][optimized_parameter_indices])
|
|
1143
|
+
optimized_value_indices = jnp.array([0])
|
|
1144
|
+
|
|
1145
|
+
if carrier_changing_values is not None:
|
|
1146
|
+
if len(carrier_changing_values.shape) == 1:
|
|
1147
|
+
carrier_changing_values = carrier_changing_values.reshape(1, -1)
|
|
1148
|
+
signal_changing_values = carrier_changing_values.copy()
|
|
1149
|
+
|
|
1150
|
+
if len(carrier_changing_parameter_indices) == 0:
|
|
1151
|
+
carrier_changing_parameter_indices = jnp.array([0])
|
|
1152
|
+
# values needs to have shape (V, R) with V as number of changing parameters
|
|
1153
|
+
# and R as number of values
|
|
1154
|
+
carrier_changing_values = jnp.array([parameters[0][carrier_changing_parameter_indices]])
|
|
1155
|
+
if len(signal_changing_parameter_indices) == 0:
|
|
1156
|
+
signal_changing_parameter_indices = jnp.array([0])
|
|
1157
|
+
signal_changing_values = jnp.array([parameters[0][signal_changing_parameter_indices]])
|
|
1158
|
+
|
|
1159
|
+
return (
|
|
1160
|
+
optimized_parameters,
|
|
1161
|
+
optimized_parameter_indices,
|
|
1162
|
+
optimized_value_indices,
|
|
1163
|
+
carrier_changing_parameter_indices,
|
|
1164
|
+
carrier_changing_values,
|
|
1165
|
+
signal_changing_parameter_indices,
|
|
1166
|
+
signal_changing_values
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
### SUPPORT DICTIONARIES ###
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
# with respect to FUNCTION_LIST in components.py
|
|
1174
|
+
FUNCTION_INDICES = {
|
|
1175
|
+
'laser': 1,
|
|
1176
|
+
'surface': 2,
|
|
1177
|
+
'space': 3,
|
|
1178
|
+
'space_modulation': 4,
|
|
1179
|
+
'signal': 5,
|
|
1180
|
+
'dummy': 6,
|
|
1181
|
+
'vacuum_quantum_noise': 7,
|
|
1182
|
+
'loss_quantum_noise': 8,
|
|
1183
|
+
'laser_amplitude_modulation': 10,
|
|
1184
|
+
'laser_frequency_modulation': 11,
|
|
1185
|
+
'f_to_z': 12,
|
|
1186
|
+
'optical_to_mechanical_left': 13,
|
|
1187
|
+
'mechanical_to_optical_left': 14,
|
|
1188
|
+
'mechanical_to_optical_right': 15,
|
|
1189
|
+
'space_lower': 9,
|
|
1190
|
+
'optical_to_mechanical_right': 0,
|
|
1191
|
+
'space_modulation_lower': 16,
|
|
1192
|
+
'squeezer': 17,
|
|
1193
|
+
'laser_frequency_modulation_lower': 18,
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
# the size of the component matrices
|
|
1198
|
+
MATRIX_SIZES = {
|
|
1199
|
+
'mirror': 4,
|
|
1200
|
+
'beamsplitter': 8,
|
|
1201
|
+
'nothing': 4,
|
|
1202
|
+
'directional_beamsplitter': 8,
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
# The indices of the ports in the component matrices, counter-clockwise,
|
|
1207
|
+
# alternating between input and output, starting at the left input
|
|
1208
|
+
PORT_DICTS = {
|
|
1209
|
+
'mirror': {
|
|
1210
|
+
'left': 0,
|
|
1211
|
+
'right': 2
|
|
1212
|
+
},
|
|
1213
|
+
'beamsplitter': {
|
|
1214
|
+
'left': 0,
|
|
1215
|
+
'right': 4,
|
|
1216
|
+
'bottom': 6,
|
|
1217
|
+
'top': 2
|
|
1218
|
+
},
|
|
1219
|
+
'nothing': {
|
|
1220
|
+
'left': 0,
|
|
1221
|
+
'right': 2
|
|
1222
|
+
},
|
|
1223
|
+
'directional_beamsplitter': {
|
|
1224
|
+
'left': 0,
|
|
1225
|
+
'right': 4,
|
|
1226
|
+
'bottom': 6,
|
|
1227
|
+
'top': 2
|
|
1228
|
+
}
|
|
1229
|
+
}
|