ncrystal-python 3.9.81__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.
Files changed (53) hide show
  1. NCrystal/__init__.py +85 -0
  2. NCrystal/__main__.py +98 -0
  3. NCrystal/_chooks.py +854 -0
  4. NCrystal/_cli_cif2ncmat.py +269 -0
  5. NCrystal/_cli_endf2ncmat.py +503 -0
  6. NCrystal/_cli_hfg2ncmat.py +144 -0
  7. NCrystal/_cli_mcstasunion.py +74 -0
  8. NCrystal/_cli_ncmat2cpp.py +31 -0
  9. NCrystal/_cli_ncmat2hkl.py +180 -0
  10. NCrystal/_cli_nctool.py +1018 -0
  11. NCrystal/_cli_vdos2ncmat.py +463 -0
  12. NCrystal/_cli_verifyatompos.py +257 -0
  13. NCrystal/_cliimpl.py +307 -0
  14. NCrystal/_cliwrap_config.py +36 -0
  15. NCrystal/_common.py +499 -0
  16. NCrystal/_coreimpl.py +114 -0
  17. NCrystal/_hfgdata.py +546 -0
  18. NCrystal/_hklobjects.py +136 -0
  19. NCrystal/_is_std.py +0 -0
  20. NCrystal/_locatelib.py +210 -0
  21. NCrystal/_miscimpl.py +354 -0
  22. NCrystal/_mmc.py +757 -0
  23. NCrystal/_msg.py +60 -0
  24. NCrystal/_ncmat2cpp_impl.py +445 -0
  25. NCrystal/_ncmatimpl.py +2131 -0
  26. NCrystal/_numpy.py +76 -0
  27. NCrystal/_testimpl.py +579 -0
  28. NCrystal/api.py +56 -0
  29. NCrystal/atomdata.py +177 -0
  30. NCrystal/cfgstr.py +77 -0
  31. NCrystal/cifutils.py +1795 -0
  32. NCrystal/cli.py +96 -0
  33. NCrystal/constants.py +134 -0
  34. NCrystal/core.py +1910 -0
  35. NCrystal/datasrc.py +226 -0
  36. NCrystal/exceptions.py +66 -0
  37. NCrystal/hfg2ncmat.py +270 -0
  38. NCrystal/mcstasutils.py +438 -0
  39. NCrystal/misc.py +317 -0
  40. NCrystal/mmc.py +35 -0
  41. NCrystal/ncmat.py +778 -0
  42. NCrystal/ncmat2cpp.py +80 -0
  43. NCrystal/obsolete.py +67 -0
  44. NCrystal/plot.py +484 -0
  45. NCrystal/plugins.py +49 -0
  46. NCrystal/test.py +76 -0
  47. NCrystal/vdos.py +1034 -0
  48. ncrystal_python-3.9.81.dist-info/LICENSE +206 -0
  49. ncrystal_python-3.9.81.dist-info/METADATA +515 -0
  50. ncrystal_python-3.9.81.dist-info/RECORD +53 -0
  51. ncrystal_python-3.9.81.dist-info/WHEEL +5 -0
  52. ncrystal_python-3.9.81.dist-info/entry_points.txt +10 -0
  53. ncrystal_python-3.9.81.dist-info/top_level.txt +1 -0
