def _refresh_hemisphere(self): if self.hemisphere is not None: self.removeItem(self.hemisphere) self.hemisphere = None if self.orbital is None or not self.options.is_show_hemisphere(): return k = energy_to_k(self.E_kin) kx, ky, kz = [self.orbital.psik[key] for key in ['kx', 'ky', 'kz']] self.dx, self.dy, self.dz = [ kx[1] - kx[0], ky[1] - ky[0], kz[1] - kz[0]] # this produces a hemisphere using the GLSurfacePlotItem, however, # the resulting hemisphere appears a little ragged at the circular # boundary # x = np.linspace(-k / self.dx, k / self.dx, 200) # y = np.linspace(-k / self.dy, k / self.dy, 200) # X, Y = np.meshgrid(x, y) # Z = np.sqrt(k**2 / (self.dx * self.dy) - X**2 - Y**2) # self.hemisphere = GLSurfacePlotItem(x=x, y=y, z=Z, color=( # 0.5, 0.5, 0.5, 0.7), shader='edgeHilight') # self.hemisphere.setGLOptions('translucent') # NEW method: nz = len(kz) X, Y, Z = np.meshgrid(kx / self.dx, ky / self.dy, kz[nz // 2:] / self.dz) data = X**2 + Y**2 + Z**2 iso = k**2 / (self.dx * self.dy) color = (0.5, 0.5, 0.5, 0.8) self.hemisphere = self._get_iso_mesh(data, iso, color, z_shift=False) self.addItem(self.hemisphere)
def set_kinetic_energy(self, E_kin, dk): kmax = energy_to_k(E_kin) if type(dk) == tuple: kxi = dk[0] kyi = dk[1] num_kx = len(kxi) num_ky = len(kyi) else: num_kx = int(2 * kmax / dk) num_ky = int(2 * kmax / dk) kxi = np.linspace(-kmax, +kmax, num_kx) kyi = np.linspace(-kmax, +kmax, num_ky) krange = ((kxi[0], kxi[-1]), (kyi[0], kyi[-1])) KX, KY = np.meshgrid(kxi, kyi, indexing='xy') KZ = np.sqrt(kmax**2 - KX**2 - KY**2) kxkykz = list( map(lambda a, b, c: (a, b, c), KX.flatten(), KY.flatten(), KZ.flatten())) data = np.reshape(self.psik['data_interp'](kxkykz), (num_kx, num_ky)) # Set kmap attributes self.kmap = { 'E_kin': E_kin, 'dk': dk, 'krange': krange, 'KX': KX, 'KY': KY, 'KZ': KZ, 'phi': 0, 'theta': 0, 'psi': 0, 'data': data }
def set_polarization(self, Ak_type, polarization, alpha, beta, gamma): if Ak_type == 'no': # Set |A.k|^2 to 1 self.Ak = { 'Ak_type': Ak_type, 'polarization': polarization, 'alpha': alpha, 'beta': beta, 'gamma': gamma, 'data': np.ones_like(self.kmap['data']) } return # Convert angles to rad and compute sin and cos for later use a = np.radians(alpha) b = np.radians(beta) sin_a = np.sin(a) cos_a = np.cos(a) sin_b = np.sin(b) cos_b = np.cos(b) # Compute gamma according to inelastic free mean path if gamma == 'auto': # lambda is calculated from the "universal curve" empirical # relation E_kin = self.kmap['E_kin'] c1 = 143 c2 = 0.054 lam = c1 * E_kin**(-2) + c2 * np.sqrt(E_kin) lam *= 10 gamma_calc = 1 / lam else: gamma_calc = gamma # Retrieve k-grid from kmap kx, ky, kz = self.kmap['KX'], self.kmap['KY'], self.kmap['KZ'] # Magnitude of k-vector k2 = kx**2 + ky**2 + kz**2 kmax = energy_to_k(self.kmap['E_kin']) # At the toroid, the emitted electron is always in the plane of # incidence and the sample is rotated if Ak_type == 'toroid' or Ak_type == 'only-toroid': # Parallel component of k-vector kpar = np.sqrt(k2 - kz**2) # |A.k|^2 factor Ak = (kpar * cos_a + kz * sin_a)**2 # At the NanoESCA, either p-polarization ,s-polarization, or # circularly polarized light can be simulated elif Ak_type == 'NanoESCA' or Ak_type == 'only-NanoESCA': # In-plane = p-polarization if polarization == 'p': Ak = kx * cos_a * cos_b + ky * cos_a * sin_b + kz * sin_a Ak = Ak**2 + gamma_calc**2 * sin_a**2 # Out-of-plane = s-polarization elif polarization == 's': Ak = -kx * sin_b + ky * cos_b Ak = Ak**2 Ak[kx**2 + ky**2 > kmax**2] = np.nan # unpolarized light, e.g. He-lamp # Compare Equation (37) in S. Moser, J. Electr. Spectr. # Rel. Phen. 214, 29-52 (2017). elif polarization == 'unpolarized': Ak = (k2 + gamma_calc**2 + 2 * kx * kz * cos_b + 2 * ky * kz * sin_b) * np.sin(2 * a) Ak += (kx**2 * sin_b + ky**2 * cos_b - kz**2 - gamma_calc**2) * np.cos(2 * a) Ak *= (2 / 3) # Circularly polarized light (right-handed) elif polarization == 'C+': polp = kx * cos_a * cos_b + ky * cos_a * sin_b + kz * sin_a pols = -kx * sin_b + ky * cos_b Ak = 0.5 * (polp**2 + gamma_calc**2 * sin_a**2) + 0.5 * \ pols**2 + (sin_b * kx - cos_b * ky) * gamma_calc * sin_a # Circularly polarized light (left-handed) elif polarization == 'C-': polp = kx * cos_a * cos_b + ky * cos_a * sin_b + kz * sin_a pols = -kx * sin_b + ky * cos_b Ak = 0.5 * (polp**2 + gamma_calc**2 * sin_a**2) + 0.5 * \ pols**2 - (sin_b * kx - cos_b * ky) * gamma_calc * sin_a # CDAD-signal (right-handed - left-handed) using empirically # damped plane wave elif polarization == 'CDAD': # Compare Equation (31) in S. Moser, J. Electr. Spectr. # Rel. Phen. 214, 29-52 (2017). Ak = +2 * (sin_b * kx - cos_b * ky) * gamma_calc * sin_a Ak[kx**2 + ky**2 > kmax**2] = np.nan # Set attributes self.Ak = { 'Ak_type': Ak_type, 'polarization': polarization, 'alpha': alpha, 'beta': beta, 'gamma': gamma, 'data': Ak }
def compute_3DFT(self, dk3D, E_kin_max, value): """Compute 3D-FT.""" # Determine required size (nkx,nky,nkz) of 3D-FT array to reach # desired resolution dk3D pad_x = max( int(2 * np.pi / (dk3D * self.psi['dx'])) - self.psi['nx'], 0) pad_y = max( int(2 * np.pi / (dk3D * self.psi['dy'])) - self.psi['ny'], 0) pad_z = max( int(2 * np.pi / (dk3D * self.psi['dz'])) - self.psi['nz'], 0) if pad_x % 2 == 1: pad_x += 1 # make sure it's an even number if pad_y % 2 == 1: pad_y += 1 # make sure it's an even number if pad_z % 2 == 1: pad_z += 1 # make sure it's an even number nkx = self.psi['nx'] + pad_x nky = self.psi['ny'] + pad_y nkz = self.psi['nz'] + pad_z # Set up k-grids for 3D-FT kx = self.set_3Dkgrid(nkx, self.psi['dx']) ky = self.set_3Dkgrid(nky, self.psi['dy']) kz = self.set_3Dkgrid(nkz, self.psi['dz']) # Compute 3D FFT # !! TESTING !!! This is supposed to yield also proper Real- and Imaginary parts!! # print(nkx, nky, nkz) # print(pad_x, pad_y, pad_z) psi_padded = np.pad(self.psi['data'], pad_width=((pad_x // 2, pad_x // 2), (pad_y // 2, pad_y // 2), (pad_z // 2, pad_z // 2)), mode='constant', constant_values=(0, 0)) psi_padded = np.fft.ifftshift(psi_padded) psik = np.fft.fftshift(np.fft.fftn(psi_padded)) # THIS IS THE OLD AND WELL TESTED WAY TO Compute 3D FFT # psik = np.fft.fftshift(np.fft.fftn(self.psi['data'], # s=[nkx, nky, nkz])) # properly normalize wave function in momentum space dkx, dky, dkz = kx[1] - kx[0], ky[1] - ky[0], kz[1] - kz[0] factor = dkx * dky * dkz * np.sum(np.abs(psik)**2) psik /= np.sqrt(factor) # Reduce size of array to value given by E_kin_max to save memory k_max = energy_to_k(E_kin_max) kx_indices = np.where((kx <= k_max) & (kx >= -k_max))[0] ky_indices = np.where((ky <= k_max) & (ky >= -k_max))[0] kz_indices = np.where((kz <= k_max) & (kz >= -k_max))[0] kx = kx[kx_indices] ky = ky[ky_indices] kz = kz[kz_indices] psik = np.take(psik, kx_indices, axis=0) psik = np.take(psik, ky_indices, axis=1) psik = np.take(psik, kz_indices, axis=2) # decide whether real, imaginry part, absolute value, or squared absolute value is used if value == 'real': psik = np.asarray(np.real(psik), order='C') elif value == 'imag': psik = np.asarray(np.imag(psik), order='C') elif value == 'abs': psik = np.abs(psik) else: psik = np.abs(psik)**2 # Define interpolating function to be used later for kmap # computation psik_interp = interp.RegularGridInterpolator((kx, ky, kz), psik, bounds_error=False, fill_value=np.nan) # Set attributes self.psik = { 'kx': kx, 'ky': ky, 'kz': kz, 'E_kin_max': E_kin_max, 'value': value, 'data': psik, 'data_interp': psik_interp } # Why? Local variables should only live until end of function # anyways... # Free memory for psik-array del psik
#declare l = 4.5; union{ cylinder { <0,0,0>,<0,0,l>,0.075 } cone { <0,0,l>, 0.15, <0,0,l+0.5>,0.0 } pigment { rgb<0,0,0>} } // hemisphere #declare k = %g; difference{ sphere{0,k} sphere{0,k-0.01}// adjust for the thickness you want box{<-k-0.1,-k-0.1,-k-0.1>,<k+0.1,k+0.1,0.0>} pigment{rgbt<1,0,0,0.5>} scale 1 } '''%(energy_to_k(E_kin)) real_stuff = ''' // x-axis #declare l = 6; union{ cylinder { <0,0,0>,<l,0,0>,0.075 } cone { <l,0,0>, 0.15, <l+0.5,0,0>,0.0 } pigment { rgb<0,0,0>} } // y-axis #declare l = 7; union{ cylinder { <0,0,0>,<0,-l,0>,0.075 } cone { <0,-l,0>, 0.15, <0,-l-0.5,0>,0.0 } pigment { rgb<0,0,0>}
def init_from_orbital_photonenergy(cls, name, orbital, parameters): """Returns a SlicedData object with the data[photonenergy,kx,ky] computed from the kmaps of one orbital for a series of photon energies. Args: name (str): name for SlicedData object orbital (list): [ 'URL',dict ] dict needs keys: 'energy' and 'name' parameters (list): list of parameters hnu_min (float): minimal photon energy hnu_max (float): maximal photon energy hnu_step (float): stepsize for photon energy fermi_energy (float): Fermi energy in eV dk (float): Desired k-resolution in kmap in Angstroem^-1. phi (float): Euler orientation angle phi in degree. theta (float): Euler orientation angle theta in degree. psi (float): Euler orientation angle psi in degree. Ak_type (string): Treatment of |A.k|^2: either 'no', 'toroid' or 'NanoESCA'. polarization (string): Either 'p', 's', 'unpolarized', C+', 'C-' or 'CDAD'. alpha (float): Angle of incidence plane in degree. beta (float): Azimuth of incidence plane in degree. gamma (float/str): Damping factor for final state in Angstroem^-1. str = 'auto' sets gamma automatically symmetrization (str): either 'no', '2-fold', '2-fold+mirror', '3-fold', '3-fold+mirror','4-fold', '4-fold+mirror' Returns: (SlicedData): SlicedData containing kmaps for various photon energies """ log = logging.getLogger('kmap') orbital = orbital[0] # only consider first orbital in list! # extract parameters hnu_min = parameters[0] hnu_max = parameters[1] hnu_step = parameters[2] fermi_energy = parameters[3] dk = parameters[4] phi, theta, psi = parameters[5], parameters[6], parameters[7] Ak_type = parameters[8] polarization = parameters[9] alpha, beta, gamma = parameters[10], parameters[11], parameters[12] symmetrization = parameters[13] # binding energy of orbital and work function BE = orbital[1]['energy'] - fermi_energy Phi = -fermi_energy # work function # determine axis_1 = photon energy hnu = np.arange(hnu_min, hnu_max, hnu_step) n_hnu = len(hnu) axis_1 = ['photonenergy', 'eV', [hnu_min, hnu_max]] # determine axis_2 and axis_3 kmax at BE_max E_kin_max = hnu[-1] - Phi + BE k_max = energy_to_k(E_kin_max) nk = int((2 * k_max) / dk) + 1 k_grid = np.linspace(-k_max, +k_max, nk) nk = len(k_grid) axis_2 = ['kx', '1/Å', [-k_max, +k_max]] axis_3 = ['ky', '1/Å', [-k_max, +k_max]] # initialize 3D-numpy array with zeros data = np.zeros((n_hnu, nk, nk)) orbital_names = [] log.info('Adding orbital to SlicedData Object, please wait!') # read orbital from cube-file database url = orbital[0] log.info('Loading from database: %s' % url) with urllib.request.urlopen(url) as f: file = f.read().decode('utf-8') orbital_data = Orbital(file) # loop over photon energies for i in range(len(hnu)): # kinetic energy of emitted electron E_kin = hnu[i] - Phi + BE kmap = orbital_data.get_kmap(E_kin, dk, phi, theta, psi, Ak_type, polarization, alpha, beta, gamma, symmetrization) kmap.interpolate(k_grid, k_grid, update=True) data[i, :, :] = kmap.data # define meta-data for tool-tip display orbital_info = orbital[1] meta_data = { 'Fermi energy (eV)': fermi_energy, 'Molecular orientation': (phi, theta, psi), '|A.k|^2 factor': Ak_type, 'Polarization': polarization, 'Incidence direction': (alpha, beta), 'Symmetrization': symmetrization, 'Orbital Info': orbital_info } return cls(name, axis_1, axis_2, axis_3, data, meta_data)
def init_from_orbitals(cls, name, orbitals, parameters): """Returns a SlicedData object with the data[BE,kx,ky] computed from the kmaps of several orbitals and broadened in energy. Args: name (str): name for SlicedData object orbitals (list): [[ 'URL1',dict1], ['URL2',dict2], ...] dict needs keys: 'energy' and 'name' parameters (list): list of parameters photon_energy (float): Photon energy in eV. fermi_energy (float): Fermi energy in eV energy_broadening (float): FWHM of Gaussian energy broadenening in eV dk (float): Desired k-resolution in kmap in Angstroem^-1. phi (float): Euler orientation angle phi in degree. theta (float): Euler orientation angle theta in degree. psi (float): Euler orientation angle psi in degree. Ak_type (string): Treatment of |A.k|^2: either 'no', 'toroid' or 'NanoESCA'. polarization (string): Either 'p', 's', 'unpolarized', C+', 'C-' or 'CDAD'. alpha (float): Angle of incidence plane in degree. beta (float): Azimuth of incidence plane in degree. gamma (float/str): Damping factor for final state in Angstroem^-1. str = 'auto' sets gamma automatically symmetrization (str): either 'no', '2-fold', '2-fold+mirror', '3-fold', '3-fold+mirror','4-fold', '4-fold+mirror' Returns: (SlicedData): SlicedData containing kmaps of all orbitals """ log = logging.getLogger('kmap') # extract parameters photon_energy = parameters[0] fermi_energy = parameters[1] energy_broadening = parameters[2] dk = parameters[3] phi, theta, psi = parameters[4], parameters[5], parameters[6] Ak_type = parameters[7] polarization = parameters[8] alpha, beta, gamma = parameters[9], parameters[10], parameters[11] symmetrization = parameters[12] # determine axis_1 from minimal and maximal binding energy extend_range = 3 * energy_broadening energies = [] for orbital in orbitals: energies.append(orbital[1]['energy']) BE_min = min(energies) - fermi_energy - extend_range BE_max = max(energies) - fermi_energy + extend_range # set energy grid spacing 6 times smaller than broadening dBE = energy_broadening / 6 nBE = int((BE_max - BE_min) / dBE) + 1 BE = np.linspace(BE_min, BE_max, nBE) nBE = len(BE) axis_1 = ['-BE', 'eV', [BE_min, BE_max]] # determine axis_2 and axis_3 kmax at BE_max Phi = -fermi_energy # work function E_kin_max = photon_energy - Phi + BE_max k_max = energy_to_k(E_kin_max) nk = int((2 * k_max) / dk) + 1 k_grid = np.linspace(-k_max, +k_max, nk) nk = len(k_grid) axis_2 = ['kx', '1/Å', [-k_max, +k_max]] axis_3 = ['ky', '1/Å', [-k_max, +k_max]] # initialize 3D-numpy array with zeros data = np.zeros((nBE, nk, nk)) orbital_names = [] # add kmaps of orbitals to log.info('Adding orbitals to SlicedData Object, please wait!') for orbital in orbitals: # binding energy of orbital BE0 = orbital[1]['energy'] - fermi_energy # kinetic energy of emitted electron E_kin = photon_energy - Phi + BE0 # Gaussian weight function norm = (1 / np.sqrt(2 * np.pi * energy_broadening**2)) weight = norm * \ np.exp(-((BE - BE0)**2 / (2 * energy_broadening**2))) url = orbital[0] log.info('Loading from database: %s' % url) with urllib.request.urlopen(url) as f: file = f.read().decode('utf-8') orbital_data = Orbital(file) orbital_names.append(orbital[1]['name']) log.info('Computing k-map for %s' % orbital[1]['name']) kmap = orbital_data.get_kmap(E_kin, dk, phi, theta, psi, Ak_type, polarization, alpha, beta, gamma, symmetrization) kmap.interpolate(k_grid, k_grid, update=True) log.info('Adding to 3D-array: %s' % orbital[1]['name']) for i in range(len(BE)): data[i, :, :] += weight[i] * np.nan_to_num(kmap.data) # set NaNs outside photoemission horizon KX, KY = np.meshgrid(k_grid, k_grid) for i in range(len(BE)): E_kin = photon_energy - Phi + BE[i] k_max = energy_to_k(E_kin) out = np.sqrt(KX**2 + KY**2) > k_max tmp = data[i, :, :] tmp[out] = np.NaN data[i, :, :] = tmp # define meta-data for tool-tip display orbital_info = {} for orbital_name, energy in zip(orbital_names, energies): orbital_info[orbital_name] = energy meta_data = { 'Photon energy (eV)': photon_energy, 'Fermi energy (eV)': fermi_energy, 'Energy broadening (eV)': energy_broadening, 'Molecular orientation': (phi, theta, psi), '|A.k|^2 factor': Ak_type, 'Polarization': polarization, 'Incidence direction': (alpha, beta), 'Symmetrization': symmetrization, 'Orbital Info': orbital_info } return cls(name, axis_1, axis_2, axis_3, data, meta_data)
rgbt = np.zeros((isosurface.faceCount(), 4), dtype=float) for c in range(3): rgbt[:, c] = color[c] rgbt[:, 3] = 1 # transparency (I guess) isosurface.setFaceColors(rgbt) p = gl.GLMeshItem( meshdata=isosurface, smooth=True, shader='edgeHilight' ) # shader options: 'balloon', 'shaded', 'normalColor', 'edgeHilight' p.setGLOptions( 'translucent') # choose between 'opaque', 'translucent' or 'additive' w.addItem(p) # add hemisphere for a given kinetic energy E_kin = 35.0 k = energy_to_k(E_kin) x = np.linspace(kx[0] / dx, kx[-1] / dx, nx) y = np.linspace(ky[0] / dy, ky[-1] / dy, ny) z = np.linspace(0, kz[-1] / dz, nz // 2) X, Y, Z = np.meshgrid(x, y, z) scalar_field = X**2 + Y**2 + Z**2 isoval = k**2 / (dx * dy) color = (0.5, 0.5, 0.5, 0.8) verts, faces = pg.isosurface(scalar_field, isoval) verts[:, 0] = verts[:, 0] - nx / 2 verts[:, 1] = verts[:, 1] - ny / 2 verts[:, 2] = verts[:, 2] isosurface = gl.MeshData(vertexes=verts, faces=faces) rgbt = np.zeros((isosurface.faceCount(), 4), dtype=float) for c in range(3):