galore 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
galore/__init__.py ADDED
@@ -0,0 +1,441 @@
1
+ ###############################################################################
2
+ # #
3
+ # GALORE: Gaussian and Lorentzian broadening for simulated spectra #
4
+ # #
5
+ # Developed by Scanlon Materials Theory Group at University College London #
6
+ # (c) 2017 #
7
+ # #
8
+ ###############################################################################
9
+ # #
10
+ # This file is part of Galore. Galore is free software: you can redistribute #
11
+ # it and/or modify it under the terms of the GNU General Public License as #
12
+ # published by the Free Software Foundation, either version 3 of the License, #
13
+ # or (at your option) any later version. This program is distributed in the #
14
+ # hope that it will be useful, but WITHOUT ANY WARRANTY; without even the #
15
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
16
+ # See the GNU General Public License for more details. You should have #
17
+ # received a copy of the GNU General Public License along with this program. #
18
+ # If not, see <http://www.gnu.org/licenses/>. #
19
+ # #
20
+ ###############################################################################
21
+
22
+ from __future__ import print_function
23
+
24
+ from collections import OrderedDict
25
+ from collections.abc import Sequence
26
+ import logging
27
+ from math import sqrt, log
28
+ import os.path
29
+
30
+ import numpy as np
31
+ from scipy.interpolate import interp1d
32
+
33
+ import galore.formats
34
+ from galore.cross_sections import get_cross_sections, cross_sections_info
35
+
36
+
37
+ def auto_limits(data_1d, padding=0.05):
38
+ """Return limiting values outside data range
39
+
40
+ Args:
41
+ data_1d (iterable): Data to obtain range from
42
+ padding (float): Scale factor for padding relative to data range
43
+
44
+ Returns:
45
+ (2-tuple) (xmin, xmax)
46
+
47
+ """
48
+ xmin, xmax = min(data_1d), max(data_1d)
49
+ auto_xmax = xmax + padding * (xmax - xmin)
50
+ auto_xmin = xmin - padding * (xmax - xmin)
51
+ return auto_xmin, auto_xmax
52
+
53
+
54
+ def process_1d_data(input=['vasprun.xml'],
55
+ gaussian=None, lorentzian=None,
56
+ sampling=1e-2,
57
+ xmin=None, xmax=None,
58
+ spikes=False,
59
+ **kwargs):
60
+ """Read 1D data series from files, process for output
61
+
62
+ Args:
63
+ input (str or 1-list):
64
+ Input data file. Pass as either a string or a list containing one
65
+ string
66
+ **kwargs:
67
+ See main command reference
68
+
69
+ Returns:
70
+ 2-tuple (np.ndarray, np.ndarray):
71
+ Resampled x-values and corresponding broadened data as 1D numpy
72
+ arrays
73
+
74
+ """
75
+
76
+ if type(input) == str:
77
+ pass
78
+ elif len(input) > 1:
79
+ raise ValueError("Simple DOS only uses one input file, "
80
+ "not list: {0}".format(input))
81
+ else:
82
+ input = input[0]
83
+
84
+ if not os.path.exists(input):
85
+ raise Exception(
86
+ "Input file {0} does not exist!".format(input))
87
+ if galore.formats.is_xml(input):
88
+ xy_data = galore.formats.read_vasprun_totaldos(input)
89
+ elif galore.formats.is_gpw(input):
90
+ xy_data = galore.formats.read_gpaw_totaldos(input)
91
+ elif galore.formats.is_doscar(input):
92
+ xy_data = galore.formats.read_doscar(input)
93
+ elif galore.formats.is_vasp_raman(input):
94
+ xy_data = galore.formats.read_vasp_raman(input)
95
+ elif galore.formats.is_csv(input):
96
+ xy_data = galore.formats.read_csv(input)
97
+ else:
98
+ xy_data = galore.formats.read_txt(input)
99
+
100
+ # Add 5% to data range if not specified
101
+ auto_xmin, auto_xmax = auto_limits(xy_data[:, 0], padding=0.05)
102
+ if xmax is None:
103
+ xmax = auto_xmax
104
+ if xmin is None:
105
+ xmin = auto_xmin
106
+
107
+ d = sampling
108
+ x_values = np.arange(xmin, xmax, d)
109
+ data_1d = galore.xy_to_1d(xy_data, x_values, spikes=spikes)
110
+
111
+ broadened_data = data_1d.copy()
112
+ if lorentzian:
113
+ broadened_data = galore.broaden(
114
+ broadened_data, d=d, dist='lorentzian', width=lorentzian)
115
+
116
+ if gaussian:
117
+ broadened_data = galore.broaden(
118
+ broadened_data, d=d, dist='gaussian', width=gaussian)
119
+
120
+ return (x_values, broadened_data)
121
+
122
+
123
+ def process_pdos(input=['vasprun.xml'],
124
+ gaussian=None, lorentzian=None,
125
+ weighting=None, sampling=1e-2,
126
+ xmin=None, xmax=None, flipx=False, **kwargs):
127
+ """Read PDOS from files, process for output
128
+
129
+ Args:
130
+ input (list or str):
131
+ Files for processing. Vasp output or space-separated files with
132
+ XXX_EL_YYY.EXT filename pattern where EL is the element label.
133
+ We recommend SYSTEM_EL_dos.dat. Alternatively, a
134
+ `pymatgen.electronic_structure.dos.CompleteDos` can be
135
+ provided. Spin channels indicated by an (up) or (down) suffix
136
+ in file header will be combined for each orbital type.
137
+ **kwargs:
138
+ See main command reference
139
+
140
+ Returns:
141
+ dict:
142
+ Weighted and resampled orbital data in format::
143
+
144
+ {'el1': {'energy': values, 's': values, 'p': values ...},
145
+ 'el2': {'energy': values, 's': values, ...}, ...}
146
+
147
+ """
148
+ if isinstance(input, str) or not isinstance(input, Sequence):
149
+ input = [input]
150
+
151
+ # Read files into dict, check for consistency
152
+ energy_label = None
153
+ pdos_data = OrderedDict()
154
+ for pdos_file in input:
155
+ if (galore.formats.is_complete_dos(pdos_file)
156
+ or galore.formats.is_xml(pdos_file)):
157
+ pdos_data = galore.formats.read_vasprun_pdos(pdos_file)
158
+ kwargs['units'] = 'eV'
159
+ break
160
+
161
+ elif galore.formats.is_gpw(pdos_file):
162
+ pdos_data = galore.formats.read_gpaw_pdos(pdos_file)
163
+ kwargs['units'] = 'eV'
164
+ break
165
+
166
+ if not os.path.exists(pdos_file):
167
+ raise Exception("Input file {0} does not "
168
+ "exist!".format(input))
169
+
170
+ basename = os.path.basename(pdos_file)
171
+ try:
172
+ element = basename.split("_")[-2]
173
+ except IndexError:
174
+ raise Exception("Couldn't guess element name from filename. "
175
+ "Please format filename as XXX_EL_YYY.EXT"
176
+ "Where EL is the element label, and XXX, YYY "
177
+ "and EXT are labels of your choice. We recommend"
178
+ "SYSTEM_EL_dos.dat")
179
+
180
+ data = galore.formats.read_pdos_txt(pdos_file)
181
+
182
+ if energy_label is None:
183
+ energy_label = data.dtype.names[0]
184
+ else:
185
+ try:
186
+ assert data.dtype.names[0] == energy_label
187
+ except AssertionError as error:
188
+ error.args += ("Energy labels are not consistent "
189
+ "between input files",)
190
+ raise
191
+
192
+ orbital_labels = data.dtype.names[1:]
193
+ pdos_data[element] = OrderedDict([('energy', data[energy_label])])
194
+ pdos_data[element].update(OrderedDict((orbital, data[orbital])
195
+ for orbital in orbital_labels))
196
+
197
+ # Work out sampling details; 5% pad added to data if no limits specified
198
+ # In x-flip mode, the user specifies these as binding energies so values
199
+ # are reversed while treating DOS data.
200
+ d = sampling
201
+ limits = (auto_limits(data['energy'], padding=0.05)
202
+ for (element, data) in pdos_data.items())
203
+ xmins, xmaxes = zip(*limits)
204
+
205
+ if xmax is None:
206
+ xmax = max(xmaxes)
207
+
208
+ if xmin is None:
209
+ xmin = min(xmins)
210
+
211
+ if flipx:
212
+ xmin, xmax = -xmax, -xmin
213
+
214
+ x_values = np.arange(xmin, xmax, d)
215
+
216
+ # Resample data into new dictionary
217
+ pdos_plotting_data = OrderedDict()
218
+ for element, el_data in pdos_data.items():
219
+ pdos_plotting_data[element] = OrderedDict([('energy', x_values)])
220
+ for orbital, orb_data in el_data.items():
221
+ if orbital == 'energy':
222
+ continue
223
+ elif orb_data is None:
224
+ continue
225
+
226
+ xy_data = np.column_stack([el_data['energy'], orb_data])
227
+
228
+ pdos_resampled = galore.xy_to_1d(xy_data, x_values)
229
+ broadened_data = pdos_resampled.copy()
230
+
231
+ if lorentzian:
232
+ broadened_data = galore.broaden(broadened_data, d=d,
233
+ dist='lorentzian',
234
+ width=lorentzian)
235
+
236
+ if gaussian:
237
+ broadened_data = galore.broaden(broadened_data, d=d,
238
+ dist='gaussian',
239
+ width=gaussian)
240
+
241
+ pdos_plotting_data[element][orbital] = broadened_data
242
+
243
+ if weighting:
244
+ cross_sections = galore.get_cross_sections(weighting,
245
+ elements=pdos_data.keys())
246
+ pdos_plotting_data = galore.apply_orbital_weights(
247
+ pdos_plotting_data, cross_sections)
248
+
249
+ return pdos_plotting_data
250
+
251
+
252
+ def xy_to_1d(xy, x_values, spikes=False):
253
+ """Convert a set of x,y coordinates to 1D array
254
+
255
+ Data is resampled to a given sequence of regularly-spaced x-values. By
256
+ default linear interpolation is used between neighbouring points, which is
257
+ suitable for resampling distribution data.
258
+
259
+ Where the data consists of a set of discrete energy levels, it should be
260
+ resampled to a series of "spikes". y-values are placed on the nearest
261
+ x-value by subtracting d/2 and rounding up. d is determined by examining
262
+ the first two elements of x_values.
263
+
264
+ Args:
265
+ xy: (ndarray) 2D numpy array of x, y values
266
+ x_values: (iterable) An evenly-spaced x-value mesh
267
+ Returns:
268
+ (np.array): re-sampled y values corresponding to x_values
269
+ """
270
+
271
+ x_values = np.array(x_values)
272
+ n_x_values = x_values.size
273
+ d = x_values[1] - x_values[0]
274
+
275
+ # Structured arrays are allowed, in which case first field is x,
276
+ # second is y. A bit of hackery is needed to slice these interchangeably.
277
+ if xy.dtype.names is None:
278
+ x_field, y_field = (Ellipsis, 0), (Ellipsis, 1)
279
+ else:
280
+ x_field, y_field = xy.dtype.names
281
+
282
+ if spikes:
283
+ spikes = np.zeros(n_x_values)
284
+ spike_locations = x_values.searchsorted(xy[x_field] - (0.5 * d))
285
+
286
+ for location, value in zip(spike_locations, xy[y_field]):
287
+ if location == 0 or location == n_x_values:
288
+ pass
289
+ else:
290
+ spikes[location] += value
291
+
292
+ return spikes
293
+
294
+ else:
295
+ y_func = interp1d(xy[x_field], xy[y_field],
296
+ assume_sorted=False,
297
+ bounds_error=False, fill_value=0)
298
+ return y_func(x_values)
299
+
300
+
301
+ def delta(f1, f2, w=1):
302
+ """Compare two frequencies, return 1 if close"""
303
+ if abs(f1 - f2) <= 0.5 * w:
304
+ return 1
305
+ else:
306
+ return 0
307
+
308
+
309
+ def lorentzian(f, f0=0, fwhm=1):
310
+ """Lorentzian function with height 1 centered on f0.
311
+
312
+ Args:
313
+ f (np.array): 1D array of x-values (e.g. frequencies)
314
+ f0 (float): Origin of function
315
+ fwhm (float): full-width half-maximum (FWHM);
316
+ i.e. the width of the function at half its maximum value.
317
+
318
+ """
319
+ return 0.5 * fwhm / (np.pi * ((f - f0)**2 + (0.5 * fwhm)**2))
320
+
321
+
322
+ def gaussian(f, f0=0, fwhm=1):
323
+ """Gaussian function with height 1 centered on f0
324
+
325
+ f (np.array): 1D array of x-values (e.g. frequencies)
326
+ f0 (float): Origin of function
327
+ fwhm (float): full-width half-maximum (FWHM); i.e. the width of the
328
+ function at half its maximum value.
329
+
330
+ """
331
+ c = fwhm / (2 * sqrt(2 * log(2)))
332
+ return np.exp(-np.power(f - f0, 2) / (2 * c**2))
333
+
334
+
335
+ def broaden(data, dist='lorentz', width=2, pad=False, d=1):
336
+ """Given a 1d data set, use convolution to apply a broadening function
337
+
338
+ Args:
339
+ data (np.array): 1D array of data points to broaden
340
+ dist (str): Type of distribution used for broadening. Currently only
341
+ "Lorentz" is supported.
342
+ width (float): Width parameter for broadening function. Determines the
343
+ full-width at half-maximum (FWHM) of the broadening function.
344
+ pad (float): Distance sampled on each side of broadening function.
345
+ d (float): x-axis distance associated with each sample in 1D data
346
+
347
+ """
348
+
349
+ if not pad:
350
+ pad = width * 20
351
+
352
+ if dist.lower() in ('lorentz', 'lorentzian'):
353
+ fwhm = width
354
+ broadening = lorentzian(np.arange(-pad, pad, d), f0=0, fwhm=fwhm)
355
+ elif dist.lower() in ('gauss', 'gaussian'):
356
+ fwhm = width
357
+ broadening = gaussian(np.arange(-pad, pad, d), f0=0, fwhm=fwhm)
358
+ else:
359
+ raise Exception('Broadening distribution '
360
+ ' "{0}" not known.'.format(dist))
361
+
362
+ pad_points = int(pad / d)
363
+ broadened_data = np.convolve(broadening, data)
364
+ broadened_data = broadened_data[pad_points:len(data) + pad_points]
365
+
366
+ return broadened_data
367
+
368
+
369
+ def apply_orbital_weights(pdos_data, cross_sections):
370
+ """Weight orbital intensities by cross-section for photoemission simulation
371
+
372
+ Args:
373
+ pdos_data (dict): DOS data in format::
374
+
375
+ {'el1': {'energy': values, 's': values, 'p': values ...},
376
+ 'el2': {'energy': values, 's': values, ...}, ...}
377
+
378
+ where DOS values are 1D numpy arrays. Orbital labels must match
379
+ cross_sections data. It is recommended to use
380
+ collections.OrderedDict instead of regular dictionaries, to ensure
381
+ consistent output.
382
+
383
+ In addition, the fields "citation", "link" and "energy" are
384
+ recognised and logged as "INFO".
385
+
386
+ cross_sections (dict): Weightings in format::
387
+
388
+ {'el1': {'1s': x1, '2s': x2, '2p': x3 ...},
389
+ 'el2': {'3s': y1, '3p': y2 ...}, ...}
390
+
391
+ The labels should correspond to the headers in the input data. It
392
+ is fine not so specify the level (e.g. use 's', 'p', etc.) as is
393
+ done in the sample data; however, this means that all levels are
394
+ being treated equally and hence probably the core levels will be
395
+ weighted incorrectly. It is possible to set the cross-section of
396
+ undesired orbitals (e.g. projection onto d-orbital for early
397
+ elements) to None; in this case the orbital will be dropped from
398
+ the returned data set.
399
+
400
+ Returns:
401
+ weighted_pdos_data (dict): Weighted data in same format as input
402
+ """
403
+
404
+ if type(cross_sections) != dict:
405
+ raise TypeError('Cross-section data should be a dictionary. Try using '
406
+ 'galore.get_cross_sections for a suitable data set.')
407
+
408
+ logging.info("Applying cross-section weighting values to PDOS:")
409
+
410
+ cross_sections_info(cross_sections, logging=logging)
411
+
412
+ logging.info(" Orbital cross-section weights per electron:")
413
+
414
+ weighted_pdos_data = OrderedDict()
415
+ for el, orbitals in pdos_data.items():
416
+ if 'warning' in cross_sections[el]:
417
+ logging.warning(" {0}: {1}".format(
418
+ el, cross_sections[el]['warning']))
419
+ weighted_orbitals = OrderedDict()
420
+ for orbital, data in orbitals.items():
421
+ if orbital == 'energy':
422
+ weighted_orbitals.update({'energy': data})
423
+ else:
424
+ try:
425
+ cs = cross_sections[el][orbital]
426
+ except KeyError:
427
+ logging.warning("Could not find cross-section data for " +
428
+ "element {0}, ".format(el) +
429
+ "orbital {0}. ".format(orbital) +
430
+ "Skipping this orbital.")
431
+ cs = None
432
+
433
+ if cs is None:
434
+ pass
435
+ else:
436
+ logging.info(" {0} {1}: {2:.3e}".format(el, orbital, cs))
437
+ weighted_orbitals.update({orbital: data * cs})
438
+
439
+ weighted_pdos_data.update({el: weighted_orbitals})
440
+
441
+ return weighted_pdos_data
galore/cli/__init__.py ADDED
File without changes