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.
modulation_da/main.py ADDED
@@ -0,0 +1,77 @@
1
+ # main.py
2
+ import sys
3
+ import os
4
+ import argparse
5
+ from datetime import datetime
6
+
7
+ # Add the current directory to system path for imports
8
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from quantum_solver import QuantumPINNSolver
11
+ from modulation_da import __version__
12
+
13
+ def run_simulation(perturbation_type):
14
+ """
15
+ Launches the simulation with the given perturbation type.
16
+ """
17
+ print("=== QUANTUM PINN SIMULATOR ===")
18
+
19
+ available_perturbations = ['gaussian', 'sinusoidal', 'rectangular',
20
+ 'exponential', 'modulated_sinusoidal']
21
+
22
+ if perturbation_type not in available_perturbations:
23
+ print(f"Invalid perturbation type: {perturbation_type}. Please choose from: {', '.join(available_perturbations)}")
24
+ return
25
+
26
+ # Create solver instance
27
+ solver = QuantumPINNSolver(perturbation_type=perturbation_type)
28
+
29
+ print(f"\nStarting simulation with {perturbation_type} perturbation...")
30
+ print(f"Simulation ID: {solver.config.simulation_id}")
31
+
32
+ try:
33
+ # LAUNCH COMPLETE SIMULATION
34
+ start_time = datetime.now()
35
+ solver.run_full_simulation()
36
+ end_time = datetime.now()
37
+
38
+ print(f"\nSimulation time: {end_time - start_time}")
39
+
40
+ # AUTOMATIC SAVING
41
+ solver.save_results()
42
+
43
+ # PLOT GENERATION
44
+ solver.generate_plots()
45
+
46
+ # DISPLAY SUMMARY
47
+ print(solver.get_simulation_summary())
48
+
49
+ except KeyboardInterrupt:
50
+ print("\n⚠ Simulation interrupted by user")
51
+ except Exception as e:
52
+ print(f"\n❌ Simulation failed: {e}")
53
+ raise
54
+
55
+ def main():
56
+ """
57
+ Main entry point for the CLI.
58
+ """
59
+ parser = argparse.ArgumentParser(description="Quantum PINN Simulator for donor-acceptor modulation.")
60
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
61
+
62
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
63
+
64
+ # 'run' command
65
+ run_parser = subparsers.add_parser('run', help='Run a simulation')
66
+ run_parser.add_argument('--perturbation-type', type=str, default='gaussian',
67
+ help='Type of perturbation to apply (e.g., gaussian, sinusoidal)')
68
+
69
+ args = parser.parse_args()
70
+
71
+ if args.command == 'run':
72
+ run_simulation(args.perturbation_type)
73
+ else:
74
+ parser.print_help()
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,103 @@
1
+ # perturbations.py
2
+ import numpy as np
3
+
4
+ class PerturbationManager:
5
+ """Management of all time-dependent perturbations"""
6
+
7
+ def __init__(self, config):
8
+ self.config = config
9
+ self.setup_parameters()
10
+
11
+ def setup_parameters(self):
12
+ """Perturbation parameters"""
13
+ self.cent = 15 * self.config.Power
14
+ self.he1, self.he2 = 0.500, 0.00
15
+ self.W0 = 5.0 * self.config.Power
16
+ self.W1 = 2 * self.config.molE / self.config.hbar
17
+ self.tau = 6 * self.config.Power
18
+
19
+ # Normalization
20
+ self.cent_norm = self.cent / self.config.t_scale
21
+ self.he1_norm = self.he1 / self.config.E_scale
22
+ self.he2_norm = self.he2 / self.config.E_scale
23
+ self.W0_norm = self.W0 / self.config.t_scale
24
+ self.W1_norm = self.W1 * self.config.t_scale
25
+ self.tau_norm = self.tau / self.config.t_scale
26
+
27
+ def gaussian_perturbation(self, t_norm):
28
+ """Gaussian perturbation"""
29
+ How = np.zeros((self.config.n, self.config.n), dtype=complex)
30
+
31
+ term1 = (-self.he1_norm * np.cos(self.W1_norm * (t_norm - self.cent_norm)) *
32
+ np.exp(-((t_norm - self.cent_norm)**2) / (2 * self.W0_norm**2)))
33
+
34
+ term2 = (-self.he2_norm * np.cos(self.W1_norm * (t_norm - self.cent_norm - self.tau_norm)) *
35
+ np.exp(-((t_norm - self.cent_norm - self.tau_norm)**2) / (2 * self.W0_norm**2)))
36
+
37
+ valeur = term1 + term2
38
+ How[0, 1] = How[1, 0] = valeur
39
+ return How
40
+
41
+ def sinusoidal_perturbation(self, t_norm):
42
+ """Continuous sinusoidal perturbation"""
43
+ How = np.zeros((self.config.n, self.config.n), dtype=complex)
44
+ amplitude = self.he1_norm
45
+ frequency = self.W1_norm
46
+
47
+ # Simple sine wave
48
+ valeur = -amplitude * np.cos(frequency * t_norm)
49
+ How[0, 1] = How[1, 0] = valeur
50
+ return How
51
+
52
+ def rectangular_perturbation(self, t_norm):
53
+ """Rectangular pulse"""
54
+ How = np.zeros((self.config.n, self.config.n), dtype=complex)
55
+ t_start, t_duration = 10, 20
56
+
57
+ if t_start <= t_norm <= t_start + t_duration:
58
+ valeur = -self.he1_norm * np.cos(self.W1_norm * t_norm)
59
+ How[0, 1] = How[1, 0] = valeur
60
+ return How
61
+
62
+ def exponential_perturbation(self, t_norm):
63
+ """Exponential perturbation"""
64
+ How = np.zeros((self.config.n, self.config.n), dtype=complex)
65
+ t_start = 10
66
+
67
+ if t_norm >= t_start:
68
+ decay = np.exp(-(t_norm - t_start) / self.W0_norm)
69
+ valeur = -self.he1_norm * decay * np.cos(self.W1_norm * t_norm)
70
+ How[0, 1] = How[1, 0] = valeur
71
+ return How
72
+
73
+ def modulated_sinusoidal(self, t_norm):
74
+ """Amplitude-modulated sine wave"""
75
+ How = np.zeros((self.config.n, self.config.n), dtype=complex)
76
+
77
+ # Gaussian envelope
78
+ envelope = np.exp(-((t_norm - self.cent_norm)**2) / (2 * self.W0_norm**2))
79
+ carrier = np.cos(self.W1_norm * t_norm)
80
+
81
+ valeur = -self.he1_norm * envelope * carrier
82
+ How[0, 1] = How[1, 0] = valeur
83
+ return How
84
+
85
+ def get_perturbation(self, t_norm, perturbation_type='gaussian'):
86
+ """Perturbation selector"""
87
+ perturbations = {
88
+ 'gaussian': self.gaussian_perturbation,
89
+ 'sinusoidal': self.sinusoidal_perturbation,
90
+ 'rectangular': self.rectangular_perturbation,
91
+ 'exponential': self.exponential_perturbation,
92
+ 'modulated_sinusoidal': self.modulated_sinusoidal
93
+ }
94
+
95
+ return perturbations[perturbation_type](t_norm)
96
+
97
+ def generate_pulse_profile(self, t_values, perturbation_type):
98
+ """Generates pulse profile for visualization"""
99
+ profile = []
100
+ for t in t_values:
101
+ H_pert = self.get_perturbation(t, perturbation_type)
102
+ profile.append(np.real(H_pert[0, 1]))
103
+ return np.array(profile)
@@ -0,0 +1,243 @@
1
+ # pinn_model.py
2
+ import tensorflow as tf
3
+ import numpy as np
4
+ from time import time
5
+ import scipy.optimize
6
+
7
+ class PINNModel:
8
+ """Complete management of PINN model and hybrid training"""
9
+ def __init__(self, config):
10
+ self.config = config
11
+ self.N = 15000
12
+ self.N_0 = 2000
13
+ self.N_pred = 5000
14
+ self.N_train_adam = 90000
15
+ self.N_train_lbfgs = 3000
16
+
17
+ def create_model(self, num_hidden=4, num_neurons=64):
18
+ """Creates the neural network"""
19
+ model = tf.keras.Sequential()
20
+ model.add(tf.keras.layers.InputLayer(input_shape=(1,)))
21
+
22
+ for _ in range(num_hidden):
23
+ model.add(tf.keras.layers.Dense(num_neurons, activation='tanh',
24
+ kernel_initializer='glorot_normal'))
25
+
26
+ model.add(tf.keras.layers.Dense(2 * self.config.n))
27
+ return model
28
+
29
+ def physics_loss(self, model, t_colloc_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i):
30
+ """Calculates physics loss based on PDE with consistent units"""
31
+ with tf.GradientTape(persistent=True) as tape:
32
+ tape.watch(t_colloc_norm)
33
+ psi_pred = model(t_colloc_norm)
34
+ u = psi_pred[:, :self.config.n]
35
+ v = psi_pred[:, self.config.n:]
36
+
37
+ u_t = tape.batch_jacobian(u, t_colloc_norm)[:, :, 0]
38
+ v_t = tape.batch_jacobian(v, t_colloc_norm)[:, :, 0]
39
+ del tape
40
+ # Source term S(t)
41
+ E_complex = tf.cast(E_norm, tf.complex64)
42
+ t_complex = tf.cast(tf.squeeze(t_colloc_norm), tf.complex64)
43
+ phase = tf.exp(-1j * E_complex * t_complex)
44
+ phase = tf.expand_dims(phase, axis=-1) # [N, 1]
45
+
46
+ psi_st_broadcast = tf.transpose(psi_st_tensor) # [1, n]
47
+ expo_term = phase * psi_st_broadcast # [N, n]
48
+ expo_term = tf.expand_dims(expo_term, axis=-1) # [N, n, 1]
49
+
50
+ S_complex = tf.matmul(HP_tensor, expo_term) # [N, n, n] @ [N, n, 1] = [N, n, 1]
51
+ S_complex = tf.squeeze(S_complex, axis=-1) # [N, n]
52
+ S_r = tf.math.real(S_complex)
53
+ S_i = tf.math.imag(S_complex)
54
+
55
+ # Residual equations
56
+ u_exp = tf.expand_dims(u, axis=-1) # [N, n, 1]
57
+ v_exp = tf.expand_dims(v, axis=-1) # [N, n, 1]
58
+
59
+ # H_t_r et H_t_i are [N, n, n]
60
+ # u_exp et v_exp are [N, n, 1]
61
+ # Result matmul: [N, n, 1] -> squeeze -> [N, n]
62
+ 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
63
+ 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
64
+
65
+ pde_loss = tf.reduce_mean(tf.square(r_u)) + tf.reduce_mean(tf.square(r_v))
66
+ return pde_loss
67
+
68
+ def initial_conditions_loss(self, model, t_0_norm):
69
+ """Loss for initial conditions (zero)"""
70
+ psi_pred_0 = model(t_0_norm)
71
+ u_0 = psi_pred_0[:, :self.config.n]
72
+ v_0 = psi_pred_0[:, self.config.n:]
73
+ ic_loss = tf.reduce_mean(tf.square(u_0)) + tf.reduce_mean(tf.square(v_0))
74
+ return ic_loss
75
+
76
+ def total_loss(self, model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i):
77
+ """Total loss combining physics and initial conditions"""
78
+ pde_loss = self.physics_loss(model, t_colloc_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i)
79
+ ic_loss = self.initial_conditions_loss(model, t_0_norm)
80
+
81
+ lambda_pde = 1.0
82
+ lambda_ic = 1.0
83
+
84
+ return lambda_pde * pde_loss + lambda_ic * ic_loss, pde_loss, ic_loss
85
+
86
+ def get_weights(self, model):
87
+ """Extracts model weights as 1D vector"""
88
+ weights = []
89
+ for layer in model.layers:
90
+ if hasattr(layer, 'get_weights') and layer.get_weights():
91
+ for w in layer.get_weights():
92
+ weights.append(w.flatten())
93
+ return np.concatenate(weights) if weights else np.array([])
94
+
95
+ def set_weights(self, model, weights_1d):
96
+ """Restores model weights from 1D vector"""
97
+ idx = 0
98
+ for layer in model.layers:
99
+ if hasattr(layer, 'get_weights') and layer.get_weights():
100
+ layer_weights = []
101
+ for w in layer.get_weights():
102
+ size = w.size
103
+ new_w = weights_1d[idx:idx+size].reshape(w.shape)
104
+ layer_weights.append(new_w)
105
+ idx += size
106
+ layer.set_weights(layer_weights)
107
+
108
+ def lbfgs_loss_func(self, weights_1d, model, t_colloc_norm, t_0_norm, E_norm,
109
+ psi_st_tensor, HP_tensor, H_t_r, H_t_i):
110
+ """Loss function for L-BFGS"""
111
+ self.set_weights(model, weights_1d)
112
+
113
+ with tf.GradientTape() as tape:
114
+ loss, pde_loss, ic_loss = self.total_loss(
115
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
116
+ )
117
+
118
+ gradients = tape.gradient(loss, model.trainable_variables)
119
+ grad_1d = []
120
+ for grad in gradients:
121
+ if grad is not None:
122
+ grad_1d.append(grad.numpy().flatten())
123
+ grad_1d = np.concatenate(grad_1d) if grad_1d else np.array([])
124
+
125
+ return float(loss.numpy()), grad_1d.astype(np.float64)
126
+
127
+ def train_hybrid(self, model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor,
128
+ HP_tensor, H_t_r, H_t_i):
129
+ """Trains the model with Adam then L-BFGS"""
130
+
131
+ # Phase 1: Adam training
132
+ print(" Phase 1: Adam training...")
133
+
134
+ # Adam optimizer with adaptive learning rate
135
+ initial_lr = 1e-3
136
+ lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(
137
+ initial_learning_rate=initial_lr,
138
+ decay_steps=30000,
139
+ decay_rate=0.9,
140
+ staircase=True
141
+ )
142
+ optimizer_adam = tf.keras.optimizers.Adam(learning_rate=lr_schedule)
143
+
144
+ @tf.function
145
+ def adam_train_step():
146
+ with tf.GradientTape() as tape:
147
+ loss, pde_loss, ic_loss = self.total_loss(
148
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
149
+ )
150
+ gradients = tape.gradient(loss, model.trainable_variables)
151
+ optimizer_adam.apply_gradients(zip(gradients, model.trainable_variables))
152
+ return loss, pde_loss, ic_loss
153
+
154
+ adam_history = {'total_loss': [], 'pde_loss': [], 'ic_loss': []}
155
+ t_start_adam = time()
156
+
157
+ for epoch in range(self.N_train_adam):
158
+ loss, pde_loss, ic_loss = adam_train_step()
159
+
160
+ if epoch % 1000 == 0:
161
+ print(f" Adam Epoch {epoch:5d}: Total = {loss:.6e}, PDE = {pde_loss:.6e}, IC = {ic_loss:.6e}")
162
+
163
+ adam_history['total_loss'].append(float(loss))
164
+ adam_history['pde_loss'].append(float(pde_loss))
165
+ adam_history['ic_loss'].append(float(ic_loss))
166
+
167
+ # Early stopping criterion for Adam
168
+ if epoch > 1000 and loss < 1e-4:
169
+ print(f" Early stopping at epoch {epoch}")
170
+ break
171
+
172
+ print(f" Adam training time: {time() - t_start_adam:.2f}s")
173
+ print(f" Final Adam loss: {loss:.6e}")
174
+
175
+ # Phase 2: L-BFGS refinement
176
+ print(" Phase 2: L-BFGS refinement...")
177
+
178
+ t_start_lbfgs = time()
179
+
180
+ # Initial weights for L-BFGS
181
+ initial_weights = self.get_weights(model)
182
+
183
+ # L-BFGS configuration
184
+ lbfgs_options = {
185
+ 'maxiter': self.N_train_lbfgs,
186
+ 'maxfun': self.N_train_lbfgs * 2,
187
+ 'ftol': 1e-12,
188
+ 'gtol': 1e-12,
189
+ 'eps': 1e-8,
190
+ 'disp': True
191
+ }
192
+
193
+ # Variables to track L-BFGS iterations
194
+ self.lbfgs_iter = 0
195
+ self.lbfgs_losses = []
196
+
197
+ def callback_func(xk):
198
+ self.lbfgs_iter += 1
199
+ if self.lbfgs_iter % 100 == 0:
200
+ self.set_weights(model, xk)
201
+ loss, pde_loss, ic_loss = self.total_loss(
202
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
203
+ )
204
+ print(f" L-BFGS Iter {self.lbfgs_iter:4d}: Total = {loss:.6e}, PDE = {pde_loss:.6e}, IC = {ic_loss:.6e}")
205
+ self.lbfgs_losses.append(float(loss))
206
+
207
+ # L-BFGS optimization
208
+ try:
209
+ result = scipy.optimize.minimize(
210
+ fun=lambda w: self.lbfgs_loss_func(w, model, t_colloc_norm, t_0_norm,
211
+ E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i),
212
+ x0=initial_weights.astype(np.float64),
213
+ method='L-BFGS-B',
214
+ jac=True,
215
+ callback=callback_func,
216
+ options=lbfgs_options
217
+ )
218
+
219
+ self.set_weights(model, result.x)
220
+
221
+ print(f" L-BFGS completed: {result.message}")
222
+ print(f" L-BFGS iterations: {result.nit}")
223
+ print(f" Final L-BFGS loss: {result.fun:.6e}")
224
+
225
+ except Exception as e:
226
+ print(f" L-BFGS error: {e}")
227
+ print(" Using Adam weights as final solution")
228
+
229
+ print(f" L-BFGS training time: {time() - t_start_lbfgs:.2f}s")
230
+
231
+ # Final Evaluation
232
+ final_loss, final_pde_loss, final_ic_loss = self.total_loss(
233
+ model, t_colloc_norm, t_0_norm, E_norm, psi_st_tensor, HP_tensor, H_t_r, H_t_i
234
+ )
235
+ print(f" Final combined loss: {final_loss:.6e}")
236
+
237
+ return {
238
+ 'adam_history': adam_history,
239
+ 'lbfgs_losses': self.lbfgs_losses,
240
+ 'final_loss': float(final_loss),
241
+ 'final_pde_loss': float(final_pde_loss),
242
+ 'final_ic_loss': float(final_ic_loss)
243
+ }
@@ -0,0 +1,50 @@
1
+ # quantum_config.py
2
+ import numpy as np
3
+ from datetime import datetime
4
+
5
+ class QuantumConfig:
6
+ """Configuration of physical parameters and normalization"""
7
+
8
+ def __init__(self):
9
+ # Physical constants
10
+ self.e_charge = 1.602176634e-19
11
+ self.hbar = 6.582119514e-16
12
+ self.KB = 8.6173303e-5
13
+ self.pi = np.pi
14
+
15
+ # System parameters
16
+ self.n = 3
17
+ self.Ne = 20
18
+ self.E_min, self.E_max = -2.5, 2.5
19
+ self.Ef, self.Temp = 0.0, 10
20
+
21
+ # Hamiltonian
22
+ self.GammaL, self.GammaR = 0.05, 0.05
23
+ self.molE, self.tm = 0.7, 0.1
24
+
25
+ # Time
26
+ self.Power = 1.0e-15
27
+ self.t_min, self.t_max = 0, 100 * self.Power
28
+
29
+ self._setup_normalization()
30
+ self._record_simulation_time()
31
+
32
+ def _setup_normalization(self):
33
+ """Normalization of physical units"""
34
+ self.E_scale = abs(self.molE)
35
+ self.t_scale = self.hbar / self.E_scale
36
+ self.current_scale = self.e_charge / self.t_scale
37
+
38
+ # Normalized parameters
39
+ self.E_min_norm = self.E_min / self.E_scale
40
+ self.E_max_norm = self.E_max / self.E_scale
41
+ self.dE_norm = (self.E_max_norm - self.E_min_norm) / self.Ne
42
+ self.Ef_norm = self.Ef / self.E_scale
43
+ self.KBT_norm = (self.KB * self.Temp) / self.E_scale
44
+ self.t_min_norm = self.t_min / self.t_scale
45
+ self.t_max_norm = self.t_max / self.t_scale
46
+
47
+ def _record_simulation_time(self):
48
+ """Records the simulation start time"""
49
+ self.start_time = datetime.now()
50
+ self.simulation_id = self.start_time.strftime("%Y%m%d_%H%M%S")