NCrystal/vdos.py ADDED
@@ -0,0 +1,1034 @@
1
+
2
+ ################################################################################
3
+ ## ##
4
+ ## This file is part of NCrystal (see https://mctools.github.io/ncrystal/) ##
5
+ ## ##
6
+ ## Copyright 2015-2024 NCrystal developers ##
7
+ ## ##
8
+ ## Licensed under the Apache License, Version 2.0 (the "License"); ##
9
+ ## you may not use this file except in compliance with the License. ##
10
+ ## You may obtain a copy of the License at ##
11
+ ## ##
12
+ ## http://www.apache.org/licenses/LICENSE-2.0 ##
13
+ ## ##
14
+ ## Unless required by applicable law or agreed to in writing, software ##
15
+ ## distributed under the License is distributed on an "AS IS" BASIS, ##
16
+ ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ##
17
+ ## See the License for the specific language governing permissions and ##
18
+ ## limitations under the License. ##
19
+ ## ##
20
+ ################################################################################
21
+
22
+ """Module with VDOS-related utilities"""
23
+
24
+ from .constants import constant_planck as _constant_planck
25
+ from .constants import constant_boltzmann as _constant_boltzmann
26
+ from .constants import constant_c as _constant_c
27
+
28
+ _is_unit_test = False
29
+
30
+ vdos_units_2_eV = {
31
+ 'eV' : 1.0,
32
+ 'meV' : 1e-3,
33
+ 'keV' : 1e3,
34
+ 'MeV' : 1e6,
35
+ 'THz' : _constant_planck*1e12,
36
+ 'GHz' : _constant_planck*1e9,
37
+ 'MHz' : _constant_planck*1e6,
38
+ 'kHz' : _constant_planck*1e3,
39
+ 'Hz' : _constant_planck,
40
+ '1/cm' : _constant_planck * _constant_c * 1e-8,# 1e-8 to get c in [cm/s] instead of [Aa/s]
41
+ }
42
+
43
+ def createVDOSDebye( debye_temperature ):
44
+ """Create simplified VDOS according to the Debye model"""
45
+ from ._numpy import _np, _ensure_numpy, _np_linspace
46
+ _ensure_numpy()
47
+ #NB: Must keep function exactly synchronised with createVDOSDebye function
48
+ #in .cc src (although leaving out temperature,boundXS,elementMassAMU args
49
+ #here):
50
+ debye_energy = _constant_boltzmann*debye_temperature
51
+ if 0.5*debye_energy < 1.001e-5:
52
+ from .exceptions import NCBadInput
53
+ raise NCBadInput('Too low Debye temperature')
54
+ vdos_egrid = _np_linspace(0.5*debye_energy,debye_energy,20)
55
+ scale = 1.0 / (debye_energy*debye_energy)
56
+ vdos_density = scale * (vdos_egrid**2)
57
+ #Actual returned egrid should contain only first and last value:
58
+ return (_np.asarray([vdos_egrid[0],vdos_egrid[-1]]) ,vdos_density)
59
+
60
+ def debyeIsotropicMSD( *, debye_temperature, temperature, mass ):
61
+ """Estimate (isotropic, harmonic) atomic mean-squared-displacement (a.k.a. "U_iso") using the
62
+ Debye Model (eq. 11+12 in R.J. Glauber, Phys. Rev. Vol98 num 6,
63
+ 1955). Unit of returned MSD value is Aa^2. Input temperatures should be
64
+ in Kelvin, and input atomic mass should be in amu.
65
+ """
66
+ from ._chooks import _get_raw_cfcts
67
+ return float(_get_raw_cfcts()['ncrystal_debyetemp2msd'](debye_temperature, temperature, mass))
68
+
69
+ def debyeTempFromIsotropicMSD( *, msd, temperature, mass ):
70
+ """The inverse of debyeIsotropicMSD (implemented via root-finding), allowing to
71
+ get the Debye temperature which will give rise to a given
72
+ mean-squared-displacement (a.k.a. "U_iso").
73
+ """
74
+ from ._chooks import _get_raw_cfcts
75
+ return float(_get_raw_cfcts()['ncrystal_msd2debyetemp'](msd, temperature, mass))
76
+
77
+ def analyseVDOS(emin,emax,density,temperature,atom_mass_amu):
78
+ """Analyse VDOS curve to extract mean-squared-displacements, Debye temperature,
79
+ effective temperature, gamma0 and integral. Input VDOS must be defined via
80
+ an array of density values, over an equidistant energy grid over [emin,emax]
81
+ (in eV). Additionally, it is required that emin>0, and a parabolic trend
82
+ towards (0,0) will be assumed for energies in [0,emin]. Units are kelvin and
83
+ eV where appropriate.
84
+ """
85
+ from ._chooks import _get_raw_cfcts
86
+ from ._numpy import _np, _ensure_numpy
87
+ _ensure_numpy()
88
+ density = _np.asarray(density,dtype=float)
89
+ return _get_raw_cfcts()['nc_vdoseval'](emin,emax,density,temperature,atom_mass_amu)
90
+
91
+ def extractGn( vdos, n, mass_amu, temperature, scatxs = 1.0, expand_egrid = True ):
92
+ """Extract Sjolander's Gn function of order n."""
93
+ assert 1 <= n <= 99999
94
+ from .misc import AnyVDOS
95
+ v = AnyVDOS(vdos)
96
+ from ._chooks import _get_raw_cfcts
97
+ emin, emax, Gn = _get_raw_cfcts()['raw_vdos2gn'](v.egrid(),v.dos(),scatxs, mass_amu, temperature, int(n) )
98
+ if not expand_egrid:
99
+ return (emin,emax),Gn
100
+ else:
101
+ from ._numpy import _ensure_numpy, _np_linspace
102
+ _ensure_numpy()
103
+ return _np_linspace( emin, emax, len(Gn) ), Gn
104
+
105
+ def extractKnl( vdos, mass_amu, temperature, vdoslux = 3, scatxs = 1.0,
106
+ order_weight_fct = None, plot = False, **plotkwargs ):
107
+ """Expand the VDOS to a scattering kernel, based on the provided
108
+ parameters.
109
+
110
+ If provided, the order_weight_function can be used to assign a weight to
111
+ each order of the expansion (e.g. 0.0 to remove the contribution of that
112
+ order). It must be a function taking a single parameter n (the order), and
113
+ return a floating point value (the weight).
114
+
115
+ If plot=True, the extracted kernel will be plotted with the
116
+ NCrystal.plot.plot_knl function.
117
+ """
118
+ from .misc import AnyVDOS
119
+ v = AnyVDOS(vdos)
120
+ from ._chooks import _get_raw_cfcts
121
+ a,b,sab = _get_raw_cfcts()['raw_vdos2knl'](v.egrid(),v.dos(),scatxs, mass_amu, temperature, vdoslux, order_weight_fct )
122
+ k = dict(alpha=a, beta=b, sab=sab, mass_amu=mass_amu,temperature=temperature,scatxs=scatxs)
123
+ if plot:
124
+ from .plot import plot_knl
125
+ plot_knl( k, **plotkwargs )
126
+ return k
127
+
128
+
129
+ class PhononDOSAnalyser:
130
+
131
+ """Immutable class which reads and facilitates interpretation of phonon DOS information.
132
+
133
+ The input data format (fmt) can either be "raw", which is a list of DOS
134
+ (label,egrid,density). It can also be an NCrystal.misc.AnyVDOS,
135
+ NCrystal.Info.DI_VDOS or NCrystal.Info.DI_VDOSDebye object, or a list of
136
+ such. Or it can be "quantumespresso", pointing to a matdyn.dos file produced
137
+ by QuantumEspresso, expecting a format like (leftmost column is the
138
+ frequency, the next column is a combined DOS and the rest are partial DOS's
139
+ for the atoms in the same order as input into QE):
140
+
141
+ # Frequency[cm^-1] DOS PDOS
142
+ -2.2774053913E+02 0.0000000000E+00 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00
143
+ -2.2674053913E+02 3.3672834745E-05 1.0609E-05 1.0446E-05 6.2610E-06 6.3576E-06
144
+ -2.2574053913E+02 7.5113401337E-05 2.3699E-05 2.3292E-05 1.3952E-05 1.4171E-05
145
+ -2.2474053913E+02 1.1552338186E-04 3.6486E-05 3.5824E-05 2.1434E-05 2.1780E-05
146
+ ...
147
+
148
+ The partial DOS columns (the 4 rightmost columns in the example above) will
149
+ be labelled "pdos_0", "pdos_1", etc. After construction, the .plot(..)
150
+ method is then typically used to inspect the data, and the labels can be
151
+ merged, dropped or changed using the methods .drop(..), .merge(..) or
152
+ .update_label(..).
153
+
154
+ Next, one might way to apply a lower threshold on the curves if needed with
155
+ .apply_cutoff(..), after choosing an appropriate threshold value by look at
156
+ the plots produced by plot_cutoff_effects and plot_cutoff_effects_on_xsects.
157
+
158
+ A word warning: The PhononDOSAnalyser is immutable, hence this code does not
159
+ do anything (mydosana is an PhononDOSAnalyser object):
160
+
161
+ mydosana.apply_cutoff( 0.015 )
162
+
163
+ Instead one must do:
164
+
165
+ mydosana_with_cutoff = mydosana.apply_cutoff()
166
+
167
+ Or simply (if you don't care to keep the old un-edited object around):
168
+
169
+ mydosana = mydosana.apply_cutoff()
170
+
171
+ In addition to setting a threshold with .apply_cutoff, for output files
172
+ intended to have a high quality but smaller footprint (e.g. files destined
173
+ for the NCrystal stdlib) one might also consider applying a regularisation,
174
+ i.e. ensuring that the energy grid is linearly spaced and all grid points
175
+ being a multiple of the binwidth (so that 0.0 would be a grid point, if the
176
+ grid would have been extended downwards).
177
+
178
+ Once the phonon curves are as desired, one most likely want to use the
179
+ .apply_to(..) method to apply all or selected VDOS curves to an
180
+ NCMATComposer instance (which one might then for instance use to write the
181
+ data to an NCMAT file).
182
+
183
+ Note that many methods on this class can optionally take a list of labels or
184
+ indices. If so, the operation will only affect those DOS curves.
185
+
186
+ """
187
+
188
+ def __init__(self, data, fmt = None ):
189
+ """Initialise from data (see class description)."""
190
+ from .exceptions import NCBadInput
191
+
192
+ if isinstance(data,tuple) and len(data)==2 and data[0]=='__internal_state__' and isinstance(data[1],dict):
193
+ import copy
194
+ self.__d = copy.deepcopy( data[1] )
195
+ return
196
+
197
+ if fmt is None:
198
+ from .misc import AnyVDOS
199
+ from .core import Info
200
+ def _is_anyvdos(x):
201
+ return any( isinstance(data,e)
202
+ for e in (AnyVDOS,Info.DI_VDOS,Info.DI_VDOSDebye) )
203
+ if _is_anyvdos( data ):
204
+ fmt = 'anyvdos'
205
+ elif data and hasattr(data,'__len__') and all( _is_anyvdos(e) for e in data ):
206
+ fmt = 'anyvdos_list'
207
+ elif data and hasattr(data,'__len__') and all( ( hasattr(e,'__len__') and len(e)==3 ) for e in data ):
208
+ fmt = 'raw'
209
+ else:
210
+ fmt = 'quantumespresso'
211
+
212
+ def _extract_from_anyvdos(x):
213
+ from .misc import AnyVDOS
214
+ v = AnyVDOS( x )
215
+ return v.label,v.egrid(),v.dos()
216
+
217
+ _srcname = None
218
+ if fmt == 'anyvdos':
219
+ doslist = [_extract_from_anyvdos(data)]
220
+ elif fmt == 'anyvdos_list':
221
+ doslist = [_extract_from_anyvdos(e) for e in data]
222
+ elif fmt == 'quantumespresso':
223
+ from .misc import AnyTextData
224
+ td = AnyTextData( data )
225
+ doslist = _read_quantumespresso( td.content )
226
+ _srcname = td.name
227
+ elif fmt == 'raw':
228
+ import copy
229
+ doslist = copy.deepcopy( data )
230
+ _srcname = 'raw data'
231
+ else:
232
+ raise NCBadInput('Invalid input format. Currently only "raw", "anyvdos", "anyvdos_list", and "quantumespresso" are supported.')
233
+
234
+ _dl = []
235
+ from ._numpy import _ensure_numpy, _np
236
+ _ensure_numpy()
237
+
238
+ nminpts = 5
239
+ for lbl,egrid,density in doslist:
240
+ egrid = _np.asarray(egrid,dtype=float).copy()
241
+ if len(egrid) < nminpts:
242
+ raise NCBadInput(f'DOS egrid has too few points (at least {nminpts} required).')
243
+ if len(density) != len(egrid):
244
+ raise NCBadInput('DOS egrid and density arrays have different lengths.')
245
+ def _is_grid( a ):
246
+ return _np.all(a[:-1] < a[1:])#stackoverflow question 47004506
247
+ #but with < instead of <=
248
+ if not _is_grid(egrid):
249
+ raise NCBadInput('DOS egrid does not consist of increasing unique values')
250
+ if _np.isinf(egrid[-1]) or not egrid[-1] > 0.0:
251
+ raise NCBadInput(f'DOS egrid has invalid upper edge value: {egrid[-1]}')
252
+
253
+ density = _np.asarray(density,dtype=float).copy()
254
+ _densmin, _densmax = density.min(), density.max()
255
+ if not _densmax > 0.0:
256
+ raise NCBadInput('Maximum density value is not > 0.0.')
257
+ if _densmin < 0.0:
258
+ if abs(_densmin) < 1e-3*_densmax:
259
+ density.clip( min = 0.0, out = density )
260
+ from . import _common as nc_common
261
+ nc_common.warn('Clipping tiny negative DOS density values to 0.0')
262
+ else:
263
+ raise NCBadInput('Negative density values observed.')
264
+
265
+ #peel off excess zeroes:
266
+ assert nminpts>3
267
+ while len(egrid) > nminpts and egrid[-3]>0.0 and density[-2]==0.0 and density[-1]==0.0:
268
+ egrid = egrid[:-1]
269
+ density = density[:-1]
270
+ while len(egrid) > 5 and density[0]==0.0 and density[1]==0.0:
271
+ egrid = egrid[1:]
272
+ density = density[1:]
273
+ if not density.max() > 0.0:
274
+ raise NCBadInput('Invalid input DOS (non-positive everywhere)')
275
+ _dl.append( ( str(lbl), egrid, density ) )
276
+ doslist = _dl
277
+
278
+ self.__d = dict( doslist = doslist, srcname = _srcname )
279
+
280
+ @property
281
+ def source_name( self ):
282
+ "Name of source data (such as the file name). Might be absent (None)."
283
+ return self.__d['srcname']
284
+
285
+ @property
286
+ def ndos( self ):
287
+ """Number of DOS curves."""
288
+ return len( self.__d['doslist'])
289
+
290
+ @property
291
+ def labels( self ):
292
+ """Available DOS labels."""
293
+ return list(lbl for lbl,_,_ in self.__d['doslist'])
294
+
295
+ def dos( self, label_or_idx ):
296
+ """Get DOS by index or label. Returns tuple of two arrays: (egrid,dos)."""
297
+ lbl,egrid,dos = self.__d['doslist'][self.__dosidx( label_or_idx )]
298
+ return egrid,dos
299
+
300
+ def drop( self, *dos_labels_or_indices ):
301
+ """
302
+ Return a new instance from which the indicated DOS has been removed.
303
+ DOS can be identified via labels or indices, and it is possible to
304
+ provide a list or tuple of them, to drop multiple
305
+ """
306
+ droplist = self.__dosidxlist( *dos_labels_or_indices )
307
+ if not droplist:
308
+ return self
309
+ o = self.__clone()
310
+ o.__d['doslist'] = [ e for i,e in enumerate(o.__d['doslist'])
311
+ if i not in droplist ]
312
+ return o
313
+
314
+ def update_label( self, old_label_or_idx, newlabel ):
315
+ """Return a new instance in which the label of the indicated DOS curve
316
+ has been updated."""
317
+ idx = self.__dosidx( old_label_or_idx )
318
+ o = self.__clone()
319
+ dl = list(o.__d['doslist'])
320
+ ll = list(dl[idx])
321
+ ll[0] = str(newlabel)
322
+ dl[idx] = tuple(ll)
323
+ o.__d['doslist'] = tuple(dl)
324
+ return o
325
+
326
+ def merge( self, *dos_labels_or_indices, weights = None, newlabel = None ):
327
+ """
328
+ Return a new instance from which the indicated DOS curves have been merged.
329
+ If weights is given, it must be a list of weights to to use in this merging.
330
+
331
+ If newlabel is not provided, the label of the resulting curve will be
332
+ automatically generated.
333
+ """
334
+ mergelist = self.__dosidxlist( *dos_labels_or_indices )
335
+ if weights and len(weights) != len(mergelist):
336
+ from .exceptions import NCBadInput
337
+ raise NCBadInput('Invalid weights provided (must be list of same length as the number of DOS curves to merge')
338
+ if len(mergelist)==1 and newlabel:
339
+ #simply a label update:
340
+ return self.update_label( mergelist[0], newlabel )
341
+ if len(mergelist)<=1:
342
+ #do nothing:
343
+ return self
344
+
345
+ egrids = [(lbl,egrid) for i,(lbl,egrid,dos) in enumerate(self.__d['doslist']) if i in mergelist]
346
+ from ._numpy import _ensure_numpy, _np
347
+ _ensure_numpy()
348
+ newegrid = egrids[0][1].copy()
349
+ for lbl,eg in egrids[1:]:
350
+ if not _np.array_equal(newegrid,eg):
351
+ from .exceptions import NCBadInput
352
+ raise NCBadInput('Can not merge DOS curves with incompatible egrids (problems merging labels "%s" and "%s")'%(egrids[0][0],lbl))
353
+
354
+ if weights is None:
355
+ weights = [ 1.0 ] * len(mergelist)
356
+ assert all( w >= 0.0 for w in weights )
357
+ import math
358
+ _wsum = math.fsum(w for w in weights)
359
+ assert _wsum > 0.0
360
+ weights = [ w/_wsum for w in weights ]
361
+
362
+ c = None
363
+ for idx,w in zip(mergelist,weights):
364
+ _ = self.dos(idx)[1].copy()
365
+ _ *= w
366
+ if c is None:
367
+ c = _
368
+ else:
369
+ c += _
370
+ assert c is not None
371
+
372
+ if newlabel is None:
373
+ newlabel = 'merged(%s)'%(','.join( [lbl for i,(lbl,_,_) in
374
+ enumerate(self.__d['doslist'])
375
+ if i in mergelist] ))
376
+ while newlabel in self.labels:
377
+ newlabel += '(uniquelabel)'
378
+ o = self.drop( mergelist )
379
+ o.__d['doslist'].append( (newlabel,newegrid,c) )
380
+ return o
381
+
382
+ def apply_regularisation( self, target_n, *dos_labels_or_indices, quiet = True ):
383
+ """Returns a new instance where the indicated lower threshold value have been
384
+ "regularised", i.e. put on a linearly spaced grid which if it had been
385
+ extended downwards, would eventually cross the point E=0. It is an error
386
+ to apply it to a curve whose first egrid point is not positive (use the
387
+ .apply_cutoff(..) method first for such curves)."""
388
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
389
+ if not selected:
390
+ return self
391
+ target_n = int(target_n)
392
+ assert target_n >= 10
393
+ def doregfct( _e, _d ):
394
+ return _do_regularise( egrid=_e, density=_d,
395
+ n=target_n, quiet = quiet)
396
+ o = self.__clone()
397
+ oldlist = o.__d['doslist']
398
+ newlist = []
399
+ for idx,(lbl,egrid,dos) in enumerate(oldlist):
400
+ if idx not in selected:
401
+ newlist.append( (lbl,egrid,dos) )
402
+ continue
403
+ assert len(egrid)==len(dos)
404
+ if not egrid[0] > 0.0:
405
+ from .exceptions import NCBadInput
406
+ raise NCBadInput('Can not regularise DOS whose first egrid value '
407
+ 'is not positive. Use the .apply_cutoff method first.')
408
+ egrid, dos = doregfct( egrid, dos )
409
+ assert len(egrid)==len(dos)
410
+ newlist.append( (lbl,egrid,dos) )
411
+ o.__d['doslist'] = newlist
412
+ return o
413
+
414
+ def apply_cutoff( self, threshold, *dos_labels_or_indices ):
415
+ """Returns a new instance where the indicated lower threshold value has been
416
+ applied to all (selected) DOS curves."""
417
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
418
+
419
+ if not selected:
420
+ return self
421
+
422
+ threshold = self._parse_threshold( threshold )
423
+
424
+ from ._numpy import _ensure_numpy, _np
425
+ _ensure_numpy()
426
+
427
+ o = self.__clone()
428
+ oldlist = o.__d['doslist']
429
+ newlist = []
430
+ for idx,(lbl,egrid,dos) in enumerate(oldlist):
431
+ if idx not in selected:
432
+ newlist.append( (lbl,egrid,dos) )
433
+ continue
434
+ assert len(egrid)==len(dos)
435
+ j = _np.argmax( egrid >= threshold )
436
+ egrid,dos = egrid[j:],dos[j:]
437
+ assert egrid[0] >= threshold
438
+ assert len(egrid)==len(dos)
439
+ newlist.append( (lbl,egrid,dos) )
440
+ o.__d['doslist'] = newlist
441
+ return o
442
+
443
+ def plot( self, *dos_labels_or_indices,
444
+ do_newfig = True,
445
+ do_show = True,
446
+ do_legend = True,
447
+ logy=None,
448
+ unit = 'eV',
449
+ ymin = None,
450
+ ymax = None,
451
+ xmin = None,
452
+ xmax = None,
453
+ ):
454
+ """Plot contained DOS curves for the selected DOS labels (or indices),
455
+ defaulting to all curves. Set do_show to false to avoid plt.show(), logy
456
+ to default to a semilogy plot, ymin to set a minimum plotrange of the
457
+ y-axis, and finally the unit argument can be used to show the DOS in
458
+ other units (e.g. "THz", "1/cm", "meV", etc.
459
+ """
460
+ self.__plot( *dos_labels_or_indices, do_newfig = do_newfig,
461
+ do_show = do_show, logy=logy, unit = unit,
462
+ do_legend=do_legend,
463
+ ymin = ymin, ymax = ymax,
464
+ xmin = xmin, xmax = xmax )
465
+
466
+ def plot_gn( self, *dos_labels_or_indices, n=1,
467
+ temperature = 293.15, masses = None,
468
+ do_newfig = True, do_show = True, do_legend = True,
469
+ logy=None, unit = 'eV' ):
470
+ """Similar to .plot() but showing the Sjolander Gn function instead. If
471
+ labels are not elements or isotopes, masses must be provided in a
472
+ list (in daltons). It is an error to try to plot a Gn function for a
473
+ vdos curve whose first egrid point is not positive (i.e. you must
474
+ first .apply_cutoff(..)).
475
+ """
476
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
477
+ self.__plot( *selected, do_newfig = do_newfig,
478
+ do_show = do_show, logy=logy, unit = unit,
479
+ do_legend=do_legend,
480
+ sjolanderGn = self.__sjolanderGn_args( selected = selected,
481
+ n=n,
482
+ masses=masses,
483
+ temperature=temperature) )
484
+
485
+ def plot_cutoff_effects( self, thresholds, *dos_labels_or_indices, gn = None,
486
+ temperature = 293.15, masses = None,
487
+ regularise_n_values = None, **plot_kwargs ):
488
+ """Plot the effect of the listed thresholds on the DOS curves for the
489
+ selected DOS labels (or indices), defaulting to all curves. Any
490
+ plot_kwargs will be used as on the .plot() method.
491
+
492
+ To see the effect on Sjolander's Gn curves instead, supply the gn
493
+ keyword (i.e. gn=1 to see G1) -- optionally along with temperature and
494
+ masses keywords (cf. .plot_gn(..)).
495
+
496
+ """
497
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
498
+
499
+ if gn is not None:
500
+ sjolanderGn = self.__sjolanderGn_args( selected = selected,
501
+ n=gn,
502
+ masses=masses,
503
+ temperature=temperature)
504
+ plot_kwargs['sjolanderGn'] = sjolanderGn
505
+
506
+ unitname,unitfactor = _parsevdosunit( plot_kwargs.get('unit','eV') )
507
+ do_show = plot_kwargs.get('do_show',True)
508
+ do_newfig = plot_kwargs.get('do_newfig',True)
509
+ color_offset = plot_kwargs.get('color_offset',0)
510
+ plot_kwargs['do_show'] = False
511
+ plot_kwargs['do_newfig'] = False
512
+ do_legend = plot_kwargs.get('do_legend',True)
513
+ do_grid = plot_kwargs.get('do_grid',True)
514
+ plot_kwargs['do_grid'] = False
515
+ plot_kwargs['do_legend'] = False
516
+
517
+ from .plot import _import_matplotlib_plt
518
+ plt = _import_matplotlib_plt()
519
+ if do_newfig:
520
+ plt.figure()
521
+
522
+ plot_thresholds = [e for e in thresholds]
523
+ if gn is None:
524
+ #Can not plot Gn without a cutoff
525
+ plot_thresholds = [None] + plot_thresholds
526
+ reg_n_vals = [None] + ( [ e for e in regularise_n_values] if regularise_n_values else [] )
527
+ for t in plot_thresholds:
528
+ for regn in reg_n_vals:
529
+ plot_kwargs['color_offset'] = color_offset
530
+ if t is None:
531
+ if regn is None:
532
+ lblcomments=['orig']
533
+ else:
534
+ lblcomments = [f'npts={regn}']
535
+ else:
536
+ t_parsed = self._parse_threshold( t )
537
+ lblcomments = [f'cut@{t_parsed/unitfactor:g}{unitname}']
538
+ if regn is not None:
539
+ lblcomments+=[f'npts={regn}']
540
+ lblcomment = ','.join(lblcomments)
541
+ plot_kwargs['labelfct'] = lambda lbl : f'{lbl} ({lblcomment})'
542
+ color_offset += len(selected)
543
+ o = self if t is None else self.apply_cutoff( t, *selected )
544
+ if regn is not None:
545
+ if t is None:
546
+ #Only go ahead in this case, if all selected egrids already have a cutoff:
547
+ if not all( self.dos(idx)[0][0]>0.0 for idx in selected ):
548
+ continue
549
+ o = o.apply_regularisation( regn, *selected )
550
+ o.__plot(*selected,**plot_kwargs)
551
+
552
+ from .plot import _plt_final
553
+ _plt_final(do_grid,do_legend,do_show)
554
+
555
+ def plot_cutoff_effects_on_xsects( self, ncmatcomposer, thresholds, cfg_params = None,
556
+ *dos_labels_or_indices, lblmap = None, **plot_kwargs ):
557
+ from .ncmat import NCMATComposer
558
+ assert isinstance(ncmatcomposer,NCMATComposer), ( "First argument in call to .plot_cutoff_"
559
+ "effects_on_xsects(..) must be an NCMATComposer object" )
560
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
561
+ unitname,unitfactor = _parsevdosunit( plot_kwargs.get('unit','eV') )
562
+ do_show = plot_kwargs.get('do_show',True)
563
+ do_newfig = plot_kwargs.get('do_newfig',True)
564
+ do_grid = plot_kwargs.get('do_grid',True)
565
+ do_legend = plot_kwargs.get('do_legend',True)
566
+ plot_kwargs['scatter_breakdown'] = False
567
+ plot_kwargs['show_scattering'] = True
568
+ plot_kwargs['show_absorption'] = False
569
+ plot_kwargs['do_newfig'] = False
570
+ plot_kwargs['do_show'] = False
571
+ plot_kwargs['do_grid'] = False
572
+ plot_kwargs['do_legend'] = False
573
+ plot_kwargs['cfg_params'] = cfg_params
574
+ color = plot_kwargs.get('color')
575
+ lblmap = self.__determine_lblmap( selected, ncmatcomposer, lblmap = lblmap, warn = True )
576
+ lbls = list(sorted(lblmap.keys()))
577
+ if not lbls:
578
+ return
579
+ from .plot import _import_matplotlib_plt
580
+ plt = _import_matplotlib_plt()
581
+ if do_newfig:
582
+ plt.figure()
583
+ colorder = self.__colorder()
584
+ for iplot, t in enumerate([e for e in thresholds]):
585
+ t = self._parse_threshold( t )
586
+ c = ncmatcomposer.clone()
587
+ o = self.apply_cutoff( t, lbls )
588
+ thr_description = f'cut@{t/unitfactor:g}{unitname}'
589
+ for lbl in lbls:
590
+ if lbl not in lblmap:
591
+ from . import _common as nc_common
592
+ nc_common.warn('Not using PhononDOSAnalyser label "{lbl}" in plot.')
593
+ continue
594
+ c.set_dyninfo_vdos( lblmap[lbl], comment = 'From PhononDOSAnalyser', **o.get_dyninfo_args(lbl) )
595
+ if not color:
596
+ plot_kwargs['color'] = colorder[iplot%len(colorder)]
597
+ plot_kwargs['labelfct'] = lambda x : thr_description
598
+ c.plot_xsect( **plot_kwargs )
599
+
600
+ if do_legend:
601
+ plt.legend()
602
+ if do_grid:
603
+ plt.grid()
604
+ t = 'DOS cutoff effect'
605
+ if cfg_params:
606
+ t += ' (%s)'%cfg_params.strip()
607
+ plt.title(t)
608
+ if do_show:
609
+ plt.show()
610
+
611
+ def apply_to( self, ncmatcomposer, *dos_labels_or_indices, lblmap = None, warn = True, cutoff = None ):
612
+ """Apply DOS curves to NCMATComposer objects, resulting in updates to
613
+ the relevant dyninfo sections. If lblmap is not given, the
614
+ determine_mapping_to_composer_labels() method is used to infer one
615
+ automatically.
616
+
617
+ If the cutoff value is provided, it will be applied to all DOS curves
618
+ first. Otherwise, a warning will be emitted and an ad-hoc threshold will
619
+ be supplied to any curves whose initial frequency point is not positive.
620
+
621
+ """
622
+ from .ncmat import NCMATComposer
623
+ assert isinstance(ncmatcomposer,NCMATComposer), ( "First argument in call to .apply"
624
+ "(..) must be an NCMATComposer object" )
625
+
626
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
627
+ if not selected:
628
+ return
629
+
630
+ from . import _common as nc_common
631
+ warnfct = nc_common.warn if warn else (lambda s : None)
632
+
633
+ lblmap = self.__determine_lblmap( selected, ncmatcomposer, lblmap = lblmap, warn = warn )
634
+ cutoff = ( self._parse_threshold( cutoff ) if cutoff is not None else None ) or 0.0
635
+ def _access_egrid( idx ):
636
+ return self.__d['doslist'][idx][1]
637
+ selected_needscutoff = [ idx for idx in selected if not _access_egrid(idx)[0] > cutoff ]
638
+ if selected_needscutoff and not cutoff:
639
+ #must autodetermine a suitable cutoff. We take it as 1% of the maximum egrid value:
640
+ cutoff = 0.03 * max( _access_egrid(idx)[-1] for idx in selected_needscutoff )
641
+ warnfct(f'Applying an ad-hoc lower DOS egrid cutoff of {cutoff:g}eV since the cutoff parameter'
642
+ ' was not provided. For important work, it is recommended to instead select one'
643
+ ' explicitly (probably after investigating - for instance with the .plot_*() methods).')
644
+
645
+ o = self.apply_cutoff( cutoff, *selected_needscutoff ) if (cutoff and selected_needscutoff) else self
646
+ if not lblmap:
647
+ warnfct('Not applying any DOS curves to NCMATComposer object'
648
+ ' (perhaps try again with the lblmap argument).')
649
+ return
650
+ for srclbl, tgtlbl in sorted(lblmap.items()):
651
+ diargs = o.get_dyninfo_args(srclbl)
652
+ assert diargs['vdos_egrid'][0] > 0.0
653
+ ncmatcomposer.set_dyninfo_vdos( tgtlbl,
654
+ comment = f'From PhononDOSAnalyser ("{self.source_name}", atom with label "{srclbl}")',
655
+ **diargs )
656
+
657
+ def is_regular( self, *dos_labels_or_indices ):
658
+ """A regular DOS has an equidistant energy grid whose first point, e0,
659
+ is positive and a binwidth which divides e0 an even number of times."""
660
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
661
+ if not selected:
662
+ return True
663
+ return all( _vdos_egrid_is_regular(self.__d['doslist'][idx][1]) for idx in selected )
664
+
665
+ def requires_cutoff( self, *dos_labels_or_indices ):
666
+ """Whether or not any of the selected curves has an initial energy grid
667
+ point which is not positive."""
668
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
669
+ return any( (not (self.__d['doslist'][idx][1][0]>0.0) ) for idx in selected )
670
+
671
+ def get_dyninfo_args( self, label_or_idx ):
672
+ """returns a dict with 'vdos_egrid' and 'vdos' keys, suitable for usage
673
+ when calling NCMATComposer.set_dyninfo_vdos(..)"""
674
+ lbl,egrid,dos = self.__d['doslist'][self.__dosidx( label_or_idx )]
675
+ return dict( vdos_egrid = egrid, vdos = dos )
676
+
677
+ def determine_mapping_to_composer_labels( self, ncmatcomposer, warn = True ):
678
+ """Try to determine a mapping between labels in this object and an
679
+ NCMATComposer object. The mapping will either be based on Z-values or
680
+ the actual label names themselves.
681
+
682
+ Unless warn=False, warnings will be emitted when a label could not be mapped.
683
+
684
+ """
685
+ from .ncmat import NCMATComposer
686
+ assert isinstance(ncmatcomposer,NCMATComposer), ( "First argument in call to .determine"
687
+ "_mapping_to_composer_labels(..) must be an NCMATComposer object" )
688
+ #First check if any labels can be mapped based on Z-values:
689
+ from . import _common as nc_common
690
+ from . import atomdata as nc_atomdata
691
+ warnfct = nc_common.warn if warn else (lambda s : None)
692
+ lbl2z = {}
693
+ self_labels = self.labels
694
+ for lbl in self_labels:
695
+ elemiso = nc_common.check_elem_or_isotope_marker( lbl )
696
+ if elemiso:
697
+ lbl2z[lbl] = nc_atomdata.elementNameToZValue(elemiso,allow_isotopes=True)
698
+ lbl2tgt_zbased = {}
699
+ for z in set(lbl2z.values()):
700
+ lbls = set( _lbl for _lbl,_z in lbl2z.items() if _z==z )
701
+ if len(lbls) == 1:
702
+ lbl = lbls.pop()
703
+ tgtlbl = ncmatcomposer.find_label( z, allow_multi = False )
704
+ if tgtlbl:
705
+ lbl2tgt_zbased[lbl] = tgtlbl
706
+
707
+ #Now, see if we can map based on having the same label in the two objects:
708
+ common_lbls = set(self_labels).intersection(set( ncmatcomposer.get_labels() ))
709
+
710
+ res = {}
711
+ for lbl in self_labels:
712
+ tgt_z = lbl2tgt_zbased.get(lbl)
713
+ tgt_lblname = lbl if lbl in common_lbls else None
714
+ if tgt_z and tgt_lblname and tgt_z != tgt_lblname:
715
+ warnfct( f'Could not map atom with PhononDOSAnalyser label "{lbl}" '
716
+ 'unambiguously to NCMATComposer (Z value implies mapping to'
717
+ f' "{tgt_z}" but the label name implies the target label "{tgt_lblname}")')
718
+ else:
719
+ tgtlbl = tgt_z or tgt_lblname
720
+ if tgtlbl:
721
+ res[lbl] = tgtlbl
722
+ else:
723
+ warnfct( f'Could not map atom with PhononDOSAnalyser label "{lbl}" '
724
+ 'unambiguously to NCMATComposer label' )
725
+ return res
726
+
727
+ def __dosidx( self, label_or_idx ):
728
+ import numbers
729
+ ll = self.__d['doslist']
730
+ if isinstance(label_or_idx,numbers.Integral):
731
+ i = int( label_or_idx )
732
+ if not 0 <= i < len(ll):
733
+ from .exceptions import NCBadInput
734
+ raise NCBadInput('Index out of range (%i is not in range 0..%i'%(i,len(ll)-1))
735
+ return i
736
+ _ = [i for i,(lbl,_,_) in enumerate(ll) if lbl == label_or_idx ]
737
+ if not _:
738
+ from .exceptions import NCBadInput
739
+ _ = '","'.join(lbl for lbl,_,_ in ll)
740
+ raise NCBadInput('Invalid label "%s" (available labels are "%s")'%(label_or_idx,_))
741
+ assert len(_) == 1
742
+ return _[0]
743
+
744
+ def __dosidxlist( self, *dos_labels_or_indices, default_is_all = False ):
745
+ if not dos_labels_or_indices:
746
+ return list( range( self.ndos ) ) if default_is_all else []
747
+ res = []
748
+ for ll in dos_labels_or_indices:
749
+ if not hasattr(ll,'__len__') or hasattr(ll,'startswith'):
750
+ #single item
751
+ res.append( self.__dosidx( ll ) )
752
+ else:
753
+ res += [ self.__dosidx( e ) for e in ll ]
754
+ return res
755
+
756
+ def __sjolanderGn_args( self, selected, n=1, masses = None, temperature = 293.15 ):
757
+ assert 1<=n<=9999
758
+ assert temperature >= 0.001
759
+ from .exceptions import NCBadInput
760
+ if masses is None:
761
+ masses = []
762
+ from .atomdata import atomDB
763
+ for idx in selected:
764
+ lbl = self.__d['doslist'][idx][0]
765
+ ad = atomDB(lbl,throwOnErrors=False)
766
+ if not ad:
767
+ raise NCBadInput( 'Can not plot Gn function for label "lbl" which does'
768
+ ' not correspond to a known element or isotope. Either'
769
+ ' change the label with .update_label(..), or directly'
770
+ ' provide masses using the "masses" parameter' )
771
+ masses.append( ad.averageMassAMU() )
772
+ if len(masses) != len(selected):
773
+ raise NCBadInput( 'Invalid number of masses provided' )
774
+ return dict( n = n, masses = masses, temperature=temperature )
775
+
776
+ def __colorder( self ):
777
+ from ._common import _palette_Few as _palette
778
+ return [ _palette[e] for e in ['red',
779
+ 'blue',
780
+ 'orange',
781
+ 'green',
782
+ 'purple',
783
+ 'yellow',
784
+ 'pink',
785
+ 'gray',
786
+ ]]
787
+
788
+ def __plot( self, *dos_labels_or_indices, do_newfig = True, do_show = True, logy=None, unit = 'eV',
789
+ color_offset = 0, labelfct = None, do_legend=True,do_grid=True, sjolanderGn = None,
790
+ ymin=None, ymax = None, xmin=None, xmax=None ):
791
+
792
+ selected = self.__dosidxlist( *dos_labels_or_indices, default_is_all = True )
793
+ gnfcts = {}
794
+ if sjolanderGn is not None:
795
+ for idx, mass in zip(selected,sjolanderGn['masses']):
796
+ lbl, egrid, dos = self.__d['doslist'][idx]
797
+ if not egrid[0]>0.0:
798
+ from .exceptions import NCBadInput
799
+ raise NCBadInput('Can not plot Gn functions for DOS curves whose initial grid'
800
+ ' point is not at a positive value (use apply_cutoff(..) to rectify')
801
+ gnfcts[idx] = extractGn( vdos = (egrid,dos),
802
+ n = sjolanderGn['n'],
803
+ mass_amu = mass,
804
+ temperature = sjolanderGn['temperature'] )
805
+
806
+
807
+
808
+ unitname,unitfactor = _parsevdosunit( unit )
809
+ from .plot import _import_matplotlib_plt
810
+ plt = _import_matplotlib_plt()
811
+ if do_newfig:
812
+ plt.figure()
813
+ from ._numpy import _ensure_numpy, _np_linspace
814
+
815
+ colorder = self.__colorder()
816
+ for idx,(lbl, egrid, dos) in enumerate( self.__d['doslist'] ):
817
+ if idx not in selected:
818
+ continue
819
+ gnfct = gnfcts.get(idx)
820
+ color = colorder[(color_offset+idx)%len(colorder)]
821
+ actual_lbl = lbl if labelfct is None else labelfct(lbl)
822
+ if gnfct:
823
+ _x,_y = gnfct[0]/unitfactor, gnfct[1]
824
+ if _is_unit_test:
825
+ def _fixup_y(y):
826
+ from ._numpy import _ensure_numpy, _np
827
+ _ensure_numpy()
828
+ assert y.min() >= 0.0
829
+ return _np.clip(y,y.max()*1e-13,None)
830
+ _y = _fixup_y(_y)#discard tiny values
831
+ plt.plot( _x,_y,
832
+ label = actual_lbl,
833
+ color = color )
834
+ else:
835
+ plt.plot( egrid/unitfactor, dos,
836
+ label = actual_lbl,
837
+ color = color )
838
+ if egrid[0]>0.0:
839
+ _k = dos[0] / egrid[0]**2
840
+ _ensure_numpy()
841
+ _x = _np_linspace(0.0, egrid[0], 2000+2)[1:-1]
842
+ plt.plot( _x/unitfactor, _k*(_x**2),ls=':',color=color)
843
+
844
+ plt.xlabel('Frequency (%s)'%unitname)
845
+ if sjolanderGn is not None:
846
+ plt.ylabel('G%i (arbitrary scale)'%sjolanderGn['n'])
847
+ else:
848
+ plt.ylabel('DOS (arbitrary scale)')
849
+ if ymin is not None or ymax is not None:
850
+ plt.ylim(ymin,ymax)
851
+ if xmin is not None or xmax is not None:
852
+ plt.xlim(xmin,xmax)
853
+ from .plot import _plt_final
854
+ _plt_final(do_grid,do_legend,do_show,logy=logy)
855
+
856
+ def __clone( self ):
857
+ return PhononDOSAnalyser( ('__internal_state__',self.__d) )
858
+
859
+ def _parse_threshold( self, value ):
860
+ import numbers
861
+ from ._common import _decodeflt
862
+ from .exceptions import NCBadInput
863
+ def impl(x):
864
+ if isinstance(x,numbers.Real):
865
+ return float(x)
866
+ if isinstance(x,str):
867
+ x=x.strip()
868
+ v = None
869
+ for un,unval in vdos_units_2_eV.items():
870
+ if x.endswith(un):
871
+ v = _decodeflt(x[:-len(un)].strip())
872
+ if v is not None:
873
+ return v *unval
874
+ if v is None:
875
+ raise NCBadInput(f'Invalid threshold string: {x}')
876
+ if hasattr(x,'__len__') and len(x)==2 and isinstance(x[1],str):
877
+ _,unitfactor = _parsevdosunit( x[1] )
878
+ v = _decodeflt( x[0] )
879
+ if v is None:
880
+ raise NCBadInput(f'Invalid threshold value: {x[0]}')
881
+ return v*unitfactor
882
+ raise NCBadInput(f'Invalid threshold: {x}')
883
+
884
+ v = impl(value)
885
+ if not v>0.0 or not v < 1e6:
886
+ raise NCBadInput(f'Invalid threshold value (out of range): {v:g}')
887
+ return v
888
+
889
+ def __determine_lblmap( self, selected, ncmatcomposer, lblmap = None, warn = True ):
890
+ from .exceptions import NCBadInput
891
+ if lblmap is None:
892
+ lblmap = self.determine_mapping_to_composer_labels( ncmatcomposer, warn = warn )
893
+ else:
894
+ #use provided lblmap, but with a few sanity checks:
895
+ composer_lbls = ncmatcomposer.get_labels()
896
+ missing = set(lblmap.values()).difference(composer_lbls)
897
+ def fmtlabellist(lbls):
898
+ return ( '"%s"'%('", "'.join(lbls)) if lbls else '' )
899
+ if missing:
900
+ raise NCBadInput('Some values in lblmap are not present in provided'
901
+ f' NCMATComposer object: {fmtlabellist(missing)} (the'
902
+ f' following labels are available: {fmtlabellist(composer_lbls)})')
903
+ missing = set(lblmap.keys()).difference(self.labels)
904
+ if missing:
905
+ raise NCBadInput('Some keys in lblmap are not actual labels in PhononDOSAnalyser'
906
+ f' object: {fmtlabellist(missing)} (the'
907
+ f' following labels are available: {fmtlabellist(self.labels)})')
908
+ lblmap = dict( lblmap.items() )
909
+ res = {}
910
+ for k,v in lblmap.items():
911
+ idx = self.__dosidx(k)
912
+ if idx in selected:
913
+ res[k] = v
914
+ return res
915
+
916
+
917
+ def _read_quantumespresso( raw_text_data ):
918
+ assert isinstance(raw_text_data,str)
919
+ _data_lines = raw_text_data.splitlines()
920
+
921
+ def _get_header():
922
+ out=[]
923
+ for ll in _data_lines:
924
+ ll = ll.strip()
925
+ if not ll:
926
+ continue
927
+ if not ll.startswith('#'):
928
+ break
929
+ out.append( ' '.join(ll[1:].split()) )
930
+ return out
931
+
932
+ hdr = _get_header()
933
+ _expected_hdr = 'Frequency[cm^-1] DOS PDOS'
934
+ if not hdr or _expected_hdr not in hdr:
935
+ from .exceptions import NCBadInput
936
+ raise NCBadInput('Invalid input format. Did not find expected header line "# %s"'%_expected_hdr)
937
+ from ._numpy import _ensure_numpy, _np
938
+ _ensure_numpy()
939
+
940
+ rawdata = _np.loadtxt( _data_lines )
941
+ assert len( rawdata.shape ) == 2
942
+ nrows, ncols = rawdata.shape if len( rawdata.shape ) == 2 else ( 0, 0 )
943
+ if not nrows >= 10 or not ncols >= 3:
944
+ from .exceptions import NCBadInput
945
+ raise NCBadInput('Invalid input format. Expected at least 3 columns and 10 rows')
946
+ npdos = ncols - 2
947
+
948
+ def _get_col( icol ):
949
+ return rawdata[:,icol]
950
+
951
+ egrid = vdos_units_2_eV['1/cm'] * _get_col( 0 )
952
+
953
+ doslist = [ ( 'combined_dos', egrid, _get_col( 1 ) ) ]
954
+ doslist += [ ('pdos%i'%ipdos, egrid, _get_col(2+ipdos) ) for ipdos in range(npdos)]
955
+ return doslist
956
+
957
+ def _vdos_egrid_is_regular( egrid ):
958
+ if not ( egrid[0] > 0.0 ):
959
+ return False
960
+ from ._common import _grid_is_linspace
961
+ if not _grid_is_linspace( egrid ):
962
+ return False
963
+ bw = ( egrid[-1]-egrid[0] ) / ( len(egrid) - 1 )
964
+ x = egrid[0] / bw
965
+ #regular if x is very near an integer >= 1
966
+ nx = round(x)
967
+ return nx >= 1 and abs(x-nx) < 1e-4
968
+
969
+ def _do_regularise( egrid, density, n, quiet = False):
970
+ #Regularisation function adapted from ncrystal_vdos2ncmat
971
+
972
+ if n >= len(egrid) and _vdos_egrid_is_regular(egrid):
973
+ #Already fine, won't get better just by wasting more points!
974
+ return egrid,density
975
+
976
+ from ._numpy import _ensure_numpy, _np, _np_linspace
977
+ _ensure_numpy()
978
+
979
+ egrid = _np.asarray(egrid,dtype=float)
980
+ density = _np.asarray(density,dtype=float)
981
+ assert len(egrid)==len(density)
982
+ assert len(egrid) >= 2
983
+ assert egrid[0] > 0.0
984
+
985
+ emin,emax=egrid[0],egrid[-1]
986
+ if quiet:
987
+ def nc_print( *a,**kw ):
988
+ pass
989
+ else:
990
+ from ._common import print as nc_print
991
+ nc_print('old range',emin,emax)
992
+ THZ = vdos_units_2_eV['THz']
993
+ nc_print('old range [THz]',emin/THZ,emax/THZ)
994
+
995
+ for k in range(1,1000000000):
996
+ #k is number of bins below emin, an integral number by definition in a regularised grid.
997
+ binwidth = emin/k
998
+ nbins=int(_np.floor((emax-emin)/binwidth))+1
999
+ eps = (emin+nbins*binwidth)-emax
1000
+ assert eps>=0.0
1001
+ if nbins+1 >= n:
1002
+ break
1003
+ n=nbins+1
1004
+ binwidth = emin/k
1005
+ new_emax = emin + (n-1) * binwidth
1006
+ if abs( (new_emax-binwidth) - emax ) < 1e-3*binwidth:
1007
+ nbins -= 1
1008
+ n -= 1
1009
+ new_emax -= binwidth
1010
+ nc_print(f" ==> Choosing regular grid with n={n} pts from emin={emin} to emax={new_emax} ({new_emax-emax} beyond old emax)")
1011
+ def retry():
1012
+ from ._common import warn
1013
+ nnew = n+100
1014
+ warn('Something went wrong in DOS regularisation with n={n}. Retrying with n={nnew}.')
1015
+ return _do_regularise(egrid,density,n = nnew, quiet = quiet )
1016
+ if not ( new_emax >= emax-binwidth*1.001e-3 ):
1017
+ return retry()
1018
+ if not ( new_emax >= emax-binwidth*1.001e-3 ):
1019
+ return retry()
1020
+ new_egrid = _np_linspace(emin,new_emax,n)
1021
+ test=new_egrid[0] / ( (new_egrid[-1]-new_egrid[0])/(len(new_egrid)-1) )
1022
+ if not abs(round(test)-test)<1e-6:
1023
+ return retry()
1024
+ new_density = _np.interp(new_egrid,egrid,density, left=0.0, right=0.0)
1025
+ nc_print('last density values in new grid:',new_density[-5:])
1026
+ return new_egrid,new_density
1027
+
1028
+ def _parsevdosunit( name ):
1029
+ unitfactor = vdos_units_2_eV.get(name or 'eV')
1030
+ if unitfactor:
1031
+ return name, unitfactor
1032
+ from .exceptions import NCBadInput
1033
+ _ = '","'.join(u for u in sorted(vdos_units_2_eV.keys()))
1034
+ raise NCBadInput('Invalid frequency unit "%s" (must be one of "%s")'%(name,_))