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.
- NCrystal/__init__.py +85 -0
- NCrystal/__main__.py +98 -0
- NCrystal/_chooks.py +854 -0
- NCrystal/_cli_cif2ncmat.py +269 -0
- NCrystal/_cli_endf2ncmat.py +503 -0
- NCrystal/_cli_hfg2ncmat.py +144 -0
- NCrystal/_cli_mcstasunion.py +74 -0
- NCrystal/_cli_ncmat2cpp.py +31 -0
- NCrystal/_cli_ncmat2hkl.py +180 -0
- NCrystal/_cli_nctool.py +1018 -0
- NCrystal/_cli_vdos2ncmat.py +463 -0
- NCrystal/_cli_verifyatompos.py +257 -0
- NCrystal/_cliimpl.py +307 -0
- NCrystal/_cliwrap_config.py +36 -0
- NCrystal/_common.py +499 -0
- NCrystal/_coreimpl.py +114 -0
- NCrystal/_hfgdata.py +546 -0
- NCrystal/_hklobjects.py +136 -0
- NCrystal/_is_std.py +0 -0
- NCrystal/_locatelib.py +210 -0
- NCrystal/_miscimpl.py +354 -0
- NCrystal/_mmc.py +757 -0
- NCrystal/_msg.py +60 -0
- NCrystal/_ncmat2cpp_impl.py +445 -0
- NCrystal/_ncmatimpl.py +2131 -0
- NCrystal/_numpy.py +76 -0
- NCrystal/_testimpl.py +579 -0
- NCrystal/api.py +56 -0
- NCrystal/atomdata.py +177 -0
- NCrystal/cfgstr.py +77 -0
- NCrystal/cifutils.py +1795 -0
- NCrystal/cli.py +96 -0
- NCrystal/constants.py +134 -0
- NCrystal/core.py +1910 -0
- NCrystal/datasrc.py +226 -0
- NCrystal/exceptions.py +66 -0
- NCrystal/hfg2ncmat.py +270 -0
- NCrystal/mcstasutils.py +438 -0
- NCrystal/misc.py +317 -0
- NCrystal/mmc.py +35 -0
- NCrystal/ncmat.py +778 -0
- NCrystal/ncmat2cpp.py +80 -0
- NCrystal/obsolete.py +67 -0
- NCrystal/plot.py +484 -0
- NCrystal/plugins.py +49 -0
- NCrystal/test.py +76 -0
- NCrystal/vdos.py +1034 -0
- ncrystal_python-3.9.81.dist-info/LICENSE +206 -0
- ncrystal_python-3.9.81.dist-info/METADATA +515 -0
- ncrystal_python-3.9.81.dist-info/RECORD +53 -0
- ncrystal_python-3.9.81.dist-info/WHEEL +5 -0
- ncrystal_python-3.9.81.dist-info/entry_points.txt +10 -0
- 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,_))
|