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/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
+ }