Source code for atomsmltr.simulation.simulator.stochastic

"""Home-made stochastic integrators
=========================================

Implements homemade integrators for stochastic systems, that is, taking into account
fluctuations due to photon scattering
"""

# % IMPORTS
import numpy as np
import scipy.constants as csts
from functools import partial

# % LOCAL IMPORTS
from .simbase import Simulation, SimRes, get_force_vec
from .deterministic import CustomSimulationBase
from ..configurator import Configuration


# % USEFUL FUNCTIONS
[docs] def random_unit_vector(shape=(1,)): """Generates a random unit vector Parameters ---------- shape : tuple, optional shape of the output will be (**shape, 3), by default (1,) Returns ------- vec : array the random unit vector """ # - get random phi and costheta rng = np.random.default_rng() phi = rng.uniform(low=0, high=2 * np.pi, size=shape) costheta = rng.uniform(low=-1, high=1, size=shape) sintheta = np.sqrt(1 - costheta**2) # - compute x, y, z x = sintheta * np.cos(phi) y = sintheta * np.sin(phi) z = costheta # - combine into an array of good shape vec = np.array([x.T, y.T, z.T]).T return vec
# % HOME-MADE SIMULATORS
[docs] class RK4St(CustomSimulationBase): """A homemade simulator based on fourth order Runge-Kutta method, taking into account spontaneous emission. Parameters ---------- config : Configuration, optional the configuration to consider for the simulation References ---------- https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods """ def __init__( self, config: Configuration = None, ): super(RK4St, self).__init__(config) self.rng = np.random.default_rng() def du_fluct(self, t, u, dt): _, scatt_list = get_force_vec(u, self.config, return_list=True) dv_tot = np.zeros_like(u[..., :3]) for scatt in scatt_list: # 0 - get scattering rate, laser wavenumber and unit vector for each laser rate = scatt["rate"] # scattering rate k = scatt["k"] # laser wavenumber u = scatt["unit_vector"] # laser unit vector Ni = rate * dt # number of scattered photons m = self.config.atom.mass # 1 - absorption fluctuation # large number of photon approximation # > fluctuation are Gaussian with std = np.sqrt(Ni) # note that dN has the same shape as Ni, and can be an array !! dN = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni))) dv_abs = (csts.hbar * k / m) * dN[..., np.newaxis] * u dv_tot = dv_tot + dv_abs # 2 - emission fluctuation # Gaussian approx for random walk, with std = sqrt(Ni/3) for x, y, z dNx = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni / 3))) dNy = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni / 3))) dNz = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni / 3))) dN = np.array([dNx.T, dNy.T, dNz.T]).T dv_em = (csts.hbar * k / m) * dN dv_tot = dv_tot + dv_em dx, dy, dz = np.zeros_like(dv_tot.T) dvx, dvy, dvz = dv_tot.T res = np.array([dx, dy, dz, dvx, dvy, dvz]).T return res def _iterate(self, t, u, dt): """returns the evolution du of u between t and t+dt Here we use the fourth order Runge-Kutta method """ # perform step # 1) deterministic part k1 = self.dudt(t, u) k2 = self.dudt(t + 0.5 * dt, u + 0.5 * k1 * dt) k3 = self.dudt(t + 0.5 * dt, u + 0.5 * k2 * dt) k4 = self.dudt(t + dt, u + k3 * dt) du = (dt / 6) * (k1 + 2 * k2 + 2 * k3 + k4) # 2) fluctating part du_fluct = self.du_fluct(t, u, dt) return du + du_fluct
[docs] class EulerSt(CustomSimulationBase): """A homemade simulator based on simple Euler integration method, taking into account spontaneous emission. Parameters ---------- config : Configuration, optional the configuration to consider for the simulation enable_fluct : Boolean, optional (default=True) if set to True, fluctations are enabled. Otherwise, the simulator boils down to a deterministic Euler simulator References ---------- TODO: put here """ def __init__( self, config: Configuration = None, enable_fluct: bool = True, ): super(EulerSt, self).__init__(config) self.enable_fluct = enable_fluct self.rng = np.random.default_rng() def du_fluct(self, t, u, dt): _, scatt_list = get_force_vec(u, self.config, return_list=True) dv_tot = np.zeros_like(u[..., :3]) for scatt in scatt_list: # 0 - get scattering rate, laser wavenumber and unit vector for each laser rate = scatt["rate"] # scattering rate k = scatt["k"] # laser wavenumber u = scatt["unit_vector"] # laser unit vector Ni = rate * dt # number of scattered photons m = self.config.atom.mass # 1 - absorption fluctuation # large number of photon approximation # > fluctuation are Gaussian with std = np.sqrt(Ni) # note that dN has the same shape as Ni, and can be an array !! dN = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni))) dv_abs = (csts.hbar * k / m) * dN[..., np.newaxis] * u dv_tot = dv_tot + dv_abs # 2 - emission fluctuation # Gaussian approx for random walk, with std = sqrt(Ni/3) for x, y, z dNx = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni / 3))) dNy = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni / 3))) dNz = np.asanyarray(self.rng.normal(loc=0, scale=np.sqrt(Ni / 3))) dN = np.array([dNx.T, dNy.T, dNz.T]).T dv_em = (csts.hbar * k / m) * dN dv_tot = dv_tot + dv_em dx, dy, dz = np.zeros_like(dv_tot.T) dvx, dvy, dvz = dv_tot.T res = np.array([dx, dy, dz, dvx, dvy, dvz]).T return res def _iterate(self, t, u, dt): """returns the evolution du of u between t and t+dt Here we use the fourth order Runge-Kutta method """ # perform step # 1) deterministic part F = self.get_force(u) _, _, _, vx, vy, vz = u.T dvx, dvy, dvz = F.T / self.config.atom.mass * dt dx = vx * dt + 0.5 * dvx * dt dy = vy * dt + 0.5 * dvy * dt dz = vz * dt + 0.5 * dvz * dt du = np.array([dx, dy, dz, dvx, dvy, dvz]).T # 2) fluctating part if self.enable_fluct: du += self.du_fluct(t, u, dt) return du