modulation-da 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.
@@ -0,0 +1,638 @@
1
+ import numpy as np
2
+ import tensorflow as tf
3
+ import matplotlib.pyplot as plt
4
+ import os
5
+ from time import time
6
+ import scipy.optimize
7
+
8
+ # Configuration TensorFlow
9
+ tf.random.set_seed(1234)
10
+ np.random.seed(1234)
11
+
12
+ class QuantumPINNSolver:
13
+ def __init__(self):
14
+ # Paramètres physiques
15
+ self.e_charge = 1.602176634e-19
16
+ self.hbar = 6.582119514e-16 # eV·s
17
+ self.KB = 8.6173303e-5 # eV/K
18
+ self.pi = np.pi
19
+
20
+ # Paramètres du système
21
+ self.n = 3 # nombre de modes
22
+ self.Ne = 20 # discrétisation en énergie
23
+ self.N = 15000 #15000 # points de collocation
24
+ self.N_0 = 2000 # points de condition initiale
25
+ self.N_pred = 5000 # points de prédiction
26
+ self.N_train_adam = 90000 # époques d'entraînement Adam
27
+ self.N_train_lbfgs = 3000 # époques d'entraînement L-BFGS
28
+
29
+ # Paramètres énergétiques (eV)
30
+ self.E_min = -2.5
31
+ self.E_max = 2.5
32
+ self.dE = (self.E_max - self.E_min) / self.Ne
33
+ self.Ef = 0.0
34
+ self.Temp = 10
35
+ self.KBT = self.KB * self.Temp
36
+
37
+ # Paramètres du système quantique (eV)
38
+ self.GammaL = 0.05
39
+ self.GammaR = 0.05
40
+ self.molE = 0.7
41
+ self.Eg = 2 * self.molE
42
+ self.tm = 0.1
43
+
44
+ # Paramètres temporels (s)
45
+ self.Power = 1.0e-15
46
+ self.t_min = 0 * self.Power
47
+ self.t_max = 100 * self.Power
48
+
49
+ # Paramètres de perturbation gaussienne
50
+ self.cent = 15 * self.Power # s
51
+ self.he1 = 0.500 # eV
52
+ self.he2 = 0.00 # eV
53
+ self.W0 = 5.0 * self.Power # s
54
+ self.W1 = self.Eg / self.hbar # rad/s
55
+ self.tau = 6 * self.Power # s
56
+
57
+ # NORMALISATION PHYSIQUE
58
+ self._setup_normalization()
59
+ self._setup_system()
60
+
61
+ def _setup_normalization(self):
62
+ """Configure les échelles de normalisation basées sur la physique"""
63
+ # Échelles caractéristiques du système
64
+ self.E_scale = abs(self.molE) # Échelle d'énergie caractéristique (eV)
65
+ self.t_scale = self.hbar / self.E_scale # Échelle de temps caractéristique (s)
66
+ self.psi_scale = 1.0 # Les fonctions d'onde sont sans dimension
67
+ # FACTEURS D'ÉCHELLE POUR LES UNITÉS PHYSIQUES
68
+ # Le courant normalisé doit être multiplié par ce facteur pour obtenir des Ampères
69
+ self.current_scale = self.e_charge / self.t_scale # A
70
+ print(f"Échelles de normalisation:")
71
+ print(f" Énergie: {self.E_scale:.3f} eV")
72
+ print(f" Temps: {self.t_scale:.2e} s")
73
+ print(f" Temps physique max: {self.t_max:.2e} s")
74
+ print(f" Temps normalisé max: {self.t_max/self.t_scale:.3f}")
75
+ print(f" Facteur de conversion du courant: {self.current_scale:.6e} A")
76
+
77
+ # Vérification de cohérence dimensionnelle
78
+ hbar_over_t_scale = self.hbar / self.t_scale # eV
79
+ print(f" Vérification: ℏ/t_scale = {hbar_over_t_scale:.3f} eV = E_scale = {self.E_scale:.3f} eV ✓")
80
+
81
+ def _setup_system(self):
82
+ """Initialise les matrices du système avec normalisation"""
83
+ # Hamiltonien non perturbé (normalisé)
84
+ self.H_0 = np.array([
85
+ [-self.molE/self.E_scale, 0.0, 0.0],
86
+ [0.0, self.molE/self.E_scale, self.tm/self.E_scale],
87
+ [0.0, self.tm/self.E_scale, self.molE/self.E_scale]
88
+ ], dtype=complex)
89
+
90
+ # Auto-énergies (normalisées)
91
+ SEL1 = np.zeros((self.n, self.n), dtype=complex)
92
+ SEL1[0, 0] = -1j * self.GammaL/self.E_scale
93
+ SEL1[1, 1] = -1j * self.GammaL/self.E_scale
94
+
95
+ SEL2 = np.zeros((self.n, self.n), dtype=complex)
96
+ SEL2[2, 2] = -1j * self.GammaR/self.E_scale
97
+
98
+ self.SELF = SEL1 + SEL2
99
+
100
+ # Normalisation des autres paramètres
101
+ self.E_min_norm = self.E_min / self.E_scale
102
+ self.E_max_norm = self.E_max / self.E_scale
103
+ self.dE_norm = self.dE / self.E_scale
104
+ self.Ef_norm = self.Ef / self.E_scale
105
+ self.KBT_norm = self.KBT / self.E_scale
106
+
107
+ self.t_min_norm = self.t_min / self.t_scale
108
+ self.t_max_norm = self.t_max / self.t_scale
109
+
110
+ # Paramètres de perturbation normalisés
111
+ self.cent_norm = self.cent / self.t_scale
112
+ self.he1_norm = self.he1 / self.E_scale
113
+ self.he2_norm = self.he2 / self.E_scale
114
+ self.W0_norm = self.W0 / self.t_scale
115
+ self.W1_norm = self.W1 * self.t_scale # W1 était en rad/s, maintenant sans dimension
116
+ self.tau_norm = self.tau / self.t_scale
117
+
118
+ # Résolution des états stationnaires avec énergies normalisées
119
+ self._solve_stationary_states()
120
+
121
+ def fermi_dirac(self, E_norm, mu_norm, KBT_norm):
122
+ """Fonction de Fermi-Dirac avec paramètres normalisés"""
123
+ return 1.0 / (1.0 + np.exp((E_norm - mu_norm) / KBT_norm))
124
+
125
+ def _solve_stationary_states(self):
126
+ """Résout les états stationnaires pour chaque énergie normalisée"""
127
+ self.stock_psi = np.zeros((self.Ne, self.n, self.n), dtype=complex)
128
+ vecs = [np.eye(self.n)[:, j:j+1] * np.sqrt(self.GammaL/self.E_scale) for j in range(self.n)]
129
+
130
+ for i in range(self.Ne):
131
+ E_norm = self.E_min_norm + i * self.dE_norm
132
+ a = E_norm * np.eye(self.n, dtype=complex)
133
+ GR = np.linalg.inv(a - self.H_0 - self.SELF)
134
+
135
+ for j in range(self.n):
136
+ vec = vecs[j]
137
+ psi_st = GR @ vec
138
+ self.stock_psi[i, j, :] = psi_st[:, 0]
139
+
140
+ def perturb_gauss(self, t_norm):
141
+ """Calcule la perturbation gaussienne avec temps normalisé"""
142
+ How = np.zeros((self.n, self.n), dtype=complex)
143
+ valeur = (-self.he1_norm * np.cos(self.W1_norm * (t_norm - self.cent_norm)) *
144
+ np.exp(-((t_norm - self.cent_norm)**2) / (2 * self.W0_norm**2)) -
145
+ self.he2_norm * np.cos(self.W1_norm * (t_norm - self.cent_norm - self.tau_norm)) *
146
+ np.exp(-((t_norm - self.cent_norm - self.tau_norm)**2) / (2 * self.W0_norm**2)))
147
+ How[0, 1] = valeur
148
+ How[1, 0] = valeur
149
+ return How
150
+
151
+ def create_model(self, num_hidden_layer=4, num_neurons_per_layer=64):
152
+ model = tf.keras.Sequential()
153
+ model.add(tf.keras.layers.InputLayer(input_shape=(1,)))
154
+ for _ in range(num_hidden_layer):
155
+ model.add(tf.keras.layers.Dense(num_neurons_per_layer, activation='tanh',
156
+ kernel_initializer='glorot_normal'))
157
+ model.add(tf.keras.layers.Dense(2 * self.n))
158
+ return model
159
+
160
+ def physics_loss(self, model, t_colloc_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i):
161
+ """Calcule la perte physique basée sur l'EDP avec unités cohérentes"""
162
+ with tf.GradientTape(persistent=True) as tape:
163
+ tape.watch(t_colloc_norm)
164
+ psi_pred = model(t_colloc_norm)
165
+ u = psi_pred[:, :self.n]
166
+ v = psi_pred[:, self.n:]
167
+
168
+ u_t = tape.batch_jacobian(u, t_colloc_norm)[:, :, 0]
169
+ v_t = tape.batch_jacobian(v, t_colloc_norm)[:, :, 0]
170
+ del tape
171
+
172
+ # Terme source S(t)
173
+ E_complex = tf.cast(E_norm, tf.complex64)
174
+ t_complex = tf.cast(tf.squeeze(t_colloc_norm), tf.complex64)
175
+ phase = tf.exp(-1j * E_complex * t_complex)
176
+ phase = tf.expand_dims(phase, axis=-1)
177
+
178
+ psi_st_broadcast = tf.transpose(psi_st_tensor)
179
+ expo_term = phase * psi_st_broadcast
180
+ expo_term = tf.expand_dims(expo_term, axis=-1)
181
+
182
+ S_complex = tf.matmul(HP_tensor, expo_term)
183
+ S_complex = tf.squeeze(S_complex, axis=-1)
184
+ S_r = tf.math.real(S_complex)
185
+ S_i = tf.math.imag(S_complex)
186
+
187
+ # Équations résiduelles
188
+ u_exp = tf.expand_dims(u, axis=-1)
189
+ v_exp = tf.expand_dims(v, axis=-1)
190
+
191
+ r_u = u_t - tf.squeeze(tf.matmul(H_t_r, v_exp), axis=-1) - tf.squeeze(tf.matmul(H_t_i, u_exp), axis=-1) - S_i
192
+ r_v = -v_t - tf.squeeze(tf.matmul(H_t_r, u_exp), axis=-1) + tf.squeeze(tf.matmul(H_t_i, v_exp), axis=-1) - S_r
193
+
194
+ pde_loss = tf.reduce_mean(tf.square(r_u)) + tf.reduce_mean(tf.square(r_v))
195
+ return pde_loss
196
+
197
+ def initial_conditions_loss(self, model, t_0_norm):
198
+ """Perte pour les conditions initiales (nulles)"""
199
+ psi_pred_0 = model(t_0_norm)
200
+ u_0 = psi_pred_0[:, :self.n]
201
+ v_0 = psi_pred_0[:, self.n:]
202
+ ic_loss = tf.reduce_mean(tf.square(u_0)) + tf.reduce_mean(tf.square(v_0))
203
+ return ic_loss
204
+
205
+ def total_loss(self, model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i):
206
+ """Perte totale combinant physique et conditions initiales"""
207
+ pde_loss = self.physics_loss(model, t_colloc_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i)
208
+ ic_loss = self.initial_conditions_loss(model, t_0_norm)
209
+
210
+ lambda_pde = 1.0
211
+ lambda_ic = 1.0
212
+
213
+ return lambda_pde * pde_loss + lambda_ic * ic_loss, pde_loss, ic_loss
214
+
215
+ def get_weights(self, model):
216
+ """Extrait les poids du modèle sous forme de vecteur 1D"""
217
+ weights = []
218
+ for layer in model.layers:
219
+ if hasattr(layer, 'get_weights') and layer.get_weights():
220
+ for w in layer.get_weights():
221
+ weights.append(w.flatten())
222
+ return np.concatenate(weights) if weights else np.array([])
223
+
224
+ def set_weights(self, model, weights_1d):
225
+ """Restaure les poids du modèle à partir d'un vecteur 1D"""
226
+ idx = 0
227
+ for layer in model.layers:
228
+ if hasattr(layer, 'get_weights') and layer.get_weights():
229
+ layer_weights = []
230
+ for w in layer.get_weights():
231
+ size = w.size
232
+ new_w = weights_1d[idx:idx+size].reshape(w.shape)
233
+ layer_weights.append(new_w)
234
+ idx += size
235
+ layer.set_weights(layer_weights)
236
+
237
+ def lbfgs_loss_func(self, weights_1d, model, t_colloc_norm, t_0_norm, E_norm,
238
+ psi_st_tensor, HP_tensor, H_t_r, H_t_i):
239
+ """Fonction de perte pour L-BFGS"""
240
+ # Restaurer les poids
241
+ self.set_weights(model, weights_1d)
242
+
243
+ # Calculer la perte
244
+ with tf.GradientTape() as tape:
245
+ loss, pde_loss, ic_loss = self.total_loss(
246
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
247
+ )
248
+
249
+ # Calculer les gradients
250
+ gradients = tape.gradient(loss, model.trainable_variables)
251
+
252
+ # Convertir les gradients en vecteur 1D
253
+ grad_1d = []
254
+ for grad in gradients:
255
+ if grad is not None:
256
+ grad_1d.append(grad.numpy().flatten())
257
+ grad_1d = np.concatenate(grad_1d) if grad_1d else np.array([])
258
+
259
+ return float(loss.numpy()), grad_1d.astype(np.float64)
260
+
261
+ def train_model_hybrid(self, model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor,
262
+ HP_tensor, H_t_r, H_t_i):
263
+ """Entraîne le modèle avec Adam puis L-BFGS"""
264
+
265
+ # Phase 1: Entraînement avec Adam
266
+ print("Phase 1: Entraînement avec Adam...")
267
+
268
+ # Optimiseur Adam avec taux d'apprentissage adaptatif
269
+ initial_lr = 1e-3
270
+ lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(
271
+ initial_learning_rate=initial_lr,
272
+ decay_steps=30000,
273
+ decay_rate=0.9,
274
+ staircase=True
275
+ )
276
+ optimizer_adam = tf.keras.optimizers.Adam(learning_rate=lr_schedule)
277
+
278
+ @tf.function
279
+ def adam_train_step():
280
+ with tf.GradientTape() as tape:
281
+ loss, pde_loss, ic_loss = self.total_loss(
282
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
283
+ )
284
+ gradients = tape.gradient(loss, model.trainable_variables)
285
+ optimizer_adam.apply_gradients(zip(gradients, model.trainable_variables))
286
+ return loss, pde_loss, ic_loss
287
+
288
+ adam_history = {'total_loss': [], 'pde_loss': [], 'ic_loss': []}
289
+ t_start_adam = time()
290
+
291
+ for epoch in range(self.N_train_adam):
292
+ loss, pde_loss, ic_loss = adam_train_step()
293
+
294
+ if epoch % 1000 == 0:
295
+ print(f"Adam Epoch {epoch:5d}: Total = {loss:.6e}, PDE = {pde_loss:.6e}, IC = {ic_loss:.6e}")
296
+
297
+ adam_history['total_loss'].append(float(loss))
298
+ adam_history['pde_loss'].append(float(pde_loss))
299
+ adam_history['ic_loss'].append(float(ic_loss))
300
+
301
+ # Critère d'arrêt précoce pour Adam
302
+ if epoch > 1000 and loss < 1e-4:
303
+ print(f"Arrêt précoce d'Adam à l'époque {epoch}")
304
+ break
305
+
306
+ print(f"Temps d'entraînement Adam: {time() - t_start_adam:.2f} secondes")
307
+ print(f"Perte finale Adam: {loss:.6e}")
308
+
309
+ # Phase 2: Raffinement avec L-BFGS
310
+ print("\nPhase 2: Raffinement avec L-BFGS...")
311
+
312
+ t_start_lbfgs = time()
313
+
314
+ # Poids initiaux pour L-BFGS
315
+ initial_weights = self.get_weights(model)
316
+
317
+ # Configuration L-BFGS
318
+ lbfgs_options = {
319
+ 'maxiter': self.N_train_lbfgs,
320
+ 'maxfun': self.N_train_lbfgs * 2,
321
+ 'ftol': 1e-12,
322
+ 'gtol': 1e-12,
323
+ 'eps': 1e-8,
324
+ 'disp': True
325
+ }
326
+
327
+ # Variables pour suivre les itérations L-BFGS
328
+ self.lbfgs_iter = 0
329
+ self.lbfgs_losses = []
330
+
331
+ def callback_func(xk):
332
+ self.lbfgs_iter += 1
333
+ if self.lbfgs_iter % 100 == 0:
334
+ # Calculer la perte actuelle pour l'affichage
335
+ self.set_weights(model, xk)
336
+ loss, pde_loss, ic_loss = self.total_loss(
337
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
338
+ )
339
+ print(f"L-BFGS Iter {self.lbfgs_iter:4d}: Total = {loss:.6e}, PDE = {pde_loss:.6e}, IC = {ic_loss:.6e}")
340
+ self.lbfgs_losses.append(float(loss))
341
+
342
+ # Optimisation L-BFGS
343
+ try:
344
+ result = scipy.optimize.minimize(
345
+ fun=lambda w: self.lbfgs_loss_func(w, model, t_colloc_norm, t_0_norm,
346
+ E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i),
347
+ x0=initial_weights.astype(np.float64),
348
+ method='L-BFGS-B',
349
+ jac=True,
350
+ callback=callback_func,
351
+ options=lbfgs_options
352
+ )
353
+
354
+ # Restaurer les poids optimaux
355
+ self.set_weights(model, result.x)
356
+
357
+ print(f"L-BFGS terminé: {result.message}")
358
+ print(f"Nombre d'itérations: {result.nit}")
359
+ print(f"Perte finale: {result.fun:.6e}")
360
+
361
+ except Exception as e:
362
+ print(f"Erreur L-BFGS: {e}")
363
+ print("Utilisation des poids d'Adam comme solution finale")
364
+
365
+ print(f"Temps d'entraînement L-BFGS: {time() - t_start_lbfgs:.2f} secondes")
366
+
367
+ # Évaluation finale
368
+ final_loss, final_pde_loss, final_ic_loss = self.total_loss(
369
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
370
+ )
371
+ print(f"Perte finale combinée: Total = {final_loss:.6e}, PDE = {final_pde_loss:.6e}, IC = {final_ic_loss:.6e}")
372
+
373
+ # Historique combiné
374
+ combined_history = {
375
+ 'adam_history': adam_history,
376
+ 'lbfgs_losses': self.lbfgs_losses,
377
+ 'final_loss': float(final_loss),
378
+ 'final_pde_loss': float(final_pde_loss),
379
+ 'final_ic_loss': float(final_ic_loss)
380
+ }
381
+
382
+ return combined_history
383
+
384
+ def solve_for_energy_mode(self, i_energy, j_mode):
385
+ """Résout pour un mode et une énergie spécifiques"""
386
+ E_norm = self.E_min_norm + i_energy * self.dE_norm
387
+
388
+ # État stationnaire
389
+ psi_st = self.stock_psi[i_energy, j_mode, :].reshape((self.n, 1))
390
+ psi_st_tensor = tf.convert_to_tensor(psi_st, dtype=tf.complex64)
391
+
392
+ # Grille temporelle pour collocation (normalisée)
393
+ #t_colloc_norm = tf.reshape(tf.linspace(self.t_min_norm, self.t_max_norm, self.N), (-1, 1))
394
+ t_colloc_norm = tf.random.uniform((self.N, 1), minval=self.t_min_norm, maxval=self.t_max_norm)
395
+ t_0_norm = tf.zeros((self.N_0, 1), dtype=tf.float32) + self.t_min_norm
396
+
397
+ # Hamiltoniens perturbés (normalisés)
398
+ HP_list = []
399
+ for k in range(self.N):
400
+ t_k_norm = float(t_colloc_norm[k, 0])
401
+ HP_k = self.perturb_gauss(t_k_norm)
402
+ HP_list.append(HP_k)
403
+
404
+ HP_tensor = tf.convert_to_tensor(np.stack(HP_list), dtype=tf.complex64)
405
+
406
+ # Hamiltonien total (normalisé)
407
+ H_0_tensor = tf.convert_to_tensor(self.H_0, dtype=tf.complex64)
408
+ SELF_tensor = tf.convert_to_tensor(self.SELF, dtype=tf.complex64)
409
+
410
+ H_0_expanded = tf.repeat(tf.expand_dims(H_0_tensor, axis=0), self.N, axis=0)
411
+ SELF_expanded = tf.repeat(tf.expand_dims(SELF_tensor, axis=0), self.N, axis=0)
412
+
413
+ H_t = HP_tensor + H_0_expanded + SELF_expanded
414
+ H_t_r = tf.math.real(H_t)
415
+ H_t_i = tf.math.imag(H_t)
416
+
417
+ # Création et entraînement du modèle
418
+ model = self.create_model()
419
+ history = self.train_model_hybrid(
420
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
421
+ )
422
+
423
+ return model, history, E_norm, psi_st_tensor
424
+
425
+ def predict_and_compute_current(self, model, E_norm, psi_st_tensor):
426
+ """Fait des prédictions et calcule le courant"""
427
+ # Grille temporelle pour prédiction (normalisée)
428
+ t_pred_norm_vals = np.linspace(self.t_min_norm, self.t_max_norm, self.N_pred)
429
+ t_pred_norm = tf.convert_to_tensor(t_pred_norm_vals.reshape(-1, 1), dtype=tf.float32)
430
+
431
+ # Hamiltoniens pour prédiction (normalisés)
432
+ HP_pred_list = []
433
+ for s in range(self.N_pred):
434
+ t_s_norm = t_pred_norm_vals[s]
435
+ HP_s = self.perturb_gauss(t_s_norm)
436
+ HP_pred_list.append(HP_s)
437
+
438
+ HP_pred = tf.convert_to_tensor(np.stack(HP_pred_list), dtype=tf.complex64)
439
+
440
+ # Prédiction du réseau
441
+ psi_pred = model(t_pred_norm) # [N_pred, 2n]
442
+ u_pred = psi_pred[:, :self.n]
443
+ v_pred = psi_pred[:, self.n:]
444
+ psi_p = tf.complex(u_pred, v_pred) # [N_pred, n]
445
+
446
+ # Ajout de l'état stationnaire avec phase (normalisée)
447
+ arg = -E_norm * t_pred_norm_vals # Argument sans dimension
448
+ phase = tf.exp(1j * tf.cast(arg, tf.complex64))
449
+ phase = tf.expand_dims(phase, axis=-1) # [N_pred, 1]
450
+
451
+ psi_st_broadcast = tf.transpose(psi_st_tensor) # [1, n]
452
+ psi_total = psi_p + phase * psi_st_broadcast # [N_pred, n]
453
+
454
+ return psi_total, t_pred_norm_vals, HP_pred
455
+
456
+ def run_full_simulation(self):
457
+ """Lance la simulation complète"""
458
+ os.makedirs("TEST1", exist_ok=True)
459
+
460
+ J_current = tf.zeros((self.N_pred,), dtype=tf.complex64)
461
+ all_histories = []
462
+
463
+ for i in range(self.Ne):
464
+ print(f"\n=== Énergie {i+1}/{self.Ne} ===")
465
+
466
+ courant_mode = tf.zeros((self.N_pred,), dtype=tf.complex64)
467
+
468
+ for j in range(self.n):
469
+ print(f"\nMode {j+1}/{self.n}")
470
+
471
+ model, history, E_norm, psi_st_tensor = self.solve_for_energy_mode(i, j)
472
+ all_histories.append(history)
473
+
474
+ psi_total, t_pred_norm_vals, HP_pred = self.predict_and_compute_current(model, E_norm, psi_st_tensor)
475
+
476
+ # Calcul du courant pour ce mode
477
+ fermi_factor = self.fermi_dirac(E_norm, self.Ef_norm, self.KBT_norm)
478
+ fermi_tensor = tf.convert_to_tensor(fermi_factor, dtype=tf.complex64)
479
+
480
+ # Hamiltonien total pour le courant (normalisé)
481
+ H_0_tensor = tf.convert_to_tensor(self.H_0, dtype=tf.complex64)
482
+ SELF_tensor = tf.convert_to_tensor(self.SELF, dtype=tf.complex64)
483
+
484
+ H_0_pred = tf.repeat(tf.expand_dims(H_0_tensor, axis=0), self.N_pred, axis=0)
485
+ SELF_pred = tf.repeat(tf.expand_dims(SELF_tensor, axis=0), self.N_pred, axis=0)
486
+
487
+ HT_pred = HP_pred + H_0_pred + SELF_pred
488
+
489
+ # Courant: J ∝ ψ₁* H₁₂ ψ₂
490
+ psi1_conj = tf.math.conj(psi_total[:, 1])
491
+ H12 = HT_pred[:, 1, 2]
492
+ psi2 = psi_total[:, 2]
493
+
494
+ courant_mode += fermi_tensor * psi1_conj * H12 * psi2
495
+
496
+ J_current += self.dE_norm * (1.0 / (2.0 * self.pi)) * courant_mode
497
+
498
+ J_current_norm = J_current
499
+
500
+ # Courant final
501
+ CJJ = -2.0 * tf.math.imag(J_current_norm)
502
+
503
+ # Sauvegarde
504
+ J_current_np = J_current_norm.numpy()
505
+ CJJ_np = CJJ.numpy()
506
+ # Conversion en unités physiques
507
+ CJJ_physical_np = CJJ_np * self.current_scale # Courant en Ampères
508
+ t_physical_vals = t_pred_norm_vals * self.t_scale # Temps en secondes
509
+
510
+ #np.savetxt("TEST1/COURANT", J_current_np)
511
+ #np.savetxt("TEST1/CJJ", CJJ_np)
512
+ # Sauvegarde des données normalisées
513
+ np.savetxt("TEST1/COURANT_normalized", J_current_np)
514
+ np.savetxt("TEST1/CJJ_normalized", CJJ_np)
515
+ np.savetxt("TEST1/time_normalized", t_pred_norm_vals)
516
+
517
+ # Sauvegarde des données physiques
518
+ np.savetxt("TEST1/CJJ_physical", CJJ_physical_np)
519
+ np.savetxt("TEST1/time_physical", t_physical_vals)
520
+
521
+ # Sauvegarde combinée pour facilité d'usage
522
+ np.savetxt("TEST1/current_data_normalized.dat",
523
+ np.column_stack((t_pred_norm_vals, CJJ_np)),
524
+ header="Time_normalized Current_normalized", fmt='%.10e')
525
+ np.savetxt("TEST1/current_data_physical.dat",
526
+ np.column_stack((t_physical_vals, CJJ_physical_np)),
527
+ header="Time_seconds Current_amperes", fmt='%.10e')
528
+
529
+ # Visualisation améliorée
530
+ fig, axes = plt.subplots(2, 2, figsize=(15, 10))
531
+
532
+ # Graphique du courant
533
+ # Graphique du courant normalisé
534
+ axes[0, 0].plot(t_pred_norm_vals, CJJ_np, label='Courant normalisé', color='navy', linewidth=2)
535
+ axes[0, 0].set_xlabel("Temps normalisé (sans dimension)", fontsize=14)
536
+ axes[0, 0].set_ylabel("Courant normalisé (sans dimension)", fontsize=14)
537
+ axes[0, 0].set_title("Courant normalisé", fontsize=14)
538
+ axes[0, 0].grid(True, alpha=0.3)
539
+
540
+ #axes[0, 0].plot(t_pred_norm_vals, CJJ_np, label='Courant CJJ(t)', color='navy', linewidth=2)
541
+ #axes[0, 0].set_xlabel("Time (unitless)", fontsize=14)
542
+ #axes[0, 0].set_ylabel("Current (unitless)", fontsize=14)
543
+ #axes[0, 0].set_title("Current variation over time", fontsize=14)
544
+ #axes[0, 0].grid(True, alpha=0.3)
545
+
546
+ # Composantes du courant complexe
547
+ axes[0, 1].plot(t_pred_norm_vals, np.real(J_current_np), label='Re(J)', color='red', linewidth=1)
548
+ axes[0, 1].plot(t_pred_norm_vals, np.imag(J_current_np), label='Im(J)', color='green', linewidth=1)
549
+ axes[0, 1].set_xlabel("Temps (unitless)", fontsize=14)
550
+ axes[0, 1].set_ylabel("J_current", fontsize=14)
551
+ axes[0, 1].set_title("Composantes du courant", fontsize=14)
552
+ axes[0, 1].legend()
553
+ axes[0, 1].grid(True, alpha=0.3)
554
+ # Convergence Adam (premier mode/énergie comme exemple)
555
+ if all_histories:
556
+ first_history = all_histories[0]
557
+ if 'adam_history' in first_history:
558
+ adam_hist = first_history['adam_history']
559
+ axes[1, 0].semilogy(adam_hist['total_loss'], label='Total Loss', color='blue', linewidth=1)
560
+ axes[1, 0].semilogy(adam_hist['pde_loss'], label='PDE Loss', color='orange', linewidth=1)
561
+ axes[1, 0].semilogy(adam_hist['ic_loss'], label='IC Loss', color='purple', linewidth=1)
562
+ axes[1, 0].set_xlabel("Epochs Adam", fontsize=12)
563
+ axes[1, 0].set_ylabel("Loss", fontsize=12)
564
+ axes[1, 0].set_title("Convergence Adam", fontsize=12)
565
+ axes[1, 0].legend()
566
+ axes[1, 0].grid(True, alpha=0.3)
567
+
568
+ # Graphique du courant physique
569
+ axes[1, 1].plot(t_physical_vals * 1e15, CJJ_physical_np,
570
+ label='Courant physique', color='darkred', linewidth=2)
571
+ axes[1, 1].set_xlabel("Temps (fs)", fontsize=12)
572
+ axes[1, 1].set_ylabel("Courant (A)", fontsize=12)
573
+ axes[1, 1].set_title("Courant physique", fontsize=12)
574
+ axes[1, 1].grid(True, alpha=0.3)
575
+
576
+
577
+ # Perturbation gaussienne
578
+ # Figure séparée pour la perturbation gaussienne
579
+ plt.figure(figsize=(10, 6))
580
+ t_pert = np.linspace(self.t_min_norm, self.t_max_norm, 1000)
581
+ pert_values = []
582
+ for t in t_pert:
583
+ H_pert = self.perturb_gauss(t)
584
+ pert_values.append(np.real(H_pert[0, 1])) # Élément (0,1) réel
585
+
586
+ plt.plot(t_pert, pert_values, label='Perturbation H₀₁(t)', color='darkgreen', linewidth=2)
587
+ plt.xlabel("Temps normalisé (sans dimension)", fontsize=14)
588
+ plt.ylabel("Amplitude normalisée", fontsize=14)
589
+ plt.title("Perturbation gaussienne", fontsize=16)
590
+ plt.grid(True, alpha=0.3)
591
+ plt.tight_layout()
592
+ plt.savefig("TEST1/perturbation_gaussian.png", dpi=300, bbox_inches='tight')
593
+ plt.show()
594
+
595
+
596
+
597
+ plt.tight_layout()
598
+ plt.savefig("TEST1/quantum_simulation_results.png", dpi=300, bbox_inches='tight')
599
+ plt.show()
600
+
601
+ # Statistiques de convergence
602
+ print("\n=== RÉSULTATS DE LA SIMULATION ===")
603
+ print(f"Courant normalisé maximal: {np.max(np.abs(CJJ_np)):.6e}")
604
+ print(f"Courant physique maximal: {np.max(np.abs(CJJ_physical_np)):.6e} A")
605
+ print(f"Courant physique maximal: {np.max(np.abs(CJJ_physical_np)):.6e} A")
606
+ print(f"Courant normalisé final: {CJJ_np[-1]:.6e}")
607
+ print(f"Courant physique final: {CJJ_physical_np[-1]:.6e} A")
608
+ print(f"Facteur de conversion: {self.current_scale:.6e} A")
609
+ # Résumé des pertes finales
610
+ final_losses = [hist.get('final_loss', 0) for hist in all_histories]
611
+ if final_losses:
612
+ print(f"Perte moyenne finale: {np.mean(final_losses):.6e}")
613
+ print(f"Perte maximale finale: {np.max(final_losses):.6e}")
614
+
615
+ return {
616
+ 'current_normalized': CJJ_np,
617
+ 'current_physical': CJJ_physical_np,
618
+ 'time_normalized': t_pred_norm_vals,
619
+ 'time_physical': t_physical_vals,
620
+ 'complex_current': J_current_np,
621
+ 'current_scale': self.current_scale,
622
+ 'histories': all_histories
623
+ }
624
+
625
+ # Point d'entrée principal
626
+ if __name__ == "__main__":
627
+ print("=== SIMULATEUR QUANTUM PINN ===")
628
+ print("Initialisation du solveur...")
629
+
630
+ solver = QuantumPINNSolver()
631
+
632
+ print("\nDémarrage de la simulation complète...")
633
+ results = solver.run_full_simulation()
634
+
635
+ print("\n=== SIMULATION TERMINÉE ===")
636
+ print("Résultats sauvegardés dans le dossier TEST1/")
637
+ print("Graphiques générés: quantum_simulation_results.png")
638
+