Source code for pylcp.heuristiceq

import numpy as np
import copy
import time
import numba
from scipy.integrate import solve_ivp
from scipy.interpolate import interp1d
from .integration_tools import solve_ivp_random
from .common import (progressBar, random_vector, spherical_dot,
                     cart2spherical, spherical2cart)
from .common import base_force_profile as force_profile
from .governingeq import governingeq

[docs]class heuristiceq(governingeq): """ Heuristic force equation The heuristic equation governs the atom or molecule as if it has a single transition between an :math:`F=0` ground state to an :math:`F'=1` excited state. Parameters ---------- laserBeams : dictionary of pylcp.laserBeams, pylcp.laserBeams, or list of pylcp.laserBeam The laserBeams that will be used in constructing the optical Bloch equations. which transitions in the block diagonal hamiltonian. It can be any of the following: * A dictionary of pylcp.laserBeams: if this is the case, the keys of the dictionary should match available :math:`d^{nm}` matrices in the pylcp.hamiltonian object. The key structure should be `n->m`. Here, it must be `g->e`. * pylcp.laserBeams: a single set of laser beams is assumed to address the transition `g->e`. * a list of pylcp.laserBeam: automatically promoted to a pylcp.laserBeams object assumed to address the transtion `g->e`. magField : pylcp.magField or callable The function or object that defines the magnetic field. hamiltonian : pylcp.hamiltonian The internal hamiltonian of the particle. a : array_like, shape (3,), optional A default acceleraiton to apply to the particle's motion, usually gravity. Default: [0., 0., 0.] r0 : array_like, shape (3,), optional Initial position of the atom or molecule. Default: [0., 0., 0.] v0 : array_like, shape (3,), optional Initial velocity of the atom or molecule. Default: [0., 0., 0.] mass : float, optional Mass of the atom or molecule. Default: 100 gamma : float, optional Decay rate of the single transition in the atom or molecule. Default: 1 k : float, optional Magnitude of the k vector for the single transition in the atom or molecule. Default: 1 """ def __init__(self, laserBeams, magField, a=np.array([0., 0., 0.]), mass=100, gamma=1, k=1, r0=np.array([0., 0., 0.]), v0=np.array([0., 0., 0.])): super().__init__(laserBeams, magField, a=a, r0=r0, v0=v0) # Check to make sure the laserBeams dictionary has only one key: for key in self.laserBeams: if key != 'g->e': print(key) raise KeyError('laserBeam dictionary should only contain ' + 'a single key of \'g->e\' for the heutisticeq.') # Finally, handle optional arguments: self.mass = mass self.gamma = gamma self.k = k # Set up a dictionary to store any resulting force profiles. self.profile = {} # Reset the current solution to None self.sol = None # Make some variables to store F, F_laser, and R_sc: self.F = np.array([0., 0., 0.]) self.F_laser = {} self.F_laser['g->e'] = np.zeros((3, self.laserBeams['g->e'].num_of_beams)) self.R = np.zeros((self.laserBeams['g->e'].num_of_beams, ))
[docs] def scattering_rate(self, r, v, t, return_kvecs=False): """ Calculates the scattering rate Parameters ---------- r : array_like Position at which to calculate the force v : array_like Velocity at which to calculate the force t : float Time at which to calculate the force return_kvecs : bool If true, returns both the scattering rate and the k-vecotrs from the lasers. Returns ------- R : array_like Array of scattering rates associated with the lasers driving the transition. kvecs : array_like If return_kvecs is True, the k-vectors of each of the lasers. This is used in heuristiceq.force, where it calls this function to calculate the scattering rate first. By returning the k-vectors with the scattering rates, it prevents the need of having to recompute the k-vectors again. """ B = self.magField.Field(r, t) Bmag = np.linalg.norm(B) if Bmag==0: Bhat = np.array([0., 0., 1.]) else: Bhat = B/np.linalg.norm(B) kvecs = self.laserBeams['g->e'].kvec(r, t) intensities = self.laserBeams['g->e'].intensity(r, t) pols = self.laserBeams['g->e'].project_pol(Bhat, r, t) deltas = self.laserBeams['g->e'].delta(t) totintensity = np.sum(intensities) for ii, (kvec, intensity, pol, delta) in enumerate(zip(kvecs, intensities, pols, deltas)): self.R[ii] = 0. polsqrd = np.abs(pol)**2 for (q, pol_i) in zip(np.array([-1., 0., 1.]), polsqrd): self.R[ii] += self.gamma/2*intensity*pol_i/\ (1+ totintensity + 4*(delta - np.dot(kvec, v) - q*Bmag)**2/self.gamma**2) if return_kvecs: return self.R, kvecs else: return self.R
[docs] def force(self, r, v, t): """ Calculates the instantaneous force Parameters ---------- r : array_like Position at which to calculate the force v : array_like Velocity at which to calculate the force t : float Time at which to calculate the force Returns ------- F : array_like total equilibrium force experienced by the atom F_laser : dictionary of array_like If return_details is True, the forces due to each laser, indexed by the manifold the laser addresses. The dictionary is keyed by the transition driven, and individual lasers are in the same order as in the pylcp.laserBeams object used to create the governing equation. """ R, kvecs = self.scattering_rate(r, v, t, return_kvecs=True) self.F_laser['g->e'] = (kvecs*R[:, np.newaxis]).T self.F = np.sum(self.F_laser['g->e'], axis=1) return self.F, self.F_laser
[docs] def evolve_motion(self, t_span, freeze_axis=[False, False, False], random_recoil=False, random_force=False, max_scatter_probability=0.1, progress_bar=False, rng=np.random.default_rng(), **kwargs): """ Evolve the motion of the atom in time. Parameters ---------- t_span : list or array_like A two element list or array that specify the initial and final time of integration. freeze_axis : list of boolean Freeze atomic motion along the specified axis. Default: [False, False, False] random_recoil : boolean Allow the atom to randomly recoil from scattering events. Default: False random_force : boolean Rather than calculating the force using the heuristieq.force() method, use the calculated scattering rates from each of the laser beam to randomly add photon absorption events that cause the atom to recoil randomly from the laser beam(s). Default: False max_scatter_probability : float When undergoing random recoils, this sets the maximum time step such that the maximum scattering probability is less than or equal to this number during the next time step. Default: 0.1 progress_bar : boolean If true, show a progress bar as the calculation proceeds. Default: False rng : numpy.random.Generator() A properly-seeded random number generator. Default: calls ``numpy.random.default.rng()`` **kwargs : Additional keyword arguments get passed to solve_ivp_random, which is what actually does the integration. Returns ------- sol : OdeSolution Bunch object that contains the following fields: * t: integration times found by solve_ivp * v: atomic velocity * r: atomic position It contains other important elements, which can be discerned from scipy's solve_ivp documentation. """ free_axes = np.bitwise_not(freeze_axis) if progress_bar: progress = progressBar() def dydt(t, y): if progress_bar: progress.update(t/t_span[1]) F, Flaser = self.force(y[3:6], y[0:3], t) return np.concatenate(( 1/self.mass*F*free_axes + self.constant_accel, y[0:3] )) def dydt_random_force(t, y): if progress_bar: progress.update(t/t_span[1]) return np.concatenate((self.constant_accel, y[0:3])) def random_recoil_func(t, y, dt): num_of_scatters = 0 total_P = np.sum(self.R)*dt if rng.random(1)<total_P: y[0:3] += self.k/self.mass*(random_vector(rng, free_axes)+ random_vector(rng, free_axes)) num_of_scatters += 1 new_dt_max = (max_scatter_probability/total_P)*dt return (num_of_scatters, new_dt_max) def random_force_func(t, y, dt): R, kvecs = self.scattering_rate(y[3:6], y[0:3], t, return_kvecs=True) num_of_scatters = 0 for kvec in kvecs[np.random.rand(len(R))<R*dt]: y[-6:-3] += kvec/self.mass y[-6:-3] += self.k/self.mass*random_vector(free_axes) num_of_scatters += 1 total_P = np.sum(self.R)*dt new_dt_max = (max_scatter_probability/total_P)*dt return (num_of_scatters, new_dt_max) if random_force: self.sol = solve_ivp_random( dydt_random_force, random_force_func, t_span, np.concatenate((self.v0, self.r0)), initial_max_step=max_scatter_probability, **kwargs ) elif random_recoil: self.sol = solve_ivp_random( dydt, random_recoil_func, t_span, np.concatenate((self.v0, self.r0)), initial_max_step=max_scatter_probability, **kwargs ) else: self.sol = solve_ivp( dydt, t_span, np.concatenate((self.v0, self.r0)), **kwargs ) if progress_bar: # Just in case the solve_ivp_random terminated due to an event. progress.update(1.) self.sol.r = self.sol.y[3:] self.sol.v = self.sol.y[:3]
[docs] def find_equilibrium_force(self, return_details=False): """ Finds the equilibrium force at the initial position Parameters ---------- return_details : boolean, optional If True, returns the forces from each laser and the scattering rate matrix. Returns ------- F : array_like total equilibrium force experienced by the atom F_laser : array_like If return_details is True, the forces due to each laser. R : array_like The scattering rate matrix. """ F, F_laser = self.force(self.r0, self.v0, t=0.) if return_details: return F, F_laser, self.R else: return F
[docs] def generate_force_profile(self, R, V, name=None, progress_bar=False): """ Map out the equilibrium force vs. position and velocity Parameters ---------- R : array_like, shape(3, ...) Position vector. First dimension of the array must be length 3, and corresponds to :math:`x`, :math:`y`, and :math:`z` components, repsectively. V : array_like, shape(3, ...) Velocity vector. First dimension of the array must be length 3, and corresponds to :math:`v_x`, :math:`v_y`, and :math:`v_z` components, repsectively. name : str, optional Name for the profile. Stored in profile dictionary in this object. If None, uses the next integer, cast as a string, (i.e., '0') as the name. progress_bar : boolean, optional Displays a progress bar as the proceeds. Default: False Returns ------- profile : pylcp.common.base_force_profile Resulting force profile. """ if not name: name = '{0:d}'.format(len(self.profile)) self.profile[name] = force_profile(R, V, self.laserBeams, None) it = np.nditer([R[0], R[1], R[2], V[0], V[1], V[2]], flags=['refs_ok', 'multi_index'], op_flags=[['readonly'], ['readonly'], ['readonly'], ['readonly'], ['readonly'], ['readonly']]) if progress_bar: progress = progressBar() for (x, y, z, vx, vy, vz) in it: # Construct the rate equations: r = np.array([x, y, z]) v = np.array([vx, vy, vz]) F, F_laser = self.force(r, v, 0.) self.profile[name].store_data(it.multi_index, None, F, F_laser, np.zeros((3,))) if progress_bar: progress.update((it.iterindex+1)/it.itersize) return self.profile[name]