Source code for MoleCool.tools.diststraj

# -*- coding: utf-8 -*-
"""
This Module contains different type of functions and classes, e.g. to calculate
simple linear trajectories through multiple apertures and to evaluate trajectories
as solutions of the Monte Carlo simulations.
"""
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
import h5py
from . import Ttov, vtoT, gaussian, FWHM2sigma, sigma2FWHM

#%%
[docs] def transversal_width2Temp(w_sigma=1.2e-3, v_Fw=170, total_dist=58e-2, mass=157, printing=True): """Calculate the velocity and corresponding temperature of a particle that is flying for a certain total distance and finally reaches a specific transversal position starting from zero position. The temperature corresponds to a transversal Gaussian distribution with standard deviation given by the transversal position `w_sigma`. Parameters ---------- w_sigma : float or np.ndarray, optional transversal position in m. The default is 1.2e-3. v_Fw : float, optional Forward velocity in m/s. The default is 170. total_dist : float, optional Total flying distance in m. The default is 58e-2. mass : float, optional atomic mass units of the particle. The default is 157. printing : bool, optional for enabling printing additional information. The default is True. Returns ------- T : float or np.ndarray temperature in K. v_tr : float or np.ndarray Transversal velocity in m/s. """ # time of flight from buffer gas output to camera position t = total_dist/v_Fw # transversal velocity correpsonding to certain transversal position (standard deviation sigma) v_tr = w_sigma/t # converting into temperature T = vtoT(v_tr,mass=157) if printing: print(f"T={T*1e3:.4g} mK, v={v_tr:.3f} m/s") return T, v_tr
#%%
[docs] def load_init_distr(system, fname, samplesize=None, x_init=0., plotting=False): """loading initial velocity and position distribution into system instance. Parameters ---------- system : ~MoleCool.System.System The System into which the distributions should be stored. fname : str file name of the distribution pkl file. samplesize : int, optional number of samples to be used from the distribution. The default is None for taking all samples. x_init : int, optional Distance from x=0 where initial free propagation starts to let the initial distribution evolve in the transverse axes. Or, initial free propagation until entering interaction zone. At x=0 The MC trajectory simulation starts. The default is 0.. plotting : bool, optional Whether plotting is enabled. The default is False. """ sampleslice = slice(samplesize) # None for all or int number for smaller number of samples # initial distributions normally at the output aperture of the buffergas cell with h5py.File(fname,'r') as h5file: rdist = h5file['dists/rdist'][:][sampleslice] vdist = h5file['dists/vdist'][:][sampleslice] # initial free propagation until entering interaction zone. x=0 remains the same rdist[:,1:] += vdist[:,1:]*x_init/vdist[:,0][:,None] system.v0, system.r0 = vdist, rdist system.x_init = x_init if plotting: plt.figure('vel dist y') plt.hist(vdist[:,1],bins=30) plt.figure('pos dist y') plt.hist(rdist[:,1]*1e3, bins=30)
#%%
[docs] class TrajectoryApertures(): def __init__(self, name='TrajApers', diameter0=3e-3, diameters=[5e-3, 3.3e-3, 10, 10], x_aper=[28e-3, 123e-3, 170e-3, 550e-3], labels=['aper1', 'aper2', 'PD1', 'PD2']): """Set up an instance for calculating linear propagation trajectories through multiple round apertures. To initiate, specify the apertures' labels and geometrics. The whole simulation is based on the following default axes definitions: - x-axis: longitudinal direction (molecular beam axis) - y-axis: transversal horizontal direction along probe lasers - z-axis: transversal vertical direction Parameters ---------- name : str, optional with this you can name the instance which is used in other functions for saving figures or hdf5 files if no other filename is specified. Default is 'TrajApers'. diameter0 : float, optional Diameter of buffer gas cell output in m. The default is 3e-3. diameters : list, optional Diameters of the apertures in m. The default is [5e-3, 3.3e-3, 10, 10]. x_aper : list, optional Positions of the apertures along the x-axis. The default is [28e-3, 123e-3, 170e-3, 580e-3]. labels : list, optional Labels of the apertures. The default is ['aper1', 'aper2', 'PD1', 'PD2']. """ self.name = name self.radii = np.array([diameter0, *diameters])/2 self.x_aper = np.array([0., *x_aper]) self.labels = ['Start', *labels] # empty lists for storing the distributions for all apertures self.rdists = [] self.vdists = [] self.hist_data = dict(r={}, v={})
[docs] def calc_propagation_apers(self, samplesize=5000000, mu=[200, 0, 0], sigma=[32., 12., 12.], v_tr_max=0.): """Calculate the distributions at every aperture using simple linear trajectories of the particles that are removed when hitting on the apertures. Parameters ---------- samplesize : int, optional how many particles or samples trajectories to be calculated. The default is 5000000. mu : list, optional mean values of all 3 axes in m/s for initializing a 3D Gaussian velocity distribution at the start. The default is [200, 0, 0]. sigma : list, optional Same as `mu` but standard deviations. The default is [32., 12., 12.]. v_tr_max : float, optional maximum initial transversal velocity at the edge of the buffer gas cell output. The transversal velocity distributions' mean is shifted linearly from 0 to v_tr_max at the edges. The default is +0. """ samplesize = int(samplesize) self.mu = np.array(mu) self.sigma = np.array(sigma) self.v_tr_max = v_tr_max #%% initial distributions # uniform transversal position distribution 2D: # base = 6 # rands = (base**np.random.rand(samplesize)-base**0)/(base**1-base**0) rands = np.random.rand(samplesize) length = np.sqrt(rands) *self.radii[0] #np.sqrt(np.rand...) for uniformly distributed points angle = 2*np.pi * np.random.rand(samplesize) y0,z0 = length * np.cos(angle), length * np.sin(angle) rdist0 = np.array([ np.zeros(samplesize), y0, z0 ]).T # Gaussian velocity distribution 3D theta = np.arctan2(rdist0[:,2],rdist0[:,1]) # arcus tangens(y,x) that is not periodic in pi but 2pi mu_arr = np.array([np.zeros(samplesize), np.cos(theta), np.sin(theta)]).T # mean velocity for Gaussian vel. distr. mu_arr[:,1:] *= (np.linalg.norm(rdist0[:,1:],axis=-1)/self.radii[0]*self.v_tr_max)[:,None] # linear increase of mean velocity from center to edge of buffer gas cell output mu_arr += self.mu[None,:] # shift the velocity distribution by a constant offset for each direction vdist0 = np.random.normal(loc=mu_arr, scale=self.sigma) # crate vel. distr. # Gaussian velocity distribution 1D which has position dependent mean values self.rdists.append(rdist0) self.vdists.append(vdist0) #%% free propagation steps through apertures for i,(radius,x_i) in enumerate(zip(self.radii[1:], self.x_aper[1:])): rdist = self.rdists[-1].copy() vdist = self.vdists[-1].copy() dx = x_i - rdist[0,0] rdist[:,1:] += vdist[:,1:] * dx / vdist[:,0][:,None] rdist[:,0] += dx lost_mols = np.where((np.linalg.norm(rdist[:,1:],axis=-1) > radius))[0] rdist = np.delete(rdist,lost_mols,axis=0) vdist = np.delete(vdist,lost_mols,axis=0) # append distributions from last aperture at the end of array self.rdists.append(rdist) self.vdists.append(vdist)
[docs] def initial_rdist_from_other(self, ind=-1, save_hdf5=False, fname=''): """Saving initial distribution of only the molecules that made it through all the apertures until reaching the aperture with index ind. Parameters ---------- ind : int, optional Index of the aperture from which the initial distribution is to be calculated. The default is -1. save_hdf5 : bool, optional Whether to save the initial distribution as hdf5. The default is False. fname : str, optional filename. The default is ''. Returns ------- rdist0_new : np.ndarray Calculated new initial distribution from final distribution. """ rdist0_new = self.rdists[ind].copy() rdist0_new[:,1:] -= self.vdists[ind][:,1:] * self.rdists[ind][0,0] / self.vdists[ind][:,0][:,None] rdist0_new[:,0] -= self.rdists[ind][0,0] if save_hdf5: with h5py.File(self.get_fname(fname,end='.hdf5'),'w') as h5file: for key in ['radii','x_aper','mu','sigma']: h5file.create_dataset(f"{key}", data=self.__dict__[key]) h5file.attrs['v_tr_max'] = self.v_tr_max h5file.create_dataset("dists/rdist", data=rdist0_new) h5file.create_dataset("dists/vdist", data=self.vdists[ind]) return rdist0_new
[docs] def get_hist_data(self, i, w, r_v='r', bins=30): """Calculate histogram data for the distribution at a certain aperture. All important evaluated arrays are saved in the dictionary hist_data. Parameters ---------- i : int index of the aperture. w : float Should imitate the imaging laser beam width as a width of the slice which is cut out of the 2D plane. So, it is the standard deviation in m of the Gaussian integrating over z-axis. r_v : str, optional position of velocity histogram ('r' or 'v'). The default is 'r'. bins : int, optional Number of bins. The default is 30. Returns ------- dict Dictionary with x and y axes of histogram data (`x_data`, `y_data`), fit data (`x_data_fit` and `y_data_fit`) and fit values (`popt`). This dictionary is also stored and can afterwards be accessed in the attribute ``hist_data`` in the following way: hist_data[r_v][i]. """ rdist = self.rdists[i] # position distribution at aperture i if r_v == 'r': dist = rdist elif r_v == 'v': dist = self.vdists[i] # estimating how many particles are within the standard deviation inds = np.where(np.abs(rdist[:,2]) < w)[0] print(i,inds.shape[0]/rdist.shape[0]) weights = gaussian(rdist[:,2], std=w) # Gaussian weighting y_data, x_bins = np.histogram(dist[:,1], bins=bins, weights=weights) x_data = (x_bins[1:] + x_bins[:-1]) / 2 # fitting: # Initial guess and bounds for the parameters p0 = [np.max(y_data), np.mean(x_data), np.std(x_data)] bounds = ([0, np.min(x_data), 0], [np.max(y_data)*10, np.max(x_data) ,np.std(x_data)*100]) popt, pcov = curve_fit(gaussian, x_data, y_data, p0=p0, bounds=bounds) # fitted data arrays x_data_fit = np.linspace(np.min(x_data), np.max(x_data), 500) y_data_fit = gaussian(x_data_fit, *popt) self.hist_data[r_v][i] = dict(x_data=x_data, y_data=y_data, popt=popt, x_bins=x_bins, x_data_fit=x_data_fit, y_data_fit=y_data_fit) return self.hist_data[r_v][i]
[docs] def plot_pos_vel_distr(self, w=1e-3, show_which=None, bins=(30,30), save_fig=False, fname=''): """Plot 1D and 2D position and velocity distributions as histograms. Parameters ---------- w : float, optional width see :meth:`get_hist_data`. The default is 1e-3. show_which : list of int, optional indices (of apertures below) for which the velocity distrs should be plotted. The default is None. bins : tuple, optional Numbers of bins for position and velocity histograms, respectively. save_hdf5 : bool, optional Whether to save the initial distribution as hdf5. The default is False. fname : str, optional filename. The default is ''. """ if not show_which: show_which = np.arange(0, len(self.rdists), 1) # plotting position distributions (histograms) 1D and 2D fig, axs = plt.subplots(3,len(show_which),sharey=False,figsize=(12,7)) # 2D histograms -------------------------------------------------------------- for i,ax in zip(show_which,axs[1]): rdist = self.rdists[i] ax.hist2d(rdist[:,1]*1e3,rdist[:,2]*1e3,bins=(bins[0],bins[0]),cmap='Blues') ax.set_aspect('equal', 'box') ax.axhline(0,color='yellow') ax.axvline(0,color='yellow') # 1D histograms -------------------------------------------------------------- # position distr: for i,ax in zip(show_which,axs[0]): hist_data = self.get_hist_data(i, w=w, r_v='r', bins=bins[0]) density = self.rdists[i].shape[0]/len(self.rdists[0]) ax.stairs(hist_data['y_data'], hist_data['x_bins']*1e3, ls='-') ax.plot(hist_data['x_data_fit']*1e3, hist_data['y_data_fit'], '--', label='Gaussian Fit', color = 'k', lw = 1) ax.set_xlabel('Position $y$ in mm') ax.set_title(f"{self.labels[i]}:\n{self.x_aper[i]*1e3} mm\n{density*1e2:.4f}%") # velocity distr: for i,ax in zip(show_which,axs[2]): hist_data = self.get_hist_data(i, w=w, r_v='v', bins=bins[1]) ax.stairs(hist_data['y_data'], hist_data['x_bins'], ls='-', fill=True) ax.plot(hist_data['x_data_fit'], hist_data['y_data_fit'], '--', label='Gaussian Fit', color = 'k', lw = 1) ax.set_xlabel('Velocity $v$ in m/s') plt.subplots_adjust(wspace=0.08) fig.suptitle(f"Init. condition: v_tr_max={self.v_tr_max}, mu={self.mu} m/s, sigma={self.sigma} m/s, w={w*1e3:.2f}mm", y=1.05) # saving figure if save_fig: plt.savefig(self.get_fname(fname,'_dists','.png'))
[docs] def get_fname(self,fname,middle='',end=''): """Add some strings for making filenames""" if not fname: return self.name + middle + end else: return fname + end
[docs] def plot_trajectories(self, N=200, ind=-1, yunit=1e-3, ylim=[-15,15], save_fig=False, fname=''): """Plotting trajectories of a certain distribution at a certain aperture. Parameters ---------- N : int, optional number of trajectories to be plotted. The default is 200. ind : int, optional Index of the aperture from which the initial distribution is to be calculated. The default is -1. yunit : float, optional unit of y-axis. The default is 1e-3. ylim : list, optional limits of y axis. The default is [-15,15]. save_hdf5 : bool, optional Whether to save the initial distribution as hdf5. The default is False. fname : str, optional filename. The default is ''. """ rdist0_new = self.initial_rdist_from_other(ind=ind, save_hdf5=False) rdist = self.rdists[ind] plt.figure() for i in np.arange(N): plt.plot([rdist0_new[i,0], rdist[i,0]], [rdist0_new[i,1]/yunit, rdist[i,1]/yunit], '-',c='C0',alpha=0.4) # plot apertures: ls_apers = {'color':'k','lw':2} for radius,x_aper,label in zip(self.radii,self.x_aper,self.labels): plt.vlines(x_aper, ymin=radius/yunit, ymax=radius*2/yunit, **ls_apers) plt.vlines(x_aper, ymax=-radius/yunit, ymin=-radius*2/yunit, **ls_apers) plt.xlabel('Position $x$ in m') plt.ylabel('Position $y$ in mm') if ylim: plt.ylim(ylim) if save_fig: plt.savefig(self.get_fname(fname,'_traj','.png'))
#%% handling and plotting of calculated Monte Carlo simulation results
[docs] def get_dist(system, key='r', unperturbed=False, radial=False, x_fly=0, xyz='z'): """Extract the results of the Monte Carlo (MC) simulation and calculate the distribution after a certain time of flight distance. Parameters ---------- system : ~MoleCool.System.System System instance from which the distributions are to be loaded. key : str, optional Type of distribution. Can be 'r' for position, 'v' for velocity and 'photons' for scattered photon number. The default is 'r'. unperturbed : bool, optional Whether to neglect the MC results and only use the unperturbed initial distribution. The default is False. radial : bool, optional Whether to calculate a radial distribution using the y and z axes. The default is False. x_fly : float, optional Time of flight distance to propagate the distribution from MC further. The default is 0. xyz : str, optional axis of the disibution to be returned. The default is 'z'. Returns ------- np.ndarray Position or velocity distribution along a certain axis or scattered photon number distribution. """ # if (not 'sols_end' in system.__dict__) and ('sols' in system.__dict__): # system.sols_end = dict() # for key_i,indices in dict(photons=-1, r=[3,4,5], v=[0,1,2]).items(): # system.sols_end[key_i] = np.array([sol.y[indices,-1] for sol in system.sols]) final_values = system.trajectory_results['final_values'] if key == 'photons': return final_values['photons'] x_init = system.x_init ixyz = {'x':0,'y':1,'z':2}[xyz] if unperturbed: v = system.v0 r = system.r0.copy() x_fly = x_fly + system.lasers.retrorefl_beams_kwargs['int_length'] else: v = final_values['v'].copy() r = final_values['r'].copy() r[:,0] += x_fly r[:,1:] += v[:,1:] * x_fly / v[:,0][:,None] arr = dict(r=r, v=v)[key] if radial: return np.sqrt(arr[:,1]**2 + arr[:,2]**2) else: return arr[:,ixyz]
[docs] def get_hist_vals(system, yz='z', w=0, radial=False, bins_per_unit=None, xrange=None, **kwargs): """Get histogram values from Monte Carlo simulation distributions. Parameters ---------- system : ~MoleCool.System.System System instance from which the distributions are to be loaded.. yz : str, optional Axis for which the histogram should be calculated. The default is 'z'. w : float or dict, optional Parameter(s) for weighting along the other transversal axis. Should imitate the imaging laser beam width as a width of the slice which is cut out of the 2D plane. So, when a float number is provided, it is the standard deviation in m of the Gaussian integrating over z-axis. If otherwise a dictionary is provided, it can contain all parameters corresponding to the gaussian function (:func:`tools.gaussian`). The default is 0. radial : bool, optional Whether to use a radial distribution. The default is False. bins_per_unit : float, optional Number of bins per unit (e.g. m or m/s). The default is None. xrange : tuple, optional xrange for evaluation the bins of the histogram. The default is None. **kwargs : kwargs further keyword arguments for :meth:`get_dist` function. Returns ------- hist_y : np.ndarray heights of the histogram's bins. hist_x : np.ndarray center positions of the histogram's bins. """ # if xrange is provided, calculate number of bins of histogram if xrange and bins_per_unit: Nbins = int(bins_per_unit*(max(xrange)-min(xrange))) + 1 else: Nbins = 30 #default values = get_dist(system, radial=radial, xyz=yz, **kwargs) if radial: weights = 1/values elif w: yz_ = dict(y='z',z='y')[yz] # other transversal axis, i.e. switch y and z axes kwargs_weights = {k:v for k,v in kwargs.items() if k not in ['key']} values_weights = get_dist(system, key='r', xyz=yz_, radial=radial, **kwargs_weights) if not isinstance(w, dict): w = dict(std=w) weights = gaussian(values_weights, **w) else: weights = None hist_y,bins = np.histogram(values, weights=weights, bins=Nbins, range=xrange) hist_x = (bins[1:]+bins[:-1])/2 # for lineplot istead of step-function return hist_x, hist_y
[docs] def plot_hist(system, key='r', yz='z', y_off=0, ax=None, color=None, scale_x=1, **kwargs): """Plotting histograms from get_hist_vals. Parameters ---------- system : ~MoleCool.System.System See :meth:`TrajectoryApertures.get_hist_data`. key : str, optional See :meth:`TrajectoryApertures.get_hist_data`. The default is 'r'. yz : str, optional See :meth:`TrajectoryApertures.get_hist_data`. The default is 'z'. y_off : float, optional Offset on y-plotting-axis. The default is 0. ax : plt.axis, optional axis instance where the data is drawn on. The default is None. color : str, optional Color of the histogram. The default is None. scale_x : float, optional Value to scale the x-axis for plotting. The default is 1. **kwargs : kwargs keyword arguements for :meth:`get_hist_vals`. """ if not ax: ax = plt.gca() # iterate through unperturbed and perturbed data arrays (forces on/off) for color_,label,unperturbed in zip([color,'grey'], ['lasers on','lasers off'], [False, True]): if key == 'photons' and unperturbed: continue hist_x, hist_y = get_hist_vals(system, key=key, yz=yz, unperturbed=unperturbed, **kwargs) # plotting ax.plot(hist_x*scale_x, hist_y+y_off, color=color_, label=label) if key == 'photons': xlabel = 'Scattered photons' else: xlabel = dict(r='Position',v='Velocity')[key] \ + f" ${key}_{yz}$ (" + dict(r='mm',v='m/s')[key]+')' ax.set_xlabel(xlabel) ax.set_ylabel('Number of molecules') ax.legend()
#%% if __name__ == '__main__': traj = TrajectoryApertures(diameter0=3e-3, name='testApers', diameters=[5e-3, 3.3e-3, 10, 10], x_aper=[28e-3, 123e-3, 170e-3, 550e-3], labels=['aper1', 'aper2', 'PD1', 'PD2']) traj.calc_propagation_apers(mu=[170, 0, 0], sigma=[32., *(np.array([1.,1.])*12)]) traj.plot_pos_vel_distr() traj.plot_trajectories() traj.initial_rdist_from_other() print(traj.hist_data['r'][4]['popt'][2]*1e3)