AOT-biomaps 2.1.3__py3-none-any.whl → 2.9.233__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.

Potentially problematic release.


This version of AOT-biomaps might be problematic. Click here for more details.

Files changed (50) hide show
  1. AOT_biomaps/AOT_Acoustic/AcousticEnums.py +64 -0
  2. AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
  3. AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
  4. AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
  5. AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
  6. AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
  7. AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
  8. AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
  9. AOT_biomaps/AOT_Experiment/Focus.py +55 -0
  10. AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
  11. AOT_biomaps/AOT_Experiment/__init__.py +9 -0
  12. AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
  13. AOT_biomaps/AOT_Optic/Absorber.py +24 -0
  14. AOT_biomaps/AOT_Optic/Laser.py +70 -0
  15. AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
  16. AOT_biomaps/AOT_Optic/__init__.py +10 -0
  17. AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
  18. AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
  19. AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
  20. AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
  21. AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
  22. AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
  23. AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
  24. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
  25. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
  26. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
  27. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
  28. AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
  29. AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
  30. AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
  31. AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
  32. AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
  33. AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
  34. AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
  35. AOT_biomaps/AOT_Recon/__init__.py +11 -0
  36. AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
  37. AOT_biomaps/Config.py +95 -0
  38. AOT_biomaps/Settings.py +45 -13
  39. AOT_biomaps/__init__.py +271 -18
  40. aot_biomaps-2.9.233.dist-info/METADATA +22 -0
  41. aot_biomaps-2.9.233.dist-info/RECORD +43 -0
  42. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
  43. AOT_biomaps/AOT_Acoustic.py +0 -1881
  44. AOT_biomaps/AOT_Experiment.py +0 -541
  45. AOT_biomaps/AOT_Optic.py +0 -219
  46. AOT_biomaps/AOT_Reconstruction.py +0 -1416
  47. AOT_biomaps/config.py +0 -54
  48. AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
  49. AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
  50. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,288 @@
