Source code for pylcp.rateeq

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Tools for solving the rate equations.
"""
import numpy as np
import copy
from scipy.optimize import minimize
from scipy.integrate import solve_ivp
from inspect import signature
from .fields import laserBeams, magField
from .common import (progressBar, random_vector, spherical_dot,
                     cart2spherical, spherical2cart, base_force_profile)
from .governingeq import governingeq
from .integration_tools import solve_ivp_random
from scipy.interpolate import interp1d

#@numba.vectorize([numba.float64(numba.complex128),numba.float32(numba.complex64)])
def abs2(x):
    return x.real**2 + x.imag**2

[docs]class force_profile(base_force_profile): """ Rate equation force profile The force profile object stores all of the calculated quantities created by the rateeq.generate_force_profile() method. It has following attributes: Attributes ---------- R : array_like, shape (3, ...) Positions at which the force profile was calculated. V : array_like, shape (3, ...) Velocities at which the force profile was calculated. F : array_like, shape (3, ...) Total equilibrium force at position R and velocity V. f_mag : array_like, shape (3, ...) Magnetic force at position R and velocity V. f : dictionary of array_like 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. Neq : array_like Equilibrium population found. Rijl : dictionary of array_like The pumping rates of 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. """ def __init__(self, R, V, laserBeams, hamiltonian): super().__init__(R, V, laserBeams, hamiltonian) # Add in the specific instance of the Rijl: self.Rijl = {} for key in laserBeams: self.Rijl[key] = np.zeros( self.R[0].shape+(len(laserBeams[key].beam_vector), hamiltonian.blocks[hamiltonian.laser_keys[key]].n, hamiltonian.blocks[hamiltonian.laser_keys[key]].m) ) def store_data(self, ind, Neq, F, F_laser, F_mag, Rijl): super().store_data(ind, Neq, F, F_laser, F_mag) for key in Rijl: self.Rijl[key][ind] = Rijl[key]
[docs]class rateeq(governingeq): """ The rate equations This class constructs the rate equations from the given laser beams, magnetic field, and hamiltonian. 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`. * 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.] include_mag_forces : boolean Optional flag to inculde magnetic forces in the force calculation. Default: True r0 : array_like, shape (3,), optional Initial position. Default: [0., 0., 0.] v0 : array_like, shape (3,), optional Initial velocity. Default: [0., 0., 0.] """ def __init__(self, laserBeams, magField, hamitlonian, a=np.array([0., 0., 0.]), include_mag_forces=True, svd_eps=1e-10, r0=np.array([0., 0., 0.]), v0=np.array([0., 0., 0.])): # First step is to save the imported laserBeams, magField, and # hamiltonian. super().__init__(laserBeams, magField, hamitlonian, a=a, r0=r0, v0=v0) self.include_mag_forces = include_mag_forces self.svd_eps = svd_eps # Check function signatures for any time dependence: self.tdepend = {} self.tdepend['B'] = False """self.tdepend['pol'] = False self.tdepend['kvec'] = False self.tdepend['det'] = False self.tdepend['intensity'] = False""" """if 't' in str(signature(self.magField)): self.tdepend['B'] = True for key in self.laserBeams: for beam in self.laserBeams[key]: if not beam.pol_sig is None and 't' in beam.pol_sig: self.tdepend['pol'] = True if not beam.kvec_sig is None and 't' in beam.kvec_sig: self.tdepend['kvec'] = True if not beam.delta_sig is None and 't' in beam.delta_sig: self.tdepend['det'] = True if not beam.intensity_sig is None and 't' in beam.intensity_sig: self.tdepend['intensity'] = True""" # Set up two dictionaries that are useful for both random forces and # random recoils: self.decay_rates = {} self.decay_N_indices = {} # If the matrix is diagonal, we get to do something cheeky. Let's just # construct the decay part of the evolution once: if np.all(self.hamiltonian.diagonal): self._calc_decay_comp_of_Rev(self.hamiltonian) # Save the recoil velocity for the relevant transitions: self.recoil_velocity = {} for key in self.hamiltonian.laser_keys: self.recoil_velocity[key] = \ self.hamiltonian.blocks[self.hamiltonian.laser_keys[key]].parameters['k']\ /self.hamiltonian.mass # A dictionary to store the pumping rates. self.Rijl = {} # Set up a dictionary to store the profiles. self.profile = {} def _calc_decay_comp_of_Rev(self, rotated_ham): """ Constructs the decay portion of the evolution matrix. Parameters ---------- rotated_ham: pylcp.hamiltonian object The diagonalized hamiltonian with rotated d_q matrices """ self.Rev_decay = np.zeros((self.hamiltonian.n, self.hamiltonian.n)) # Go through each of the blocks and calculate the decay rate # contributions to the rate equations. This would be simpler with # rotated_ham.make_full_matrices(), but I would imagine it would # be significantly slower. # Let's use the laser_keys dictionary to go through the non-zero d_q: for key in self.hamiltonian.laser_keys: # The index of the current d_q matrix: ind = rotated_ham.laser_keys[key] # Grab the block of interest: d_q_block = rotated_ham.blocks[ind] # The offset index for the lower states: noff = int(np.sum(rotated_ham.ns[:ind[0]])) # The offset index for the higher states: moff = int(np.sum(rotated_ham.ns[:ind[1]])) # The number of lower states: n = rotated_ham.ns[ind[0]] # The number of higher states: m = rotated_ham.ns[ind[1]] # Calculate the decay of the states attached to this block: gamma = d_q_block.parameters['gamma'] k = d_q_block.parameters['k'] # Let's see if we can avoid a for loop here: # for jj in range(rotated_ham.ns[ll]): # self.Rev_decay[n+jj, n+jj] -= gamma*\ # np.sum(np.sum(abs2( # other_block.matrix[:, :, jj] # ))) # Save the decay rates out of the excited state: self.decay_rates[key] = gamma*np.sum(abs2( d_q_block.matrix[:, :, :] ), axis=(0,1)) # Save the indices for the excited states of this d_q block # for the random_recoil function: self.decay_N_indices[key] = np.arange(moff, moff+m) # Add (more accurately, subtract) these decays to the evolution matrix: self.Rev_decay[(np.arange(moff, moff+m), np.arange(moff, moff+m))] -= \ self.decay_rates[key] # Now calculate the decays into the lower state connected by this # d_q: self.Rev_decay[noff:noff+n, moff:moff+m] += \ gamma*np.sum(abs2(d_q_block.matrix), axis=0) return self.Rev_decay def _calc_pumping_rates(self, r, v, t, Bhat): """ This method calculates the pumping rates for each laser beam: """ for key in self.laserBeams: # Extract the relevant d_q matrix: ind = self.hamiltonian.rotated_hamiltonian.laser_keys[key] d_q = self.hamiltonian.rotated_hamiltonian.blocks[ind].matrix gamma = self.hamiltonian.blocks[ind].parameters['gamma'] # Extract the energies: E1 = np.diag(self.hamiltonian.rotated_hamiltonian.blocks[ind[0],ind[0]].matrix) E2 = np.diag(self.hamiltonian.rotated_hamiltonian.blocks[ind[1],ind[1]].matrix) E2, E1 = np.meshgrid(E2, E1) # Initialize the pumping matrix: self.Rijl[key] = np.zeros((len(self.laserBeams[key].beam_vector),) + d_q.shape[1:]) # Grab the laser parameters: kvecs = self.laserBeams[key].kvec(r, t) intensities = self.laserBeams[key].intensity(r, t) deltas = self.laserBeams[key].delta(t) projs = self.laserBeams[key].project_pol(Bhat, R=r, t=t) # Loop through each laser beam driving this transition: for ll, (kvec, intensity, proj, delta) in enumerate(zip(kvecs, intensities, projs, deltas)): fijq = np.abs(d_q[0]*proj[2] + d_q[1]*proj[1] +d_q[2]*proj[0])**2 # Finally, calculate the scattering rate the polarization # onto the appropriate basis: self.Rijl[key][ll] = gamma*intensity/2*\ fijq/(1 + 4*(-(E2 - E1) + delta - np.dot(kvec, v))**2/gamma**2) def _add_pumping_rates_to_Rev(self): # Now add the pumping rates into the rate equation propogation matrix: for key in self.laserBeams: ind = self.hamiltonian.rotated_hamiltonian.laser_keys[key] n_off = sum(self.hamiltonian.rotated_hamiltonian.ns[:ind[0]]) n = self.hamiltonian.rotated_hamiltonian.ns[ind[0]] m_off = sum(self.hamiltonian.rotated_hamiltonian.ns[:ind[1]]) m = self.hamiltonian.rotated_hamiltonian.ns[ind[1]] # Sum over all lasers: Rij = np.sum(self.Rijl[key], axis=0) # Add the off diagonal components: self.Rev[n_off:n_off+n, m_off:(m_off+m)] += Rij self.Rev[m_off:(m_off+m), n_off:n_off+n] += Rij.T # Add the diagonal components. self.Rev[(np.arange(n_off, n_off+n), np.arange(n_off, n_off+n))] -= np.sum(Rij, axis=1) self.Rev[(np.arange(m_off, m_off+m), np.arange(m_off, m_off+m))] -= np.sum(Rij, axis=0)
[docs] def construct_evolution_matrix(self, r, v, t=0., default_axis=np.array([0., 0., 1.])): """ Constructs the evolution matrix at a given position and time. Parameters ---------- r : array_like, shape (3,) Position at which to calculate the equilibrium population v : array_like, shape (3,) Velocity at which to calculate the equilibrium population t : float Time at which to calculate the equilibrium population """ if self.tdepend['B']: B = self.magField.Field(r, t) else: B = self.magField.Field(r) # Calculate its magnitude: Bmag = np.linalg.norm(B, axis=0) # Calculate the Bhat direction: if Bmag > 1e-10: Bhat = B/Bmag else: Bhat = default_axis # Diagonalize the hamiltonian at this location: self.hamiltonian.diag_static_field(Bmag) # Re-initialize the evolution matrix: self.Rev = np.zeros((self.hamiltonian.n, self.hamiltonian.n)) if not np.all(self.hamiltonian.diagonal): # Reconstruct the decay matrix to match this new field. self.Rev_decay = self._calc_decay_comp_of_Rev( self.hamiltonian.rotated_hamiltonian ) self.Rev += self.Rev_decay # Recalculate the pumping rates: Bhat = self._calc_pumping_rates(r, v, t, Bhat) # Add the pumping rates to the evolution matrix: self._add_pumping_rates_to_Rev() return self.Rev, self.Rijl
[docs] def equilibrium_populations(self, r, v, t, **kwargs): """ Returns the equilibrium population as determined by the rate equations This method uses singular matrix decomposition to find the equilibrium state of the rate equations at a given position, velocity, and time. Parameters ---------- r : array_like, shape (3,) Position at which to calculate the equilibrium population v : array_like, shape (3,) Velocity at which to calculate the equilibrium population t : float Time at which to calculate the equilibrium population return_details : boolean, optional In addition to the equilibrium populations, return the full population evolution matrix and the scattering rates for each of the lasers Returns ------- Neq : array_like Equilibrium population vector Rev : array_like If return details is True, the evolution matrix for the state populations. Rijl : dictionary of array_like If return details is True, the scattering rates for each laser and each combination of states between the manifolds specified by the dictionary's index. """ return_details = kwargs.pop('return_details', False) Rev, Rijl = self.construct_evolution_matrix(r, v, t, **kwargs) # Find the singular values: U, S, VH = np.linalg.svd(Rev) Neq = np.compress(S <= self.svd_eps, VH, axis=0).T Neq /= np.sum(Neq) if Neq.shape[1] > 1: Neq = np.nan*Neq[:, 0] #raise ValueError("more than one equilbrium state found") # The operations above return a column vector, this collapses the column # vector into an array: Neq = Neq.flatten() if return_details: return (Neq, Rev, Rijl) else: return Neq
[docs] def force(self, r, t, N, return_details=True): """ Calculates the instantaneous force Parameters ---------- r : array_like Position at which to calculate the force t : float Time at which to calculate the force N : array_like Relative state populations return_details : boolean, optional If True, returns the forces from each laser and the magnetic forces. 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. F_mag : array_like If return_details is True, the forces due to the magnetic field. """ F = np.zeros((3,)) f = {} for key in self.laserBeams: f[key] = np.zeros((3, len(self.laserBeams[key].beam_vector))) ind = self.hamiltonian.laser_keys[key] n_off = sum(self.hamiltonian.ns[:ind[0]]) n = self.hamiltonian.ns[ind[0]] m_off = sum(self.hamiltonian.ns[:ind[1]]) m = self.hamiltonian.ns[ind[1]] Ne, Ng = np.meshgrid(N[m_off:(m_off+m)], N[n_off:(n_off+n)], ) for ll, beam in enumerate(self.laserBeams[key].beam_vector): kvec = beam.kvec(r, t) f[key][:, ll] += kvec*np.sum(self.Rijl[key][ll]*(Ng - Ne), axis=(0,1)) F += np.sum(f[key], axis=1) fmag = np.array([0., 0., 0.]) if self.include_mag_forces: gradBmag = self.magField.gradFieldMag(r) for ii, block in enumerate(np.diag(self.hamiltonian.blocks)): ind1 = int(np.sum(self.hamiltonian.ns[:ii])) ind2 = int(np.sum(self.hamiltonian.ns[:ii+1])) if self.hamiltonian.diagonal[ii]: if isinstance(block, tuple): fmag += np.sum(np.real( block[1].matrix[1] @ N[ind1:ind2] ))*gradBmag elif isinstance(block, self.hamiltonian.vector_block): fmag += np.sum(np.real( block.matrix[1] @ N[ind1:ind2] ))*gradBmag else: if isinstance(block, tuple): fmag += np.sum(np.real( self.hamiltonian.U[ii].T @ block[1].matrix[1] @ self.hamiltonian.U[ii]) @ N[ind1:ind2])*gradBmag elif isinstance(block, self.hamiltonian.vector_block): fmag += np.sum(np.real( self.hamiltonian.U[ii].T @ block.matrix[1] @ self.hamiltonian.U[ii]) @ N[ind1:ind2])*gradBmag F += fmag if return_details: return F, f, fmag else: return F
[docs] def set_initial_pop(self, N0): """ Sets the initial populations Parameters ---------- N0 : array_like The initial state population vector :math:`N_0`. It must have :math:`n` elements, where :math:`n` is the total number of states in the system. """ if len(N0) != self.hamiltonian.n: raise ValueError('Npop has only %d entries for %d states.' % (len(N0), self.hamiltonian.n)) if np.any(np.isnan(N0)) or np.any(np.isinf(N0)): raise ValueError('Npop has NaNs or Infs!') self.N0 = N0
[docs] def set_initial_pop_from_equilibrium(self): """ Sets the initial populations based on the equilibrium population at the initial position and velocity and time t=0. """ self.N0 = self.equilibrium_populations(self.r0, self.v0, t=0.)
[docs] def evolve_populations(self, t_span, **kwargs): """ Evolve the state population in time. This function integrates the rate equations to determine how the populations evolve in time. Any initial velocity is kept constant. It is analogous to obe.evolve_density(). Parameters ---------- t_span : list or array_like A two element list or array that specify the initial and final time of integration. **kwargs : Additional keyword arguments get passed to solve_ivp, which is what actually does the integration. Returns ------- sol : OdeSolution Bunch object that contains the following fields: * t: integration times found by solve_ivp * rho: density matrix * v: atomic velocity (constant) * r: atomic position It contains other important elements, which can be discerned from scipy's solve_ivp documentation. """ if any([self.tdepend[key] for key in self.tdepend.keys()]): raise NotImplementedError('Time dependence not yet implemented.') else: Rev, Rijl = self.construct_evolution_matrix(self.r0, self.v0) self.sol = solve_ivp(lambda t, N: Rev @ N, t_span, self.N0, **kwargs) return self.sol
[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, record_force=False, rng=np.random.default_rng(), **kwargs): """ Evolve the populations :math:`N` and the motion of the atom in time. This function evolves the rate equations, moving the atom through space, given the instantaneous force, for some period of 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 rateeq.force() method, use the calculated scattering rates from each of the laser beam (combined with the instantaneous populations) 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 and/or force, 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 record_force : boolean If true, record the instantaneous force and store in the solution. 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 * N: population vs. time * 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() if record_force: ts = [] Fs = [] def motion(t, y): N = y[:-6] v = y[-6:-3] r = y[-3:] Rev, Rijl = self.construct_evolution_matrix(r, v, t) if not random_force: if record_force: F = self.force(r, t, N, return_details=True) ts.append(t) Fs.append(F) F = F[0] else: F = self.force(r, t, N, return_details=False) dydt = np.concatenate((Rev @ N, F*free_axes/self.hamiltonian.mass+ self.constant_accel, v)) else: dydt = np.concatenate((Rev @ N, self.constant_accel, v)) if np.any(np.isnan(dydt)): raise ValueError('Enountered a NaN!') if progress_bar: progress.update(t/t_span[-1]) return dydt def random_force_func(t, y, dt): total_P = 0 num_of_scatters = 0 # Go over all available keys: for key in self.laserBeams: # Extract the pumping rate from each laser: Rl = np.sum(self.Rijl[key], axis=(1,2)) # Calculate the probability to scatter a photon from the laser: P = Rl*dt # Roll the dice N times, where $N=\sum(lasers) dice = rng.random(len(P)) # Give them kicks! for ii in np.arange(len(Rl))[dice<P]: num_of_scatters += 1 y[-6:-3] += self.laserBeams[key].beam_vector[ii].kvec(y[-3:], t)/\ self.hamiltonian.mass # Can branch to a differe, lower state, but let's ignore that # for the moment. y[-6:-3] += self.recoil_velocity[key]*(random_vector(rng, free_axes)) total_P += np.sum(P) # Calculate a new maximum dt to make sure we evolve while not # exceeding dt max: new_dt_max = (max_scatter_probability/total_P)*dt return (num_of_scatters, new_dt_max) def random_recoil_func(t, y, dt): num_of_scatters = 0 total_P = 0. # Go over each block in the Hamiltonian and compute the decay: for key in self.decay_rates: P = dt*self.decay_rates[key]*y[self.decay_N_indices[key]] # Roll the dice N times, where $N=\sum(n_i) dice = rng.random(len(P)) # For any random number that is lower than P_i, add a # recoil velocity. for ii in range(np.sum(dice<P)): num_of_scatters += 1 y[-6:-3] += self.recoil_velocity[key]*(random_vector(rng, free_axes)+ random_vector(rng, free_axes)) # Save the total probability of a scatter: total_P += np.sum(P) # Calculate a new maximum dt to make sure we evolve while not # exceeding dt max: new_dt_max = (max_scatter_probability/total_P)*dt return (num_of_scatters, new_dt_max) y0 = np.concatenate((self.N0, self.v0, self.r0)) if random_force: self.sol = solve_ivp_random(motion, random_force_func, t_span, y0, initial_max_step=max_scatter_probability, **kwargs) elif random_recoil: self.sol = solve_ivp_random(motion, random_recoil_func, t_span, y0, initial_max_step=max_scatter_probability, **kwargs) else: self.sol = solve_ivp(motion, t_span, y0, **kwargs) if progress_bar: # Just in case the solve_ivp_random terminated due to an event. progress.update(1.) # Rearrange the solution: self.sol.N = self.sol.y[:-6] self.sol.v = self.sol.y[-6:-3] self.sol.r = self.sol.y[-3:] if record_force: f = interp1d(ts[:-1], np.array([f[0] for f in Fs[:-1]]).T) self.sol.F = f(self.sol.t) f = interp1d(ts[:-1], np.array([f[2] for f in Fs[:-1]]).T) self.sol.fmag = f(self.sol.t) self.sol.f = {} for key in Fs[0][1]: f = interp1d(ts[:-1], np.array([f[1][key] for f in Fs[:-1]]).T) self.sol.f[key] = f(self.sol.t) self.sol.f[key] = np.swapaxes(self.sol.f[key], 0, 1) del self.sol.y return self.sol
[docs] def find_equilibrium_force(self, return_details=False, **kwargs): """ Finds the equilibrium force at the initial position This method works by finding the equilibrium population through the rateeq.equilibrium_population() function, then calculating the resulting force. Parameters ---------- return_details : boolean, optional If true, returns the forces from each laser and the scattering rate matrix. Default: False kwargs : Any additional keyword arguments to be passed to equilibrium_populations() 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. Neq : array_like If return_details is True, the equilibrium populations. Rijl : dictionary of array_like If return details is True, the scattering rates for each laser and each combination of states between the manifolds specified by the dictionary's index. F_mag : array_like If return_details is True, the forces due to the magnetic field. ii : int Number of iterations needed to converge. """ if any([self.tdepend[key] for key in self.tdepend.keys()]): raise NotImplementedError('Time dependence not yet implemented.') else: N_eq, Rev, Rijl = self.equilibrium_populations( self.r0, self.v0, t=0., return_details=True, **kwargs ) F_eq, f_eq, f_mag = self.force(self.r0, 0., N_eq) if return_details: return F_eq, f_eq, N_eq, Rijl, f_mag else: return F_eq
[docs] def generate_force_profile(self, R, V, name=None, progress_bar=False, **kwargs): """ 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 kwargs : Any additional keyword arguments to be passed to rateeq.find_equilibrium_force() Returns ------- profile : pylcp.rateeq.force_profile Resulting force profile. """ if not name: name = '{0:d}'.format(len(self.profile)) self.profile[name] = force_profile(R, V, self.laserBeams, self.hamiltonian) 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]) # Update position (use multi_index), populations, and forces. self.set_initial_position_and_velocity(r, v) try: F, f, Neq, Rijl, f_mag = self.find_equilibrium_force( return_details=True, **kwargs ) except: raise ValueError( "Unable to find solution at " +\ "r=({0:0.2f},{1:0.2f},{2:0.2f})".format(x, y, z) + " and " + "v=({0:0.2f},{1:0.2f},{2:0.2f})".format(vx, vy, vz) ) if progress_bar: progress.update((it.iterindex+1)/it.itersize) self.profile[name].store_data(it.multi_index, Neq, F, f, f_mag, Rijl)