numerics
Loading...
Searching...
No Matches
tdse_solver.cpp
Go to the documentation of this file.
1/// @file tdse_solver.cpp
2/// @brief Implementation of TDSESolver -- 2-D TDSE via Strang splitting + Thomas algorithm.
3///
4/// Algorithm chain per step():
5/// 1. kick_V(dt/2) -- diagonal phase multiplication
6/// 2. sweep_x(dt/2) -- Crank-Nicolson in x (Thomas per column fiber)
7/// 3. sweep_y(dt) -- Crank-Nicolson in y (Thomas per row fiber)
8/// 4. sweep_x(dt/2) -- again
9/// 5. kick_V(dt/2) -- again
10///
11/// Eigenstates: num::lanczos on the real N^2xN^2 Hamiltonian (matrix-free matvec).
12/// Bessel zeros: num::brent on J_m(x) for exact CircularWell eigenvalues.
13
14#include "tdse_solver.hpp"
15
17#include "analysis/roots.hpp"
18#include "core/util/math.hpp"
19
20#include <chrono>
21#include <stdexcept>
22#include <cmath>
23
24namespace tdse {
25
26using cplx = std::complex<double>;
27
28
29TDSESolver::TDSESolver(int N_, double L_, double dt_)
30 : N(N_), L(L_), h(L_ / static_cast<double>(N_ + 1)), dt(dt_),
31 psi(static_cast<num::idx>(N_ * N_), cplx(0.0, 0.0)),
32 V(static_cast<num::idx>(N_ * N_), 0.0)
33{
34 refactor_();
35 init_gaussian(L_ * 0.25, L_ * 0.5, 4.0, 0.0, L_ * 0.07);
36}
37
38void TDSESolver::refactor_() {
39 adi_ = num::CrankNicolsonADI(N, dt, h);
40}
41
42// Potential setup
43
44void TDSESolver::set_potential(Potential p, double param) {
45 current_potential_ = p;
46 num::scale(V, 0.0); // zero the potential before rebuilding
47
48 const double cx = L * 0.5, cy = L * 0.5;
49 const double V0 = 5000.0; // hard wall height
50
51 if (p == Potential::Barrier) {
52 // Vertical wall at x = L*0.55, gap centred at y = L/2, width = L*0.12
53 double wall_x = L * 0.55;
54 double gap_cy = cy;
55 double gap_w = L * 0.12;
56 int wall_th = std::max(1, N / 60); // ~4 cells thick
57
58 int ix0 = static_cast<int>((wall_x / L) * N - wall_th);
59 int ix1 = static_cast<int>((wall_x / L) * N + wall_th);
60
61 for (int i = ix0; i <= std::min(ix1, N-1); ++i) {
62 for (int j = 0; j < N; ++j) {
63 double y = (j + 1.0) * h;
64 if (std::abs(y - gap_cy) > gap_w * 0.5)
65 V[at(i, j)] = V0;
66 }
67 }
68 }
69 else if (p == Potential::DoubleSlit) {
70 double wall_x = L * 0.5;
71 double gap_sep = L * 0.12; // centre-to-centre separation
72 double gap_w = L * 0.06; // each slit width
73 int wall_th = std::max(1, N / 60);
74
75 int ix0 = static_cast<int>((wall_x / L) * N - wall_th);
76 int ix1 = static_cast<int>((wall_x / L) * N + wall_th);
77
78 for (int i = ix0; i <= std::min(ix1, N-1); ++i) {
79 for (int j = 0; j < N; ++j) {
80 double y = (j + 1.0) * h;
81 bool in_slit1 = std::abs(y - (cy - gap_sep * 0.5)) < gap_w * 0.5;
82 bool in_slit2 = std::abs(y - (cy + gap_sep * 0.5)) < gap_w * 0.5;
83 if (!in_slit1 && !in_slit2)
84 V[at(i, j)] = V0;
85 }
86 }
87 }
88 else if (p == Potential::Harmonic) {
89 double omega = (param > 0) ? param : 1.5;
90 for (int i = 0; i < N; ++i) {
91 double x = (i + 1.0) * h - cx;
92 for (int j = 0; j < N; ++j) {
93 double y = (j + 1.0) * h - cy;
94 V[at(i, j)] = 0.5 * omega * omega * (x*x + y*y);
95 }
96 }
97 }
98 else if (p == Potential::CircularWell) {
99 double R = (param > 0) ? param : L * 0.4;
100 for (int i = 0; i < N; ++i) {
101 double x = (i + 1.0) * h - cx;
102 for (int j = 0; j < N; ++j) {
103 double y = (j + 1.0) * h - cy;
104 if (std::sqrt(x*x + y*y) > R)
105 V[at(i, j)] = V0;
106 }
107 }
108 }
109 // Free: all zeros already set
110
111 modes_ready = false;
112}
113
114// Initial conditions
115
116void TDSESolver::init_gaussian(double x0, double y0, double kx, double ky, double sigma) {
117 for (int i = 0; i < N; ++i) {
118 double x = (i + 1.0) * h;
119 for (int j = 0; j < N; ++j) {
120 double y = (j + 1.0) * h;
121 double dx = x - x0, dy = y - y0;
122 double env = std::exp(-(dx*dx + dy*dy) / (2.0 * sigma * sigma));
123 double phi = kx * dx + ky * dy;
124 psi[at(i, j)] = env * cplx(std::cos(phi), std::sin(phi));
125 }
126 }
127 renormalize();
128}
129
131 if (!modes_ready || k < 0 || k >= static_cast<int>(modes.size()))
132 throw std::runtime_error("tdse: modes not ready or k out of range");
133 const auto& phi = modes[k].phi;
134 for (int n2 = 0; n2 < N * N; ++n2)
135 psi[n2] = cplx(phi[n2], 0.0);
136 // already normalised
137}
138
139// Strang splitting sub-steps
140
141void TDSESolver::kick_V(double tau) {
142 for (int n2 = 0; n2 < N * N; ++n2) {
143 double angle = -V[n2] * tau;
144 psi[n2] *= cplx(std::cos(angle), std::sin(angle));
145 }
146}
147
148void TDSESolver::sweep_x(double tau) { adi_.sweep(psi, true, tau); }
149void TDSESolver::sweep_y(double tau) { adi_.sweep(psi, false, tau); }
150
151// step() -- Strang splitting
152
154 auto t0 = std::chrono::high_resolution_clock::now();
155
156 kick_V(dt * 0.5);
157 sweep_x(dt * 0.5);
158 sweep_y(dt);
159 sweep_x(dt * 0.5);
160 kick_V(dt * 0.5);
161
162 auto t1 = std::chrono::high_resolution_clock::now();
163 stats.step_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
164}
165
166// Observables
167
169 // ||psi||_L2 = num::norm(psi) * h (rectangle rule on uniform grid)
170 return num::norm(psi) * h;
171}
172
174 // <H> = <psi | -1/(2h^2) * Lap + V | psi> * h^2
175 // laplacian_stencil_2d gives lap[k] = (Σ4 nbrs) - 4*psi[k] (Dirichlet)
176 num::CVector lap(psi.size());
178
179 const double c = 1.0 / (2.0 * h * h);
180 double E = 0.0;
181 for (int k = 0; k < N * N; ++k) {
182 cplx Hpsi = -c * lap[k] + cplx(V[k]) * psi[k];
183 E += psi[k].real() * Hpsi.real() + psi[k].imag() * Hpsi.imag();
184 }
185 return E * h * h;
186}
187
189 double inv = 1.0 / (num::norm(psi) * h);
190 num::scale(psi, cplx{inv, 0.0});
191}
192
193// Eigenstate computation -- Lanczos on the real Hamiltonian
194
196 // H is real symmetric -> eigenstates are real. Work entirely in real arithmetic.
197 const int n2 = N * N;
198 const double c = 1.0 / (2.0 * h * h);
199
200 // Matrix-free Hamiltonian matvec: H = -c * Lap + V (Dirichlet)
201 num::Vector lap_buf(n2);
202 auto ham_mv = [&](const num::Vector& x, num::Vector& y) {
203 num::laplacian_stencil_2d(x, lap_buf, N);
204 for (int i = 0; i < n2; ++i)
205 y[i] = -c * lap_buf[i] + V[i] * x[i];
206 };
207
208 num::idx max_steps = std::min(static_cast<num::idx>(5 * k),
209 static_cast<num::idx>(n2));
210 auto res = num::lanczos(ham_mv, static_cast<num::idx>(n2), k, 1e-8, max_steps);
211
212 modes.clear();
213 int got = static_cast<int>(res.ritz_values.size());
214 modes.reserve(got);
215
216 for (int m = 0; m < got; ++m) {
217 EigenMode em;
218 em.energy = res.ritz_values[m];
219 em.phi.resize(n2);
220
221 double norm_sq = 0;
222 for (int r = 0; r < n2; ++r) {
223 em.phi[r] = res.ritz_vectors(r, m);
224 norm_sq += em.phi[r] * em.phi[r];
225 }
226 double inv = 1.0 / std::sqrt(norm_sq * h * h);
227 for (double& v : em.phi) v *= inv;
228
229 modes.push_back(std::move(em));
230 }
231
232 if (current_potential_ == Potential::CircularWell) {
233 double R = L * 0.4; // default radius (matches set_potential)
234 // Exact energies: E_{m,n} = j_{m,n}^2 / (2R^2)
235 int mode_idx = 0;
236 for (int m = 0; m <= 2 && mode_idx < got; ++m) {
237 double bracket_lo = 1.0;
238 for (int nz = 1; nz <= 3 && mode_idx < got; ++nz) {
239 auto Jm = [m](num::real x) { return num::bessel_j(m, x); };
240 double lo = bracket_lo, hi = lo + 0.5;
241 int scan = 0;
242 while (Jm(lo) * Jm(hi) > 0 && scan < 200) { lo = hi; hi += 0.5; ++scan; }
243 if (Jm(lo) * Jm(hi) > 0) break;
244
245 auto res2 = num::brent(Jm, lo, hi, 1e-10);
246 double E_exact = (res2.root * res2.root) / (2.0 * R * R);
247 bracket_lo = hi + 0.01;
248
249 modes[mode_idx++].exact_energy = E_exact;
250 if (m > 0 && mode_idx < got)
251 modes[mode_idx++].exact_energy = E_exact;
252 }
253 }
254 }
255
256 stats.n_modes = got;
257 modes_ready = true;
258}
259
260} // namespace tdse
Dense vector with optional GPU storage, templated over scalar type T.
Definition vector.hpp:23
constexpr idx size() const noexcept
Definition vector.hpp:77
double dt
Time step.
int N
Interior grid points per axis.
num::Vector V
NxN potential (real, length N^2)
double L
Domain length ([0,L]x[0,L])
double compute_energy() const
<psi|H|psi> using 5-point Laplacian and the potential.
void set_mode(int k)
Set psi to the k-th eigenmode (modes must be computed first).
TDSESolver(int N, double L, double dt)
void init_gaussian(double x0, double y0, double kx, double ky, double sigma)
Gaussian wavepacket: psi = A*exp(-(r-r0)^2/sigma^2)*exp(i*k*r), then normalised.
std::vector< EigenMode > modes
void compute_modes(int k=8)
double h
Grid spacing = L/(N+1)
num::CVector psi
NxN wavefunction (complex, length N^2)
void set_potential(Potential p, double param=0.0)
Build potential and recompute Thomas factorisations.
double compute_norm() const
integral|psi|^2 dA (Gauss-Legendre over each grid strip).
int at(int i, int j) const
void step()
Advance one full time step (Strang splitting).
Lanczos algorithm – k extreme eigenvalues of a large sparse matrix.
Thin wrappers around C++17 <cmath> and <numeric> with readable names.
void laplacian_stencil_2d(const BasicVector< T > &x, BasicVector< T > &y, int N)
Definition stencil.hpp:32
double real
Definition types.hpp:10
real bessel_j(real nu, real x)
J_nu(x) – Bessel function of the first kind.
Definition math.hpp:53
RootResult brent(ScalarFn f, real a, real b, real tol=1e-10, idx max_iter=1000)
Brent's method (bisection + secant + inverse quadratic interpolation)
Definition roots.cpp:54
std::size_t idx
Definition types.hpp:11
void scale(Vector &v, real alpha, Backend b=default_backend)
v *= alpha
Definition vector.cpp:27
real norm(const Vector &x, Backend b=default_backend)
Euclidean norm.
Definition vector.cpp:69
LanczosResult lanczos(MatVecFn matvec, idx n, idx k, real tol=1e-10, idx max_steps=0, Backend backend=Backend::seq)
Lanczos eigensolver for large sparse symmetric matrices.
Definition lanczos.cpp:30
@ DoubleSlit
Double-slit wall – interference.
@ Barrier
Single vertical barrier with one gap – tunnelling.
@ CircularWell
V = 0 inside radius R, V = V0 outside (Bessel eigenstates)
@ Harmonic
V = 1/2*omega^2*r^2 (coherent state dynamics)
std::complex< double > cplx
Root-finding methods for scalar equations f(x) = 0.
void sweep(CVector &psi, bool x_axis, double tau) const
Definition adi.hpp:66
std::vector< double > phi
NxN real wavefunction, L^2-normalised.
double energy
Ritz eigenvalue (~= energy)
int n_modes
Number of computed eigenmodes.
double step_ms
Wall time for one step()
2-D Time-Dependent Schrödinger Equation solver