1
+ from AOT_biomaps.Config import config
2
+ from AOT_biomaps.AOT_Experiment.Tomography import Tomography
3
+ from .ReconEnums import ReconType
4
+ from .ReconTools import mse, ssim
5
+
6
+ import os
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+ from abc import ABC, abstractmethod
10
+
11
+
12
+ class Recon(ABC):
13
+ def __init__(self, experiment, saveDir = None, isGPU = config.get_process() == 'gpu', isMultiGPU = True if config.numGPUs > 1 else False, isMultiCPU = True):
14
+ self.reconPhantom = None
15
+ self.reconLaser = None
16
+ self.experiment = experiment
17
+ self.reconType = None
18
+ self.saveDir = saveDir
19
+ self.MSE = None
20
+ self.SSIM = None
21
+ self.CRC = None
22
+
23
+ self.isGPU = isGPU
24
+ self.isMultiGPU = isMultiGPU
25
+ self.isMultiCPU = isMultiCPU
26
+
27
+ if str(type(self.experiment)) != str(Tomography):
28
+ raise TypeError(f"Experiment must be of type {Tomography}")
29
+
30
+ @abstractmethod
31
+ def run(self,withTumor = True):
32
+ pass
33
+
34
+ def save(self, withTumor=True, overwrite=False, date=None, show_logs=True):
35
+ """
36
+ Save the reconstruction results (reconPhantom is with tumor, reconLaser is without tumor) and indices of the saved recon results, in numpy format.
37
+
38
+ Args:
39
+ withTumor (bool): If True, saves reconPhantom. If False, saves reconLaser. Default is True.
40
+ overwrite (bool): If False, does not save if the file already exists. Default is False.
41
+
42
+ Warnings:
43
+ reconPhantom and reconLaser are lists of 2D numpy arrays, each array corresponding to one iteration.
44
+ """
45
+ isExisting, filepath = self.checkExistingFile(date=date)
46
+ if isExisting and not overwrite:
47
+ return
48
+
49
+ filename = 'reconPhantom.npy' if withTumor else 'reconLaser.npy'
50
+ filepathRecon = os.path.join(filepath, filename)
51
+
52
+ if withTumor:
53
+ if not self.reconPhantom or len(self.reconPhantom) == 0:
54
+ raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
55
+ np.save(filepathRecon, np.array(self.reconPhantom))
56
+ else:
57
+ if not self.reconLaser or len(self.reconLaser) == 0:
58
+ raise ValueError("Reconstructed laser is empty. Run reconstruction first.")
59
+ np.save(filepathRecon, np.array(self.reconLaser))
60
+
61
+ if self.indices is not None and len(self.indices) > 0:
62
+ filepathIndices = os.path.join(filepath, "indices.npy")
63
+ np.save(filepathIndices, np.array(self.indices))
64
+
65
+ if show_logs:
66
+ print(f"Reconstruction results saved to {os.path.dirname(filepath)}")
67
+
68
+ @abstractmethod
69
+ def checkExistingFile(self, date = None):
70
+ pass
71
+
72
+ def calculateCRC(self, use_ROI=True):
73
+ """
74
+ Computes the Contrast Recovery Coefficient (CRC) for all ROIs combined or globally.
75
+ For analytic reconstruction: returns a single CRC value.
76
+ For iterative reconstruction: returns a list of CRC values (one per iteration).
77
+ If iteration is specified, returns CRC for that specific iteration only.
78
+
79
+ :param iteration: Specific iteration index (optional). If None, computes for all iterations.
80
+ :param use_ROI: If True, computes CRC for all ROIs combined. If False, computes global CRC.
81
+ :return: CRC value or list of CRC values.
82
+ """
83
+ if self.reconType is None:
84
+ raise ValueError("Run reconstruction first")
85
+
86
+ if self.reconLaser is None or self.reconLaser == []:
87
+ raise ValueError("Reconstructed laser is empty. Run reconstruction first.")
88
+ if self.reconPhantom is None or self.reconPhantom == []:
89
+ raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
90
+
91
+ # Handle empty reconstructions
92
+ if self.reconLaser is None or self.reconLaser == []:
93
+ print("Reconstructed laser is empty. Running reconstruction without tumor...")
94
+ self.run(withTumor=False, isSavingEachIteration=True)
95
+
96
+ # Get the ROI mask(s) from the phantom if needed
97
+ if use_ROI:
98
+ self.experiment.OpticImage.find_ROI()
99
+ global_mask = np.logical_or.reduce(self.experiment.OpticImage.maskList)
100
+
101
+ # Analytic reconstruction case
102
+ if self.reconType is ReconType.Analytic:
103
+ if use_ROI:
104
+ recon_ratio = np.mean(self.reconPhantom[global_mask]) / np.mean(self.reconLaser[global_mask])
105
+ lambda_ratio = np.mean(self.experiment.OpticImage.phantom[global_mask]) / np.mean(self.experiment.OpticImage.laser.intensity[global_mask])
106
+ else:
107
+ recon_ratio = np.mean(self.reconPhantom) / np.mean(self.reconLaser)
108
+ lambda_ratio = np.mean(self.experiment.OpticImage.phantom) / np.mean(self.experiment.OpticImage.laser.intensity)
109
+
110
+ self.CRC =(recon_ratio - 1) / (lambda_ratio - 1)
111
+
112
+ # Iterative reconstruction case
113
+ else:
114
+ iterations = range(len(self.reconPhantom))
115
+
116
+ crc_list = []
117
+ for it in iterations:
118
+ if use_ROI:
119
+ recon_ratio = np.mean(self.reconPhantom[it][global_mask]) / np.mean(self.reconLaser[it][global_mask])
120
+ lambda_ratio = np.mean(self.experiment.OpticImage.phantom[global_mask]) / np.mean(self.experiment.OpticImage.laser.intensity[global_mask])
121
+ else:
122
+ recon_ratio = np.mean(self.reconPhantom[it]) / np.mean(self.reconLaser[it])
123
+ lambda_ratio = np.mean(self.experiment.OpticImage.phantom) / np.mean(self.experiment.OpticImage.laser.intensity)
124
+
125
+ crc_list.append((recon_ratio - 1) / (lambda_ratio - 1))
126
+
127
+ self.CRC = crc_list
128
+
129
+ def calculateMSE(self):
130
+ """
131
+ Calculate the Mean Squared Error (MSE) of the reconstruction.
132
+
133
+ Returns:
134
+ mse: float or list of floats, Mean Squared Error of the reconstruction
135
+ """
136
+
137
+ if self.reconPhantom is None or self.reconPhantom == []:
138
+ raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
139
+
140
+ if self.reconType in (ReconType.Analytic, ReconType.DeepLearning):
141
+ self.MSE = mse(self.experiment.OpticImage.phantom, self.reconPhantom)
142
+
143
+ elif self.reconType in (ReconType.Algebraic, ReconType.Bayesian, ReconType.Convex):
144
+ self.MSE = []
145
+ for theta in self.reconPhantom:
146
+ self.MSE.append(mse(self.experiment.OpticImage.phantom, theta))
147
+
148
+ def calculateSSIM(self):
149
+ """
150
+ Calculate the Structural Similarity Index (SSIM) of the reconstruction.
151
+
152
+ Returns:
153
+ ssim: float or list of floats, Structural Similarity Index of the reconstruction
154
+ """
155
+
156
+ if self.reconPhantom is None or self.reconPhantom == []:
157
+ raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
158
+
159
+ if self.reconType in (ReconType.Analytic, ReconType.DeepLearning):
160
+ data_range = self.reconPhantom.max() - self.reconPhantom.min()
161
+ self.SSIM = ssim(self.experiment.OpticImage.phantom, self.reconPhantom, data_range=data_range)
162
+
163
+ elif self.reconType in (ReconType.Algebraic, ReconType.Bayesian):
164
+ self.SSIM = []
165
+ for theta in self.reconPhantom:
166
+ data_range = theta.max() - theta.min()
167
+ ssim_value = ssim(self.experiment.OpticImage.phantom, theta, data_range=data_range)
168
+ self.SSIM.append(ssim_value)
169
+
170
+
171
+ def show(self, withTumor=True, savePath=None):
172
+ if withTumor:
173
+ if self.reconPhantom is None or self.reconPhantom == []:
174
+ raise ValueError("Reconstructed phantom with tumor is empty. Run reconstruction first.")
175
+ if isinstance(self.reconPhantom, list):
176
+ image = self.reconPhantom[-1]
177
+ else:
178
+ image = self.reconPhantom
179
+ if self.experiment.OpticImage is None:
180
+ fig, axs = plt.subplots(1, 1, figsize=(10, 10))
181
+ else:
182
+ fig, axs = plt.subplots(1, 2, figsize=(20, 10))
183
+ # Phantom original
184
+ im1 = axs[1].imshow(
185
+ self.experiment.OpticImage.phantom,
186
+ cmap='hot',
187
+ vmin=0,
188
+ vmax=1,
189
+ extent=(
190
+ self.experiment.params.general['Xrange'][0],
191
+ self.experiment.params.general['Xrange'][1],
192
+ self.experiment.params.general['Zrange'][1],
193
+ self.experiment.params.general['Zrange'][0]
194
+ ),
195
+ aspect='equal'
196
+ )
197
+ axs[1].set_title("Phantom with tumor")
198
+ axs[1].set_xlabel("x (mm)", fontsize=12)
199
+ axs[1].set_ylabel("z (mm)", fontsize=12)
200
+ axs[1].tick_params(axis='both', which='major', labelsize=8)
201
+ # Phantom reconstruit
202
+ im0 = axs[0].imshow(
203
+ image,
204
+ cmap='hot',
205
+ vmin=0,
206
+ vmax=1,
207
+ extent=(
208
+ self.experiment.params.general['Xrange'][0],
209
+ self.experiment.params.general['Xrange'][1],
210
+ self.experiment.params.general['Zrange'][1],
211
+ self.experiment.params.general['Zrange'][0]
212
+ ),
213
+ aspect='equal'
214
+ )
215
+ axs[0].set_title("Reconstructed phantom with tumor")
216
+ axs[0].set_xlabel("x (mm)", fontsize=12)
217
+ axs[0].set_ylabel("z (mm)", fontsize=12)
218
+ axs[0].tick_params(axis='both', which='major', labelsize=8)
219
+ axs[0].tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
220
+ else:
221
+ if self.reconLaser is None or self.reconLaser == []:
222
+ raise ValueError("Reconstructed laser without tumor is empty. Run reconstruction first.")
223
+ if isinstance(self.reconLaser, list):
224
+ image = self.reconLaser[-1]
225
+ else:
226
+ image = self.reconLaser
227
+ if self.experiment.OpticImage is None:
228
+ fig, axs = plt.subplots(1, 1, figsize=(10, 10))
229
+ else:
230
+ fig, axs = plt.subplots(1, 2, figsize=(20, 10))
231
+ # Laser original
232
+ im1 = axs[1].imshow(
233
+ self.experiment.OpticImage.laser.intensity,
234
+ cmap='hot',
235
+ vmin=0,
236
+ vmax=np.max(self.experiment.OpticImage.laser.intensity),
237
+ extent=(
238
+ self.experiment.params.general['Xrange'][0],
239
+ self.experiment.params.general['Xrange'][1],
240
+ self.experiment.params.general['Zrange'][1],
241
+ self.experiment.params.general['Zrange'][0]
242
+ ),
243
+ aspect='equal'
244
+ )
245
+ axs[1].set_title("Laser without tumor")
246
+ axs[1].set_xlabel("x (mm)", fontsize=12)
247
+ axs[1].set_ylabel("z (mm)", fontsize=12)
248
+ axs[1].tick_params(axis='both', which='major', labelsize=8)
249
+ # Laser reconstruit
250
+ im0 = axs[0].imshow(
251
+ image,
252
+ cmap='hot',
253
+ vmin=0,
254
+ vmax=np.max(self.experiment.OpticImage.laser.intensity),
255
+ extent=(
256
+ self.experiment.params.general['Xrange'][0],
257
+ self.experiment.params.general['Xrange'][1],
258
+ self.experiment.params.general['Zrange'][1],
259
+ self.experiment.params.general['Zrange'][0]
260
+ ),
261
+ aspect='equal'
262
+ )
263
+ axs[0].set_title("Reconstructed laser without tumor")
264
+ axs[0].set_xlabel("x (mm)", fontsize=12)
265
+ axs[0].set_ylabel("z (mm)", fontsize=12)
266
+ axs[0].tick_params(axis='both', which='major', labelsize=8)
267
+ axs[0].tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
268
+
269
+ # Colorbar commune
270
+ fig.subplots_adjust(bottom=0.2)
271
+ cbar_ax = fig.add_axes([0.25, 0.08, 0.5, 0.03])
272
+ cbar = fig.colorbar(im0, cax=cbar_ax, orientation='horizontal')
273
+ cbar.set_label('Normalized Intensity', fontsize=12)
274
+ cbar.ax.tick_params(labelsize=8)
275
+
276
+ plt.subplots_adjust(wspace=0.3)
277
+
278
+ if savePath is not None:
279
+ if not os.path.exists(savePath):
280
+ os.makedirs(savePath)
281
+ if withTumor:
282
+ plt.savefig(os.path.join(savePath, 'recon_with_tumor.png'), dpi=300, bbox_inches='tight')
283
+ else:
284
+ plt.savefig(os.path.join(savePath, 'recon_without_tumor.png'), dpi=300, bbox_inches='tight')
285
+
286
+ plt.show()
287
+
288
+
AOT_biomaps/Config.py ADDED
@@ -0,0 +1,95 @@
1
+ from pynvml import nvmlInit, nvmlDeviceGetCount, nvmlDeviceGetHandleByIndex, nvmlDeviceGetMemoryInfo, nvmlShutdown, NVMLError
2
+ import psutil
3
+
4
+ class Config:
5
+ _instance = None
6
+
7
+ def __new__(cls, *args, **kwargs):
8
+ if not cls._instance:
9
+ cls._instance = super(Config, cls).__new__(cls, *args, **kwargs)
10
+ return cls._instance
11
+
12
+ def __init__(self):
13
+ if not hasattr(self, 'initialized'):
14
+ self.initialized = True
15
+ self.numGPUs = 0
16
+ self.bestGPU = None
17
+ self.process = 'cpu' # Valeur par défaut
18
+ self.numCPUs = psutil.cpu_count(logical=False)
19
+ self.availableMemory = 100 - self.get_memory_usage()
20
+ self.batchSize = self.calculate_batch_size()
21
+ self._init_gpu()
22
+
23
+ def _init_gpu(self):
24
+ """Initialise les informations liées au GPU."""
25
+ try:
26
+ nvmlInit()
27
+ self.numGPUs = nvmlDeviceGetCount()
28
+ if self.numGPUs > 0:
29
+ self.process = 'gpu'
30
+ self.bestGPU = self.select_best_gpu()
31
+ else:
32
+ self.process = 'cpu'
33
+ self.bestGPU = None
34
+ except NVMLError as e:
35
+ print(f"NVIDIA GPU not available: {e}")
36
+ self.process = 'cpu'
37
+ self.bestGPU = None
38
+ self.numGPUs = 0
39
+ except Exception as e:
40
+ print(f"Unexpected error during GPU initialization: {e}")
41
+ self.process = 'cpu'
42
+ self.bestGPU = None
43
+ self.numGPUs = 0
44
+ finally:
45
+ try:
46
+ nvmlShutdown()
47
+ except:
48
+ pass # Évite les erreurs si nvmlShutdown est appelé plusieurs fois
49
+
50
+ def set_process(self, process):
51
+ """Définit le processus à utiliser ('cpu' ou 'gpu')."""
52
+ if process not in ['cpu', 'gpu']:
53
+ raise ValueError("process must be 'cpu' or 'gpu'")
54
+ self.process = process
55
+
56
+ def get_process(self):
57
+ """Retourne le processus actuel ('cpu' ou 'gpu')."""
58
+ return self.process
59
+
60
+ def select_best_gpu(self):
61
+ """Sélectionne le GPU avec le plus de mémoire disponible."""
62
+ try:
63
+ nvmlInit()
64
+ best_gpu = 0
65
+ max_memory = 0
66
+ for i in range(self.numGPUs):
67
+ handle = nvmlDeviceGetHandleByIndex(i)
68
+ mem_info = nvmlDeviceGetMemoryInfo(handle)
69
+ available_memory = mem_info.total - mem_info.used
70
+ if available_memory > max_memory:
71
+ max_memory = available_memory
72
+ best_gpu = i
73
+ return best_gpu
74
+ except NVMLError as e:
75
+ print(f"Failed to select GPU: {e}")
76
+ return 0 # Retourne le premier GPU par défaut en cas d'erreur
77
+ finally:
78
+ try:
79
+ nvmlShutdown()
80
+ except:
81
+ pass
82
+
83
+ def get_memory_usage(self):
84
+ """Retourne l'utilisation actuelle de la mémoire RAM (en pourcentage)."""
85
+ return psutil.virtual_memory().percent
86
+
87
+ def calculate_batch_size(self, max_memory_usage=90, min_batch_size=1, max_batch_size=20):
88
+ """Calcule dynamiquement la taille du batch en fonction de la mémoire disponible."""
89
+ if self.availableMemory > max_memory_usage:
90
+ return max_batch_size
91
+ else:
92
+ return max(min_batch_size, int((self.availableMemory / max_memory_usage) * max_batch_size))
93
+
94
+ # Initialisation unique de la configuration
95
+ config = Config()
AOT_biomaps/Settings.py CHANGED
@@ -1,39 +1,71 @@
1
1
  import yaml
