Skip to main content

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}