embedded_schrodinger/lib.rs
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (C) 2025 Jorge Andre Castro
3//
4// This program is free software; you can redistribute it and/or modify it
5// under the terms of the GNU General Public License version 2 as published
6// by the Free Software Foundation.
7//
8// This program is distributed in the hope that it will be useful, but
9// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11// for more details.
12
13#![no_std]
14#![forbid(unsafe_code)]
15#![doc = include_str!("../README.md")]
16
17use embedded_f32_sqrt::sqrt;
18
19/// Solveur de l'équation de Schrödinger **indépendante du temps en 1D**.
20///
21/// La résolution est basée sur la **méthode de tir** (*shooting method*)
22/// combinée à une dichotomie binaire de 30 itérations.
23///
24/// L'implémentation est entièrement `no_std`, sans `libm` ni liaison C.
25/// Toute opération de racine carrée est déléguée à
26/// [`embedded-f32-sqrt`](https://docs.rs/embedded-f32-sqrt), qui utilise
27/// l'algorithme de Newton-Raphson sur `f32` IEEE 754.
28///
29/// # Paramètre générique
30///
31/// `N` : nombre de points de la grille spatiale (doit être ≥ 3).
32///
33/// # Exemple
34///
35/// ```rust
36/// use embedded_schrodinger::SchrodingerSolver;
37///
38/// const N: usize = 100;
39///
40/// // Puits de potentiel infini (V = 0 à l'intérieur)
41/// let potential = [0.0f32; N];
42///
43/// // Unités atomiques : ħ = 1, m = 1, dx = 0.01
44/// let solver = SchrodingerSolver::new(potential, 0.01, 1.0, 1.0);
45///
46/// let mut psi = [0.0f32; N];
47/// let energy = solver.find_eigenstate(0.1, 20.0, &mut psi)
48/// .expect("Pas de solution trouvée dans l'intervalle donné");
49///
50/// // En unités atomiques, E₁ ≈ π²/2 ≈ 4.935 pour L = 1 (N·dx = 1.0)
51/// assert!((energy - 4.935).abs() < 0.5);
52/// ```
53pub struct SchrodingerSolver<const N: usize> {
54 /// Tableau du potentiel discret `V(xᵢ)` sur la grille spatiale.
55 pub potential: [f32; N],
56 /// Pas spatial `dx` (en mètres ou en unités réduites selon le système).
57 pub dx: f32,
58 /// Constante de Planck réduite `ħ` (en J·s ou unités réduites).
59 pub hbar: f32,
60 /// Masse de la particule `m` (en kg ou unités réduites).
61 pub mass: f32,
62}
63
64impl<const N: usize> SchrodingerSolver<N> {
65 /// Crée un nouveau solveur.
66 ///
67 /// # Arguments
68 ///
69 /// - `potential` — valeurs de `V(xᵢ)` aux `N` points de la grille.
70 /// - `dx` — pas spatial uniforme.
71 /// - `hbar` — constante de Planck réduite `ħ`.
72 /// - `mass` — masse de la particule.
73 ///
74 /// # Exemple
75 ///
76 /// ```rust
77 /// use embedded_schrodinger::SchrodingerSolver;
78 /// let solver = SchrodingerSolver::<64>::new([0.0f32; 64], 0.01, 1.0, 1.0);
79 /// ```
80 #[inline]
81 pub fn new(potential: [f32; N], dx: f32, hbar: f32, mass: f32) -> Self {
82 Self { potential, dx, hbar, mass }
83 }
84
85 /// Intègre la fonction d'onde de **gauche à droite** pour une énergie `energy` donnée.
86 ///
87 /// Applique la relation de récurrence issue de la discrétisation de l'équation
88 /// de Schrödinger par **différences finies centrées** :
89 ///
90 /// ```text
91 /// ψᵢ₊₁ = 2ψᵢ − ψᵢ₋₁ − (2m·dx²/ħ²)(E − V(xᵢ))ψᵢ
92 /// ```
93 ///
94 /// Les conditions aux limites initiales sont celles d'un **puits infini** :
95 /// `ψ(0) = 0`, `ψ(1) = 0.001` (impulsion de départ arbitraire).
96 ///
97 /// # Valeur de retour
98 ///
99 /// Valeur de `ψ` au dernier point de la grille (`ψ[N-1]`).
100 /// Si la propagation diverge (NaN ou infini), la dernière valeur saine est renvoyée.
101 pub fn integrate_wavefunction(&self, energy: f32, psi: &mut [f32; N]) -> f32 {
102 psi[0] = 0.0;
103 psi[1] = 0.001;
104
105 // Facteur cinétique : (2·m·dx²) / ħ²
106 let k_factor = (2.0 * self.mass * self.dx * self.dx) / (self.hbar * self.hbar);
107
108 for i in 1..(N - 1) {
109 psi[i + 1] = 2.0 * psi[i]
110 - psi[i - 1]
111 - k_factor * (energy - self.potential[i]) * psi[i];
112
113 // Protection anti-divergence : évite de corrompre le calcul sur MCU
114 if psi[i + 1].is_infinite() || psi[i + 1].is_nan() {
115 return psi[i];
116 }
117 }
118
119 psi[N - 1]
120 }
121
122 /// Normalise la fonction d'onde `ψ` selon la condition `∫|ψ|² dx = 1`.
123 ///
124 /// L'intégration numérique est effectuée par la **méthode des rectangles**.
125 /// La racine carrée est calculée via
126 /// [`embedded_f32_sqrt::sqrt`] (Newton-Raphson, aucun recours à `libm`).
127 ///
128 /// # Erreurs
129 ///
130 /// - `"Erreur mathématique lors du calcul de la norme (NaN/Infinity)"` si
131 /// la somme produit une valeur non-numérique.
132 /// - `"Impossible de normaliser une fonction d'onde de norme nulle"` si la
133 /// norme calculée est nulle.
134 pub fn normalize(&self, psi: &mut [f32; N]) -> Result<(), &'static str> {
135 let mut sum = 0.0f32;
136
137 for &val in psi.iter() {
138 sum += val * val * self.dx;
139 }
140
141 let norm = sqrt(sum)
142 .map_err(|_| "Erreur mathématique lors du calcul de la norme (NaN/Infinity)")?;
143
144 if norm > 0.0 {
145 for val in psi.iter_mut() {
146 *val /= norm;
147 }
148 Ok(())
149 } else {
150 Err("Impossible de normaliser une fonction d'onde de norme nulle")
151 }
152 }
153
154 /// Recherche un **état propre** (énergie + fonction d'onde) par la **méthode de tir**.
155 ///
156 /// L'algorithme effectue une dichotomie binaire sur l'intervalle `[e_min, e_max]`.
157 /// À chaque itération, il intègre `ψ` et analyse la valeur frontière droite pour
158 /// déterminer de quel côté se trouve la valeur propre recherchée.
159 ///
160 /// 30 itérations donnent une précision de l'ordre de `2⁻³⁰` de l'intervalle
161 /// initial, soit la précision machine du type `f32`.
162 ///
163 /// # Arguments
164 ///
165 /// - `e_min` — borne basse de la plage de recherche (unité cohérente avec `hbar`/`mass`).
166 /// - `e_max` — borne haute de la plage de recherche.
167 /// - `psi` — tampon mutable où sera écrite la fonction d'onde normalisée.
168 ///
169 /// # Valeur de retour
170 ///
171 /// - `Ok(energy)` — énergie propre convergée en unité choisie.
172 /// - `Err(msg)` — si la normalisation finale échoue.
173 ///
174 /// # Exemple
175 ///
176 /// ```rust
177 /// use embedded_schrodinger::SchrodingerSolver;
178 ///
179 /// const N: usize = 200;
180 /// let solver = SchrodingerSolver::new([0.0f32; N], 0.005, 1.0, 1.0);
181 /// let mut psi = [0.0f32; N];
182 ///
183 /// let e1 = solver.find_eigenstate(1.0, 10.0, &mut psi).unwrap();
184 /// // En unités atomiques, E₁ ≈ π²/2 ≈ 4.935
185 /// assert!((e1 - 4.935).abs() < 0.3);
186 /// ```
187 pub fn find_eigenstate(
188 &self,
189 mut e_min: f32,
190 mut e_max: f32,
191 psi: &mut [f32; N],
192 ) -> Result<f32, &'static str> {
193 let mut e_mid = 0.0f32;
194
195 for _ in 0..30 {
196 e_mid = 0.5 * (e_min + e_max);
197 let psi_boundary = self.integrate_wavefunction(e_mid, psi);
198
199 // CORRECTION : logique de bissection corrigée.
200 // ψ_boundary > 0 signifie qu'on est en-dessous de la valeur propre,
201 // donc on relève e_min (et non e_max comme c'était le cas avant).
202 if psi_boundary > 0.0 {
203 e_min = e_mid;
204 } else {
205 e_max = e_mid;
206 }
207 }
208
209 self.normalize(psi)?;
210
211 Ok(e_mid)
212 }
213}
214
215// ─────────────────────────────────────────────────────────────────────────────
216// Tests unitaires
217// ─────────────────────────────────────────────────────────────────────────────
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 /// En unités atomiques (ħ = 1, m = 1), dans un puits infini de largeur L,
224 /// l'énergie du niveau fondamental est :
225 /// E₁ = π² / (2 L²)
226 ///
227 /// Pour L = 1.0 (N·dx = 1.0) : E₁ ≈ 4.9348
228 #[test]
229 fn test_energie_fondamentale_puits_infini() {
230 const N: usize = 200;
231 let dx = 1.0 / N as f32;
232 let solver = SchrodingerSolver::new([0.0f32; N], dx, 1.0, 1.0);
233 let mut psi = [0.0f32; N];
234
235 let energy = solver
236 .find_eigenstate(1.0, 15.0, &mut psi)
237 .expect("find_eigenstate a échoué");
238
239 // Référence analytique : π²/2 ≈ 4.9348
240 let e_ref = core::f32::consts::PI * core::f32::consts::PI / 2.0;
241 assert!(
242 (energy - e_ref).abs() < 0.5,
243 "Énergie fondamentale hors tolérance : {energy} vs {e_ref}"
244 );
245 }
246
247 /// La norme de la fonction d'onde normalisée doit être égale à 1
248 /// (à la précision numérique de l'intégration rectangulaire).
249 #[test]
250 fn test_normalisation_norme_unitaire() {
251 const N: usize = 200;
252 let dx = 1.0 / N as f32;
253 let solver = SchrodingerSolver::new([0.0f32; N], dx, 1.0, 1.0);
254 let mut psi = [0.0f32; N];
255
256 solver
257 .find_eigenstate(1.0, 15.0, &mut psi)
258 .expect("find_eigenstate a échoué");
259
260 let norm_sq: f32 = psi.iter().map(|&v| v * v * dx).sum();
261 assert!(
262 (norm_sq - 1.0).abs() < 1e-3,
263 "La norme au carré devrait être ≈ 1, obtenu : {norm_sq}"
264 );
265 }
266
267 /// Un potentiel constant non nul décale l'énergie propre.
268 /// Pour V₀ = 1.0, E₁_décalée ≈ E₁_libre + V₀.
269 #[test]
270 fn test_potentiel_constant_decalage_energie() {
271 const N: usize = 200;
272 let dx = 1.0 / N as f32;
273
274 let solver_libre = SchrodingerSolver::new([0.0f32; N], dx, 1.0, 1.0);
275 let solver_offset = SchrodingerSolver::new([1.0f32; N], dx, 1.0, 1.0);
276
277 let mut psi = [0.0f32; N];
278
279 let e_libre = solver_libre
280 .find_eigenstate(1.0, 15.0, &mut psi)
281 .expect("Solver libre a échoué");
282
283 let e_offset = solver_offset
284 .find_eigenstate(2.0, 16.0, &mut psi)
285 .expect("Solver décalé a échoué");
286
287 assert!(
288 (e_offset - e_libre - 1.0).abs() < 0.5,
289 "Décalage en énergie incorrect : {e_offset} - {e_libre} ≠ 1.0"
290 );
291 }
292
293 /// Vérifie que la normalisation renvoie une erreur sur un tableau nul.
294 #[test]
295 fn test_normalisation_vecteur_nul_retourne_erreur() {
296 const N: usize = 50;
297 let solver = SchrodingerSolver::new([0.0f32; N], 0.01, 1.0, 1.0);
298 let mut psi = [0.0f32; N];
299
300 let result = solver.normalize(&mut psi);
301 assert!(result.is_err(), "Devrait échouer sur une fonction d'onde nulle");
302 }
303
304 /// Vérifie que `integrate_wavefunction` retourne une valeur finie
305 /// pour une énergie physiquement raisonnable.
306 #[test]
307 fn test_integration_produit_valeur_finie() {
308 const N: usize = 100;
309 let dx = 0.01;
310 let solver = SchrodingerSolver::new([0.0f32; N], dx, 1.0, 1.0);
311 let mut psi = [0.0f32; N];
312
313 let boundary = solver.integrate_wavefunction(5.0, &mut psi);
314 assert!(
315 boundary.is_finite(),
316 "La valeur frontière doit être finie, obtenu : {boundary}"
317 );
318 }
319}