2
2
 
3
- # Définir la classe Paramètre
4
3
  class Params:
5
4
  def __init__(self, path):
6
- config = self._initParams(path) # Appel correct de la méthode
7
-
5
+ config = self._initParams(path)
6
+ print(f"Configuration loaded from {path}")
8
7
  self.general = config.get('general', {})
9
8
  self.acoustic = config.get('acoustic', {})
10
9
  self.optic = config.get('optic', {})
11
10
  self.reconstruction = config.get('reconstruction', {})
12
11
 
13
12
  def __repr__(self):
14
- return (f"Params(general={self.general}, acoustic={self.acoustic}, optic={self.optic}), reconstruction={self.reconstruction})")
13
+ return (f"Params(general={self.general}, acoustic={self.acoustic}, optic={self.optic}, "
14
+ f"reconstruction={self.reconstruction})")
15
15
 
16
16
  def _initParams(self, path):
17
- """
18
- Initialize parameters from the YAML configuration file.
19
- """
20
17
  if not path.endswith('.yaml'):
21
18
  raise ValueError("The configuration file must be a YAML file with a .yaml extension.")
22
-
23
19
  try:
24
20
  with open(path, 'r') as file:
25
21
  config = yaml.safe_load(file)
26
22
  if config is None:
27
23
  raise ValueError("The configuration file is empty or not valid YAML.")
