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.
Files changed (40) hide show
  1. SuPyMode/__init__.py +9 -0
  2. SuPyMode/_version.py +24 -0
  3. SuPyMode/boundaries.cp313-win_amd64.pyd +0 -0
  4. SuPyMode/directories.py +51 -0
  5. SuPyMode/eigensolver.cp313-win_amd64.pyd +0 -0
  6. SuPyMode/mesh.cp313-win_amd64.pyd +0 -0
  7. SuPyMode/model_parameters.cp313-win_amd64.pyd +0 -0
  8. SuPyMode/plotter.py +325 -0
  9. SuPyMode/propagation.py +202 -0
  10. SuPyMode/representation/__init__.py +23 -0
  11. SuPyMode/representation/adiabatic.py +111 -0
  12. SuPyMode/representation/beating_length.py +95 -0
  13. SuPyMode/representation/beta.py +78 -0
  14. SuPyMode/representation/eigen_value.py +79 -0
  15. SuPyMode/representation/field.py +486 -0
  16. SuPyMode/representation/index.py +80 -0
  17. SuPyMode/representation/normalized_coupling.py +98 -0
  18. SuPyMode/solver.py +203 -0
  19. SuPyMode/supermode.cp313-win_amd64.pyd +0 -0
  20. SuPyMode/superset.py +701 -0
  21. SuPyMode/superset_plots.py +497 -0
  22. SuPyMode/taper.cp313-win_amd64.pyd +0 -0
  23. SuPyMode/utils.py +196 -0
  24. SuPyMode/validation_data/SBB_figure_4_16_a/LP01-LP02.csv +98 -0
  25. SuPyMode/validation_data/SBB_figure_4_16_a/LP01-LP21.csv +88 -0
  26. SuPyMode/validation_data/SBB_figure_4_16_a/LP01-LP41.csv +86 -0
  27. SuPyMode/validation_data/SBB_figure_4_16_a/LP11-LP12.csv +82 -0
  28. SuPyMode/validation_data/SBB_figure_4_16_a/LP11-LP31.csv +95 -0
  29. SuPyMode/validation_data/SBB_figure_4_16_a/LP11-LP51.csv +81 -0
  30. SuPyMode/validation_data/SBB_figure_4_16_b/LP01-LP02.csv +285 -0
  31. SuPyMode/validation_data/SBB_figure_4_16_b/LP01-LP21.csv +75 -0
  32. SuPyMode/validation_data/SBB_figure_4_16_b/LP01-LP41.csv +237 -0
  33. SuPyMode/validation_data/SBB_figure_4_16_b/LP11-LP12.csv +126 -0
  34. SuPyMode/validation_data/SBB_figure_4_16_b/LP11-LP31.csv +156 -0
  35. SuPyMode/validation_data/SBB_figure_4_16_b/LP11-LP51.csv +224 -0
  36. SuPyMode/workflow.py +253 -0
  37. supymode-2.4.3.dist-info/METADATA +241 -0
  38. supymode-2.4.3.dist-info/RECORD +40 -0
  39. supymode-2.4.3.dist-info/WHEEL +5 -0
  40. 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")