Source code for abel.tools.vmi

# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import numpy as np
from abel.tools.polar import reproject_image_into_polar
from scipy.ndimage import map_coordinates
from scipy.ndimage.interpolation import shift
from scipy.optimize import curve_fit


[docs]def angular_integration(IM, origin=None, Jacobian=True, dr=1, dt=None): """Angular integration of the image. Returns the one-dimensional intensity profile as a function of the radial coordinate. Note: the use of Jacobian=True applies the correct Jacobian for the integration of a 3D object in spherical coordinates. Parameters ---------- IM : 2D numpy.array The data image. origin : tuple Image center coordinate relative to *bottom-left* corner defaults to ``rows//2, cols//2``. Jacobian : boolean Include :math:`r\sin\\theta` in the angular sum (integration). Also, ``Jacobian=True`` is passed to :func:`abel.tools.polar.reproject_image_into_polar`, which includes another value of ``r``, thus providing the appropriate total Jacobian of :math:`r^2\sin\\theta`. dr : float Radial coordinate grid spacing, in pixels (default 1). `dr=0.5` may reduce pixel granularity of the speed profile. dt : float Theta coordinate grid spacing in radians. if ``dt=None``, dt will be set such that the number of theta values is equal to the height of the image (which should typically ensure good sampling.) Returns ------ r : 1D numpy.array radial coordinates speeds : 1D numpy.array Integrated intensity array (vs radius). """ polarIM, R, T = reproject_image_into_polar( IM, origin, Jacobian=Jacobian, dr=dr, dt=dt) dt = T[0, 1] - T[0, 0] if Jacobian: # x r sinθ polarIM = polarIM * R * np.abs(np.sin(T)) speeds = np.trapz(polarIM, axis=1, dx=dt) n = speeds.shape[0] return R[:n, 0], speeds # limit radial coordinates range to match speed
[docs]def average_radial_intensity(IM, **kwargs): """Calculate the average radial intensity of the image, averaged over all angles. This differs form :func:`abel.tools.vmi.angular_integration` only in that it returns the average intensity, and not the integrated intensity of a 3D image. It is equivalent to calling :func:`abel.tools.vmi.angular_integration` with `Jacobian=True` and then dividing the result by 2*pi. Parameters ---------- IM : 2D numpy.array The data image. kwargs : additional keyword arguments to be passed to :func:`abel.tools.vmi.angular_integration` Returns ------- r : 1D numpy.array radial coordinates intensity : 1D numpy.array one-dimensional intensity profile as a function of the radial coordinate. """ R, intensity = angular_integration(IM, Jacobian=False, **kwargs) intensity /= 2*np.pi return R, intensity
[docs]def radial_integration(IM, radial_ranges=None): """ Intensity variation in the angular coordinate. This function is the :math:`\\theta`-coordinate complement to :func:`abel.tools.vmi.angular_integration` Evaluates intensity vs angle for defined radial ranges. Determines the anisotropy parameter for each radial range. See :doc:`examples/example_PAD.py <examples>` Parameters ---------- IM : 2D numpy.array Image data radial_ranges : list of tuple ranges or int step tuple integration ranges ``[(r0, r1), (r2, r3), ...]`` evaluates the intensity vs angle for the radial ranges ``r0_r1``, ``r2_r3``, etc. int - the whole radial range ``(0, step), (step, 2*step), ..`` Returns ------- Beta : array of tuples (beta0, error_beta_fit0), (beta1, error_beta_fit1), ... corresponding to the radial ranges Amplitude : array of tuples (amp0, error_amp_fit0), (amp1, error_amp_fit1), ... corresponding to the radial ranges Rmidpt : numpy float 1d array radial-mid point of each radial range Intensity_vs_theta: 2D numpy.array Intensity vs angle distribution for each selected radial range. theta: 1D numpy.array Angle coordinates, referenced to vertical direction. """ polarIM, r_grid, theta_grid = reproject_image_into_polar(IM) theta = theta_grid[0, :] # theta coordinates r = r_grid[:, 0] # radial coordinates if radial_ranges is None: radial_ranges = 1 if isinstance(radial_ranges, int): rr = np.arange(0, r[-1], radial_ranges) # @DanHickstein clever code to map ranges radial_ranges = list(zip(rr[:-1], rr[1:])) Intensity_vs_theta = [] radial_midpt = [] Beta = [] Amp = [] for rr in radial_ranges: subr = np.logical_and(r >= rr[0], r <= rr[1]) # sum intensity across radius of spectral feature intensity_vs_theta_at_R = np.sum(polarIM[subr], axis=0) Intensity_vs_theta.append(intensity_vs_theta_at_R) radial_midpt.append(np.mean(rr)) beta, amp = anisotropy_parameter(theta, intensity_vs_theta_at_R) Beta.append(beta) Amp.append(amp) return Beta, Amp, radial_midpt, Intensity_vs_theta, theta
[docs]def anisotropy_parameter(theta, intensity, theta_ranges=None): """ Evaluate anisotropy parameter :math:`\\beta`, for :math:`I` vs :math:`\\theta` data. .. math:: I = \\frac{\sigma_\\text{total}}{4\pi} [ 1 + \\beta P_2(\cos\\theta) ] where :math:`P_2(x)=\\frac{3x^2-1}{2}` is a 2nd order Legendre polynomial. `Cooper and Zare "Angular distribution of photoelectrons" J Chem Phys 48, 942-943 (1968) <http://dx.doi.org/10.1063/1.1668742>`_ Parameters ---------- theta: 1D numpy array Angle coordinates, referenced to the vertical direction. intensity: 1D numpy array Intensity variation with angle theta_ranges: list of tuples Angular ranges over which to fit ``[(theta1, theta2), (theta3, theta4)]``. Allows data to be excluded from fit, default include all data Returns ------- beta : tuple of floats (anisotropy parameter, fit error) amplitude : tuple of floats (amplitude of signal, fit error) """ def P2(x): # 2nd order Legendre polynomial return (3*x*x-1)/2 def PAD(theta, beta, amplitude): return amplitude*(1 + beta*P2(np.cos(theta))) # Eq. (1) as above # angular range of data to be included in the fit if theta_ranges is not None: subtheta = np.ones(len(theta), dtype=bool) for rt in theta_ranges: subtheta = np.logical_and( subtheta, np.logical_and(theta >= rt[0], theta <= rt[1])) theta = theta[subtheta] intensity = intensity[subtheta] # fit angular intensity distribution try: popt, pcov = curve_fit(PAD, theta, intensity) beta, amplitude = popt error_beta, error_amplitude = np.sqrt(np.diag(pcov)) # physical range if beta > 2 or beta < -1: beta, error_beta = np.nan, np.nan except: beta, error_beta = np.nan, np.nan amplitude, error_amplitude = np.nan, np.nan return (beta, error_beta), (amplitude, error_amplitude)
[docs]def toPES(radial, intensity, energy_cal_factor, per_energy_scaling=True, photon_energy=None, Vrep=None, zoom=1): """ Convert speed radial coordinate into electron kinetic or electron binding energy. Return the photoelectron spectrum (PES). This calculation uses a single scaling factor ``energy_cal_factor`` to convert the radial pixel coordinate into electron kinetic energy. Additional experimental parameters: ``photon_energy`` will give the energy scale as electron binding energy, in the same energy units, while ``Vrep``, the VMI lens repeller voltage (volts), provides for a voltage independent scaling factor. i.e. ``energy_cal_factor`` should remain approximately constant. The ``energy_cal_factor`` is readily determined by comparing the generated energy scale with published spectra. e.g. for O\ :sup:`-` photodetachment, the strongest fine-structure transition occurs at the electron affinity :math:`EA = 11,784.676(7)` cm :math:`^{-1}`. Values for the ANU experiment are given below, see also `examples/example_hansenlaw.py`. Parameters ---------- radial : numpy 1D array radial coordinates. intensity : numpy 1D array intensity values, at the radial array. energy_cal_factor : float energy calibration factor that will convert radius squared into energy. The units affect the units of the output. e.g. inputs in eV/pixel\ :sup:`2`, will give output energy units in eV. A value of :math:`1.148427\\times 10^{-5}` cm\ :math:`^{-1}/`\ pixel\ :sup:`2` applies for "examples/data/O-ANU1024.txt" (with Vrep = -98 volts). per_energy_scaling : bool sets the intensity Jacobian. If `True`, the returned intensities correspond to an "intensity per eV" or "intensity per cm\ :sup:`-1` ". If `False`, the returned intensities correspond to an "intensity per pixel". Optional: photon_energy : None or float measurement photon energy. The output energy scale is then set to electron-binding-energy in units of `energy_cal_factor`. The conversion from wavelength (nm) to `photon_energy` in (cm\ :`sup:-1`\ ) is :math:`10^{7}/\lambda` (nm) e.g. `1.0e7/812.51` for "examples/data/O-ANU1024.txt". Vrep : None or float repeller voltage. Convenience parameter to allow the `energy_cal_factor` to remain constant, for different VMI lens repeller voltages. Defaults to `None`, in which case no extra scaling is applied. e.g. `-98 volts`, for "examples/data/O-ANU1024.txt". zoom : float additional scaling factor if the input experimental image has been zoomed. Default 1. Returns ------- eKBE : numpy 1d-array of floats energy scale for the photoelectron spectrum in units of `energy_cal_factor`. Note that the data is no-longer on a uniform grid. PES : numpy 1d-array of floats the photoelectron spectrum, scaled according to the `per_energy_scaling` input parameter. """ if Vrep is not None: energy_cal_factor *= np.abs(Vrep)/zoom**2 eKE = radial**2*energy_cal_factor if photon_energy is not None: # electron binding energy eBKE = photon_energy - eKE else: eBKE = eKE # Jacobian correction to intensity, radius has been squared # We have E = c1 - c2 * r**2, where c1 and c2 are constants. To get thei # Jacobian, we find dE/dr = 2c2r. Since the coordinates are getting # stretched at high E and "squished" at low E, we know that we need to # divide by this factor. intensity[1:] /= (2*radial[1:]) # 1: to exclude R = 0 if per_energy_scaling: # intensity per unit energy intensity /= energy_cal_factor # sort into ascending order indx = eBKE.argsort() return eBKE[indx], intensity[indx]