28
-
29
- # Vérifiez si 'Parameters' est la clé racine
30
24
  if 'Parameters' in config:
31
25
  config = config['Parameters']
32
-
33
- print("Configuration loaded:", config)
34
- return config # Retourne le dictionnaire config
26
+ return config
35
27
  except FileNotFoundError:
36
28
  raise FileNotFoundError(f"The file {path} does not exist.")
37
29
  except yaml.YAMLError as e:
38
30
  raise ValueError(f"Error parsing YAML file: {e}")
39
31
 
32
+ def show_Parameters(self):
33
+ config = {
34
+ 'general': self.general,
35
+ 'acoustic': self.acoustic,
36
+ 'optic': self.optic,
37
+ 'reconstruction': self.reconstruction
38
+ }
39
+ self._print_config(config)
40
+
41
+ def _print_config(self, config, indent=0):
42
+ border = "+" + "-" * 100 + "+"
43
+ print(border)
44
+ print(f"|{' Configuration Loaded '.center(100)}|")
45
+ print(border)
46
+ categories = {
47
+ 'General': config.get('general', {}),
48
+ 'Acoustic': config.get('acoustic', {}),
49
+ 'Optic': config.get('optic', {}),
50
+ 'Reconstruction': config.get('reconstruction', {})
51
+ }
52
+ for category, params in categories.items():
53
+ print("|" + category.center(100) + "|")
54
+ print(border)
55
+ self._print_params(params, indent + 2)
56
+ print(border)
57
+
58
+ def _print_params(self, params, indent):
59
+ if isinstance(params, dict):
60
+ for key, value in params.items():
61
+ if isinstance(value, (dict, list)):
62
+ print(f"|{' ' * indent}{key}:")
63
+ self._print_params(value, indent + 2)
64
+ else:
65
+ print(f"|{' ' * indent}{key}: {value}")
66
+ elif isinstance(params, list):
67
+ for item in params:
68
+ if isinstance(item, (dict, list)):
69
+ self._print_params(item, indent)
70
+ else:
71
+ print(f"|{' ' * indent}- {item}")