SuPyMode 2.4.3__cp313-cp313-win_amd64.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.
- SuPyMode/__init__.py +9 -0
- SuPyMode/_version.py +24 -0
- SuPyMode/boundaries.cp313-win_amd64.pyd +0 -0
- SuPyMode/directories.py +51 -0
- SuPyMode/eigensolver.cp313-win_amd64.pyd +0 -0
- SuPyMode/mesh.cp313-win_amd64.pyd +0 -0
- SuPyMode/model_parameters.cp313-win_amd64.pyd +0 -0
- SuPyMode/plotter.py +325 -0
- SuPyMode/propagation.py +202 -0
- SuPyMode/representation/__init__.py +23 -0
- SuPyMode/representation/adiabatic.py +111 -0
- SuPyMode/representation/beating_length.py +95 -0
- SuPyMode/representation/beta.py +78 -0
- SuPyMode/representation/eigen_value.py +79 -0
- SuPyMode/representation/field.py +486 -0
- SuPyMode/representation/index.py +80 -0
- SuPyMode/representation/normalized_coupling.py +98 -0
- SuPyMode/solver.py +203 -0
- SuPyMode/supermode.cp313-win_amd64.pyd +0 -0
- SuPyMode/superset.py +701 -0
- SuPyMode/superset_plots.py +497 -0
- SuPyMode/taper.cp313-win_amd64.pyd +0 -0
- SuPyMode/utils.py +196 -0
- SuPyMode/validation_data/SBB_figure_4_16_a/LP01-LP02.csv +98 -0
- SuPyMode/validation_data/SBB_figure_4_16_a/LP01-LP21.csv +88 -0
- SuPyMode/validation_data/SBB_figure_4_16_a/LP01-LP41.csv +86 -0
- SuPyMode/validation_data/SBB_figure_4_16_a/LP11-LP12.csv +82 -0
- SuPyMode/validation_data/SBB_figure_4_16_a/LP11-LP31.csv +95 -0
- SuPyMode/validation_data/SBB_figure_4_16_a/LP11-LP51.csv +81 -0
- SuPyMode/validation_data/SBB_figure_4_16_b/LP01-LP02.csv +285 -0
- SuPyMode/validation_data/SBB_figure_4_16_b/LP01-LP21.csv +75 -0
- SuPyMode/validation_data/SBB_figure_4_16_b/LP01-LP41.csv +237 -0
- SuPyMode/validation_data/SBB_figure_4_16_b/LP11-LP12.csv +126 -0
- SuPyMode/validation_data/SBB_figure_4_16_b/LP11-LP31.csv +156 -0
- SuPyMode/validation_data/SBB_figure_4_16_b/LP11-LP51.csv +224 -0
- SuPyMode/workflow.py +253 -0
- supymode-2.4.3.dist-info/METADATA +241 -0
- supymode-2.4.3.dist-info/RECORD +40 -0
- supymode-2.4.3.dist-info/WHEEL +5 -0
- supymode-2.4.3.dist-info/licenses/LICENSE +21 -0
SuPyMode/superset.py
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
# Built-in imports
|
|
5
|
+
import logging
|
|
6
|
+
import pickle
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import combinations, product
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
import numpy
|
|
13
|
+
|
|
14
|
+
# Third-party imports
|
|
15
|
+
from scipy.integrate import solve_ivp
|
|
16
|
+
from scipy.interpolate import interp1d
|
|
17
|
+
|
|
18
|
+
# Local imports
|
|
19
|
+
from SuPyMode.model_parameters import ModelParameters
|
|
20
|
+
from SuPyMode.supermode import SUPERMODE
|
|
21
|
+
|
|
22
|
+
from SuPyMode.taper import AlphaProfile
|
|
23
|
+
from SuPyMode.propagation import Propagation
|
|
24
|
+
from SuPyMode.superset_plots import SuperSetPlots
|
|
25
|
+
from SuPyMode.utils import parse_filename
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SuperSet(SuperSetPlots):
|
|
30
|
+
"""
|
|
31
|
+
A set of supermodes calculated for a specific optical fiber configuration.
|
|
32
|
+
|
|
33
|
+
This class manages operations on supermodes, including sorting, plotting, and calculations
|
|
34
|
+
related to fiber optics simulations.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
model_parameters : ModelParameters
|
|
39
|
+
Parameters defining the model for simulation.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
model_parameters: ModelParameters
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
self._transmission_matrix = None
|
|
46
|
+
self.supermodes = []
|
|
47
|
+
itr_list = self.model_parameters.itr_list
|
|
48
|
+
self._itr_to_slice = interp1d(
|
|
49
|
+
itr_list,
|
|
50
|
+
numpy.arange(itr_list.size),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __getitem__(self, idx: int) -> SUPERMODE:
|
|
54
|
+
return self.supermodes[idx]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def fundamental_supermodes(self) -> list[SUPERMODE]:
|
|
58
|
+
"""
|
|
59
|
+
Returns the fundamental supermodes based on the highest beta values and minimal spatial overlap.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
list of SUPERMODE
|
|
64
|
+
A list of fundamental supermodes.
|
|
65
|
+
"""
|
|
66
|
+
return self.get_fundamental_supermodes(tolerance=1e-2)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def non_fundamental_supermodes(self) -> list[SUPERMODE]:
|
|
70
|
+
"""
|
|
71
|
+
Returns the non-fundamental supermodes based on the specified spatial overlap tolerance.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
list of SUPERMODE
|
|
76
|
+
A list of non-fundamental supermodes.
|
|
77
|
+
"""
|
|
78
|
+
return self.get_non_fundamental_supermodes(tolerance=1e-2)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def transmission_matrix(self) -> numpy.ndarray:
|
|
82
|
+
"""
|
|
83
|
+
Returns the supermode transmission matrix.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
numpy.ndarray
|
|
88
|
+
The transmission matrix for the supermodes.
|
|
89
|
+
"""
|
|
90
|
+
if self._transmission_matrix is None:
|
|
91
|
+
self.compute_transmission_matrix()
|
|
92
|
+
|
|
93
|
+
return self._transmission_matrix
|
|
94
|
+
|
|
95
|
+
def itr_to_slice(self, itr_list: list[float]) -> list[int]:
|
|
96
|
+
"""
|
|
97
|
+
Convert ITR values to corresponding slice numbers.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
itr_list : list of float
|
|
102
|
+
Inverse taper ratio values.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
list of int
|
|
107
|
+
List of slice numbers corresponding to the ITR values.
|
|
108
|
+
"""
|
|
109
|
+
itr_list = numpy.asarray(itr_list)
|
|
110
|
+
|
|
111
|
+
return numpy.floor(self._itr_to_slice(itr_list)).astype(int)
|
|
112
|
+
|
|
113
|
+
def get_fundamental_supermodes(self, *, tolerance: float = 0.1) -> list[SUPERMODE]:
|
|
114
|
+
"""
|
|
115
|
+
Returns a list of fundamental supermodes with the highest propagation constant values and minimal spatial overlap.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
tolerance : float, optional
|
|
120
|
+
Tolerance for spatial overlap (default is 0.1).
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
list of SUPERMODE
|
|
125
|
+
List of fundamental supermodes.
|
|
126
|
+
"""
|
|
127
|
+
self.sort_modes_by_beta()
|
|
128
|
+
|
|
129
|
+
fundamental_supermodes = [self.supermodes[0]]
|
|
130
|
+
|
|
131
|
+
def absolute_overlap(mode_0: SUPERMODE, mode_1: SUPERMODE) -> float:
|
|
132
|
+
field_0 = numpy.abs(mode_0.field.data[0])
|
|
133
|
+
norm_0 = field_0.sum()
|
|
134
|
+
field_0 /= numpy.sqrt(norm_0)
|
|
135
|
+
|
|
136
|
+
field_1 = numpy.abs(mode_1.field.data[0])
|
|
137
|
+
norm_1 = field_1.sum()
|
|
138
|
+
field_1 /= numpy.sqrt(norm_1)
|
|
139
|
+
|
|
140
|
+
overlap = numpy.sum(field_0 * field_1)
|
|
141
|
+
|
|
142
|
+
return overlap
|
|
143
|
+
|
|
144
|
+
for mode_0 in self.supermodes:
|
|
145
|
+
abs_overlap = [
|
|
146
|
+
absolute_overlap(mode_0, mode_1) for mode_1 in fundamental_supermodes
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
abs_overlaps = numpy.asarray(abs_overlap)
|
|
150
|
+
|
|
151
|
+
if numpy.any(abs_overlaps > tolerance):
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
fundamental_supermodes.append(mode_0)
|
|
155
|
+
|
|
156
|
+
return fundamental_supermodes
|
|
157
|
+
|
|
158
|
+
def get_non_fundamental_supermodes(
|
|
159
|
+
self, *, tolerance: float = 0.1
|
|
160
|
+
) -> list[SUPERMODE]:
|
|
161
|
+
"""
|
|
162
|
+
Returns a list of non-fundamental supermodes that do not overlap with the fundamental modes.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
tolerance : float, optional
|
|
167
|
+
Tolerance for spatial overlap (default is 0.1).
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
list of SUPERMODE
|
|
172
|
+
List of non-fundamental supermodes.
|
|
173
|
+
"""
|
|
174
|
+
non_fundamental_supermodes = self.supermodes
|
|
175
|
+
|
|
176
|
+
for supermodes in self.get_fundamental_supermodes(tolerance=tolerance):
|
|
177
|
+
non_fundamental_supermodes.remove(supermodes)
|
|
178
|
+
|
|
179
|
+
return non_fundamental_supermodes
|
|
180
|
+
|
|
181
|
+
def get_mode_solver_classification(self) -> list[list[SUPERMODE]]:
|
|
182
|
+
"""
|
|
183
|
+
Returns a list of modes classified by solver number.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
list of list of SUPERMODE
|
|
188
|
+
List of lists containing modes classified by solver number.
|
|
189
|
+
"""
|
|
190
|
+
solver_numbers = [mode.solver_number for mode in self]
|
|
191
|
+
|
|
192
|
+
number_of_solvers = len(set(solver_numbers))
|
|
193
|
+
|
|
194
|
+
mode_solver_array = [[] for i in range(number_of_solvers)]
|
|
195
|
+
|
|
196
|
+
for mode in self:
|
|
197
|
+
mode_solver_array[mode.solver_number].append(mode)
|
|
198
|
+
|
|
199
|
+
return mode_solver_array
|
|
200
|
+
|
|
201
|
+
def label_supermodes(self, *label_list) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Assign labels to the supermodes.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
label_list : tuple
|
|
208
|
+
Labels to assign to the supermodes.
|
|
209
|
+
"""
|
|
210
|
+
for n, label in enumerate(label_list):
|
|
211
|
+
self[n].label = label
|
|
212
|
+
|
|
213
|
+
setattr(self, label, self[n])
|
|
214
|
+
|
|
215
|
+
def reset_labels(self) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Reset labels for all supermodes to default values.
|
|
218
|
+
"""
|
|
219
|
+
for n, super_mode in enumerate(self.supermodes):
|
|
220
|
+
super_mode.label = f"mode_{n}"
|
|
221
|
+
|
|
222
|
+
def compute_transmission_matrix(self) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Calculate the transmission matrix including only the propagation constant.
|
|
225
|
+
"""
|
|
226
|
+
shape = [
|
|
227
|
+
len(self.supermodes),
|
|
228
|
+
len(self.supermodes),
|
|
229
|
+
len(self.model_parameters.itr_list),
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
self._transmission_matrix = numpy.zeros(shape)
|
|
233
|
+
|
|
234
|
+
for mode in self.supermodes:
|
|
235
|
+
self._transmission_matrix[mode.mode_number, mode.mode_number, :] = (
|
|
236
|
+
mode.beta.data * 2.0 * numpy.pi
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def add_coupling_to_t_matrix(
|
|
240
|
+
self, *, t_matrix: numpy.ndarray, adiabatic_factor: numpy.ndarray
|
|
241
|
+
) -> numpy.ndarray:
|
|
242
|
+
"""
|
|
243
|
+
Add coupling coefficients to the transmission matrix.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
t_matrix : numpy.ndarray
|
|
248
|
+
Transmission matrix to which coupling values are added.
|
|
249
|
+
adiabatic_factor : numpy.ndarray
|
|
250
|
+
Adiabatic factor, set to one if None (normalized coupling).
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
numpy.ndarray
|
|
255
|
+
Updated transmission matrix with coupling values.
|
|
256
|
+
"""
|
|
257
|
+
size = t_matrix.shape[-1]
|
|
258
|
+
|
|
259
|
+
t_matrix = t_matrix.astype(complex)
|
|
260
|
+
|
|
261
|
+
for mode_0, mode_1 in combinations(self.supermodes, 2):
|
|
262
|
+
coupling = mode_0.normalized_coupling.get_values(mode_1)[:size]
|
|
263
|
+
|
|
264
|
+
coupling *= adiabatic_factor
|
|
265
|
+
|
|
266
|
+
t_matrix[mode_0.mode_number, mode_1.mode_number, :] = -coupling
|
|
267
|
+
t_matrix[mode_1.mode_number, mode_0.mode_number, :] = +coupling
|
|
268
|
+
|
|
269
|
+
if numpy.isnan(t_matrix).any():
|
|
270
|
+
raise ValueError("Nan values detected in transmission matrix.")
|
|
271
|
+
if numpy.isinf(t_matrix).any():
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"Inf values detected in transmission matrix, verify that there is no hybrid mode in the computation."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return t_matrix
|
|
277
|
+
|
|
278
|
+
def compute_coupling_factor(self, *, coupler_length: float) -> numpy.ndarray:
|
|
279
|
+
"""
|
|
280
|
+
Compute the coupling factor defined by the derivative of the inverse taper ratio.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
coupler_length : float
|
|
285
|
+
Length of the coupler.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
numpy.ndarray
|
|
290
|
+
Coupling factor as a function of distance in the coupler.
|
|
291
|
+
"""
|
|
292
|
+
dx = coupler_length / (self.model_parameters.n_slice)
|
|
293
|
+
|
|
294
|
+
ditr = numpy.gradient(numpy.log(self.model_parameters.itr_list), axis=0)
|
|
295
|
+
|
|
296
|
+
return ditr / dx
|
|
297
|
+
|
|
298
|
+
def get_transmision_matrix_from_profile(
|
|
299
|
+
self, *, profile: AlphaProfile, add_coupling: bool = True
|
|
300
|
+
) -> tuple:
|
|
301
|
+
"""
|
|
302
|
+
Get the transmission matrix from the given profile.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
profile : AlphaProfile
|
|
307
|
+
Z-profile of the coupler.
|
|
308
|
+
add_coupling : bool, optional
|
|
309
|
+
Whether to add coupling to the transmission matrix (default is True).
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
tuple
|
|
314
|
+
Tuple containing distance, ITR vector, and transmission matrix.
|
|
315
|
+
"""
|
|
316
|
+
profile.initialize()
|
|
317
|
+
|
|
318
|
+
final_slice = self.itr_to_slice(itr_list=profile.smallest_itr)
|
|
319
|
+
|
|
320
|
+
sub_t_matrix = self.transmission_matrix[..., :final_slice]
|
|
321
|
+
|
|
322
|
+
sub_itr_vector = self.model_parameters.itr_list[:final_slice]
|
|
323
|
+
|
|
324
|
+
if add_coupling:
|
|
325
|
+
sub_t_matrix = self.add_coupling_to_t_matrix(
|
|
326
|
+
t_matrix=sub_t_matrix,
|
|
327
|
+
adiabatic_factor=profile.evaluate_adiabatic_factor(itr=sub_itr_vector),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
sub_distance = profile.evaluate_distance_vs_itr(sub_itr_vector)
|
|
331
|
+
|
|
332
|
+
return sub_distance, sub_itr_vector, sub_t_matrix
|
|
333
|
+
|
|
334
|
+
def propagate(
|
|
335
|
+
self,
|
|
336
|
+
*,
|
|
337
|
+
profile: AlphaProfile,
|
|
338
|
+
initial_amplitude: list,
|
|
339
|
+
max_step: Optional[float] = None,
|
|
340
|
+
add_coupling: bool = True,
|
|
341
|
+
method: str = "RK45",
|
|
342
|
+
**kwargs: dict,
|
|
343
|
+
) -> Propagation:
|
|
344
|
+
"""
|
|
345
|
+
Propagate the amplitudes of the supermodes in a coupler based on the given profile.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
profile : AlphaProfile
|
|
350
|
+
The z-profile of the coupler.
|
|
351
|
+
initial_amplitude : list
|
|
352
|
+
The initial amplitude as a list of complex numbers.
|
|
353
|
+
max_step : float, optional
|
|
354
|
+
The maximum step size used by the solver (default is None).
|
|
355
|
+
add_coupling : bool, optional
|
|
356
|
+
Whether to add coupling to the transmission matrix (default is True).
|
|
357
|
+
method : str, optional
|
|
358
|
+
Integration method to be used by the solver (default is 'RK45').
|
|
359
|
+
**kwargs : dict
|
|
360
|
+
Additional keyword arguments to be passed to the solver.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
Propagation
|
|
365
|
+
A Propagation object containing the results of the propagation.
|
|
366
|
+
"""
|
|
367
|
+
initial_amplitude = numpy.asarray(initial_amplitude, dtype=complex)
|
|
368
|
+
|
|
369
|
+
if max_step is None:
|
|
370
|
+
max_step = self.model_parameters.wavelength / 200
|
|
371
|
+
|
|
372
|
+
sub_distance, sub_itr_vector, sub_t_matrix = (
|
|
373
|
+
self.get_transmision_matrix_from_profile(
|
|
374
|
+
profile=profile, add_coupling=add_coupling
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
z_to_itr = interp1d(
|
|
379
|
+
profile.distance,
|
|
380
|
+
profile.itr_list,
|
|
381
|
+
bounds_error=False,
|
|
382
|
+
fill_value="extrapolate",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
itr_to_t_matrix = interp1d(
|
|
386
|
+
sub_itr_vector, sub_t_matrix, bounds_error=False, fill_value="extrapolate"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def model(z, y):
|
|
390
|
+
itr = z_to_itr(z)
|
|
391
|
+
return 1j * itr_to_t_matrix(itr) @ y
|
|
392
|
+
|
|
393
|
+
sol = solve_ivp(
|
|
394
|
+
fun=model,
|
|
395
|
+
y0=initial_amplitude,
|
|
396
|
+
t_span=[0, profile.total_length],
|
|
397
|
+
method=method,
|
|
398
|
+
vectorized=True,
|
|
399
|
+
max_step=max_step,
|
|
400
|
+
**kwargs,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Check power conservation across the propagation
|
|
404
|
+
norm = numpy.sum(numpy.abs(sol.y) ** 2, axis=0)
|
|
405
|
+
if not numpy.allclose(norm, 1.0, atol=1e-1):
|
|
406
|
+
logging.warning(
|
|
407
|
+
f"Power conservation not achieved [{max_step = }, atol = 1e-1]."
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return Propagation(
|
|
411
|
+
superset=self,
|
|
412
|
+
amplitudes=sol.y,
|
|
413
|
+
distance=sol.t,
|
|
414
|
+
profile=profile,
|
|
415
|
+
z_to_itr=z_to_itr,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def interpret_initial_input(
|
|
419
|
+
self, initial_amplitude: list | SUPERMODE
|
|
420
|
+
) -> numpy.ndarray:
|
|
421
|
+
"""
|
|
422
|
+
Interpret the initial amplitude input to ensure compatibility with the expected number of supermodes.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
initial_amplitude : list or SUPERMODE
|
|
427
|
+
The initial amplitude as either a list of complex numbers or a SUPERMODE object.
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
numpy.ndarray
|
|
432
|
+
The initial amplitudes as a NumPy array of complex numbers.
|
|
433
|
+
|
|
434
|
+
Raises
|
|
435
|
+
------
|
|
436
|
+
ValueError
|
|
437
|
+
If the length of the initial amplitude list does not match the number of supermodes.
|
|
438
|
+
"""
|
|
439
|
+
if isinstance(initial_amplitude, SUPERMODE):
|
|
440
|
+
amplitudes = initial_amplitude.amplitudes
|
|
441
|
+
else:
|
|
442
|
+
amplitudes = initial_amplitude
|
|
443
|
+
|
|
444
|
+
amplitude_size = len(amplitudes)
|
|
445
|
+
number_of_supermodes = len(self.supermodes)
|
|
446
|
+
|
|
447
|
+
if amplitude_size != number_of_supermodes:
|
|
448
|
+
raise ValueError(
|
|
449
|
+
f"Amplitudes size: {amplitude_size} does not match with the number of supermodes: {number_of_supermodes}"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return numpy.asarray(amplitudes, dtype=complex)
|
|
453
|
+
|
|
454
|
+
def _sort_modes(self, ordering_keys) -> List[SUPERMODE]:
|
|
455
|
+
"""
|
|
456
|
+
Sort supermodes using specified keys provided as tuples in `ordering_keys`.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
ordering_keys : tuple
|
|
461
|
+
Tuple containing keys to sort by.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
list of SUPERMODE
|
|
466
|
+
Sorted list of supermodes.
|
|
467
|
+
"""
|
|
468
|
+
order = numpy.lexsort(ordering_keys)
|
|
469
|
+
sorted_supermodes = [self.supermodes[idx] for idx in order]
|
|
470
|
+
for i, supermode in enumerate(sorted_supermodes):
|
|
471
|
+
supermode.mode_number = i
|
|
472
|
+
return sorted_supermodes
|
|
473
|
+
|
|
474
|
+
def sort_modes_by_beta(self) -> None:
|
|
475
|
+
"""
|
|
476
|
+
Sort supermodes in descending order of their propagation constants (beta).
|
|
477
|
+
"""
|
|
478
|
+
lexort_index = ([-mode.get_betas()[-1] for mode in self.supermodes],)
|
|
479
|
+
|
|
480
|
+
print(lexort_index)
|
|
481
|
+
|
|
482
|
+
self.all_supermodes = self._sort_modes(lexort_index)
|
|
483
|
+
|
|
484
|
+
def sort_modes(
|
|
485
|
+
self, sorting_method: str = "beta", keep_only: Optional[int] = None
|
|
486
|
+
) -> None:
|
|
487
|
+
"""
|
|
488
|
+
Sort supermodes according to the specified method, optionally limiting the number of modes retained.
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
sorting_method : str, optional
|
|
493
|
+
Sorting method to use, either "beta" or "symmetry+beta" (default is "beta").
|
|
494
|
+
keep_only : int, optional
|
|
495
|
+
Number of supermodes to retain after sorting (default is None).
|
|
496
|
+
|
|
497
|
+
Raises
|
|
498
|
+
------
|
|
499
|
+
ValueError
|
|
500
|
+
If an unrecognized sorting method is provided.
|
|
501
|
+
"""
|
|
502
|
+
match sorting_method.lower():
|
|
503
|
+
case "beta":
|
|
504
|
+
self.sort_modes_by_beta()
|
|
505
|
+
case "symmetry+beta":
|
|
506
|
+
self.sort_modes_by_solver_and_beta()
|
|
507
|
+
case _:
|
|
508
|
+
raise ValueError(
|
|
509
|
+
f"Unrecognized sorting method: {sorting_method}, accepted values are ['beta', 'symmetry+beta']"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
self.supermodes = (
|
|
513
|
+
self.all_supermodes[:keep_only]
|
|
514
|
+
if keep_only is not None
|
|
515
|
+
else self.all_supermodes
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
def sort_modes_by_solver_and_beta(self) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Sort supermodes primarily by solver number and secondarily by descending propagation constant (beta).
|
|
521
|
+
"""
|
|
522
|
+
lexort_index = (
|
|
523
|
+
[mode.solver_number for mode in self.supermodes],
|
|
524
|
+
[-mode.beta.data[-1] for mode in self.supermodes],
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
self.all_supermodes = self._sort_modes(lexort_index)
|
|
528
|
+
|
|
529
|
+
def is_compute_compatible(self, pair_of_mode: tuple) -> bool:
|
|
530
|
+
"""
|
|
531
|
+
Determine whether the specified pair of modes is compatible for computation.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
pair_of_mode : tuple
|
|
536
|
+
The pair of modes to be checked.
|
|
537
|
+
|
|
538
|
+
Returns
|
|
539
|
+
-------
|
|
540
|
+
bool
|
|
541
|
+
True if the pair of modes is compatible for computation, False otherwise.
|
|
542
|
+
"""
|
|
543
|
+
mode_0, mode_1 = pair_of_mode
|
|
544
|
+
return mode_0.is_computation_compatible(mode_1)
|
|
545
|
+
|
|
546
|
+
def remove_duplicate_combination(self, supermodes_list: list) -> list[SUPERMODE]:
|
|
547
|
+
"""
|
|
548
|
+
Remove duplicate combinations from the mode combination list irrespective of the order.
|
|
549
|
+
|
|
550
|
+
Parameters
|
|
551
|
+
----------
|
|
552
|
+
supermodes_list : list of tuple
|
|
553
|
+
List of mode combinations.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
list of tuple
|
|
558
|
+
Reduced list of unique supermode combinations.
|
|
559
|
+
"""
|
|
560
|
+
output_list = []
|
|
561
|
+
|
|
562
|
+
for mode0, mode1 in supermodes_list:
|
|
563
|
+
if (mode0, mode1) not in output_list and (mode1, mode0) not in output_list:
|
|
564
|
+
output_list.append((mode0, mode1))
|
|
565
|
+
|
|
566
|
+
return output_list
|
|
567
|
+
|
|
568
|
+
def interpret_combination(self, mode_of_interest: list, combination: str) -> set:
|
|
569
|
+
"""
|
|
570
|
+
Interpret user input for mode selection and return the combination of modes to consider.
|
|
571
|
+
|
|
572
|
+
Parameters
|
|
573
|
+
----------
|
|
574
|
+
mode_of_interest : list
|
|
575
|
+
List of modes of interest.
|
|
576
|
+
combination : str
|
|
577
|
+
Mode selection method ('pairs' or 'specific').
|
|
578
|
+
|
|
579
|
+
Returns
|
|
580
|
+
-------
|
|
581
|
+
set of tuple
|
|
582
|
+
Set of mode combinations.
|
|
583
|
+
"""
|
|
584
|
+
self.test_valid_input(
|
|
585
|
+
variable_name="combination",
|
|
586
|
+
user_input=combination,
|
|
587
|
+
valid_inputs=["pairs", "specific"],
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
match combination:
|
|
591
|
+
case "pairs":
|
|
592
|
+
mode_combinations = product(mode_of_interest, mode_of_interest)
|
|
593
|
+
case "specific":
|
|
594
|
+
mode_combinations = product(mode_of_interest, self.supermodes)
|
|
595
|
+
|
|
596
|
+
mode_combinations = filter(self.is_compute_compatible, mode_combinations)
|
|
597
|
+
|
|
598
|
+
mode_combinations = self.remove_duplicate_combination(mode_combinations)
|
|
599
|
+
|
|
600
|
+
return set(mode_combinations)
|
|
601
|
+
|
|
602
|
+
@parse_filename
|
|
603
|
+
def save_instance(self, filename: str) -> Path:
|
|
604
|
+
"""
|
|
605
|
+
Save the SuperSet instance as a serialized pickle file.
|
|
606
|
+
|
|
607
|
+
Parameters
|
|
608
|
+
----------
|
|
609
|
+
filename : str
|
|
610
|
+
Filename for the serialized instance.
|
|
611
|
+
|
|
612
|
+
Returns
|
|
613
|
+
-------
|
|
614
|
+
Path
|
|
615
|
+
The path to the saved instance file.
|
|
616
|
+
"""
|
|
617
|
+
with open(filename.with_suffix(".pickle"), "wb") as output_file:
|
|
618
|
+
pickle.dump(self, output_file, pickle.HIGHEST_PROTOCOL)
|
|
619
|
+
|
|
620
|
+
return filename
|
|
621
|
+
|
|
622
|
+
@parse_filename
|
|
623
|
+
def export_data(
|
|
624
|
+
self,
|
|
625
|
+
filename: str,
|
|
626
|
+
mode_of_interest: list[SUPERMODE] | str = "all",
|
|
627
|
+
combination: list | str = "pairs",
|
|
628
|
+
) -> Path:
|
|
629
|
+
"""
|
|
630
|
+
Export the SuperSet data as CSV files, saving specific attributes of the modes or combinations of modes.
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
filename : str
|
|
635
|
+
The directory where the files will be saved.
|
|
636
|
+
mode_of_interest : list, optional
|
|
637
|
+
List of modes to be exported (default is 'all').
|
|
638
|
+
combination : list, optional
|
|
639
|
+
List of mode combinations to be exported (default is None).
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
Path
|
|
644
|
+
The path to the directory where the files were saved.
|
|
645
|
+
"""
|
|
646
|
+
mode_of_interest = self.interpret_mode_of_interest(
|
|
647
|
+
superset=self, mode_of_interest=mode_of_interest
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
combination = self.interpret_combination(
|
|
651
|
+
mode_of_interest=mode_of_interest, combination=combination
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# Create the directory if it doesn't exist
|
|
655
|
+
output_dir = Path(filename)
|
|
656
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
657
|
+
|
|
658
|
+
def _export_single_data(attribute_name: str):
|
|
659
|
+
"""Helper function to export data for a single attribute for all modes of interest."""
|
|
660
|
+
for mode in mode_of_interest:
|
|
661
|
+
data = getattr(mode, attribute_name).data
|
|
662
|
+
sub_filename = output_dir / f"{attribute_name}_{mode.label}".replace(
|
|
663
|
+
"}", ""
|
|
664
|
+
).replace("{", "")
|
|
665
|
+
data = numpy.vstack([self.model_parameters.itr_list, data])
|
|
666
|
+
numpy.savetxt(
|
|
667
|
+
fname=sub_filename.with_suffix(".csv"),
|
|
668
|
+
X=data,
|
|
669
|
+
delimiter=",",
|
|
670
|
+
header=f"ITR, {attribute_name}",
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def _export_combination_data(attribute_name: str):
|
|
674
|
+
"""Helper function to export data for a single attribute for all combinations of modes."""
|
|
675
|
+
for mode_0, mode_1 in combination:
|
|
676
|
+
data = getattr(mode_0, attribute_name).get_values(
|
|
677
|
+
other_supermode=mode_1
|
|
678
|
+
)
|
|
679
|
+
sub_filename = (
|
|
680
|
+
output_dir
|
|
681
|
+
/ f"{attribute_name}_{mode_0.label}_{mode_1.label}".replace(
|
|
682
|
+
"}", ""
|
|
683
|
+
).replace("{", "")
|
|
684
|
+
)
|
|
685
|
+
data = numpy.vstack([self.model_parameters.itr_list, data])
|
|
686
|
+
numpy.savetxt(
|
|
687
|
+
fname=sub_filename.with_suffix(".csv"),
|
|
688
|
+
X=data,
|
|
689
|
+
delimiter=",",
|
|
690
|
+
header=f"ITR, {attribute_name}",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# Export single-mode attributes
|
|
694
|
+
_export_single_data("index")
|
|
695
|
+
_export_single_data("beta")
|
|
696
|
+
_export_single_data("eigenvalue")
|
|
697
|
+
|
|
698
|
+
# Export combination-mode attributes
|
|
699
|
+
_export_combination_data("adiabatic")
|
|
700
|
+
_export_combination_data("beating_length")
|
|
701
|
+
_export_combination_data("normalized_coupling")
|