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/_ncmatimpl.py
ADDED
|
@@ -0,0 +1,2131 @@
|
|
|
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
|
+
"""
|
|
23
|
+
|
|
24
|
+
Internal implementation details for NCMAT utilities in ncmat.py
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__all__ = []
|
|
29
|
+
from . import core as _nc_core
|
|
30
|
+
from . import _common as _nc_common
|
|
31
|
+
import math
|
|
32
|
+
import copy
|
|
33
|
+
|
|
34
|
+
_magic_two_space=b'\xc3\x98@'.decode('utf8')
|
|
35
|
+
|
|
36
|
+
class NCMATComposerImpl:
|
|
37
|
+
|
|
38
|
+
def set_plotlabel( self, lbl ):
|
|
39
|
+
if lbl != self.__params.get('plotlabel'):
|
|
40
|
+
self.__dirty()
|
|
41
|
+
self.__params['plotlabel'] = lbl
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def plotlabel( self ):
|
|
45
|
+
return self.__params.get('plotlabel')
|
|
46
|
+
|
|
47
|
+
def add_secondary_phase(self, fraction, cfgstr, normalise ):
|
|
48
|
+
if normalise:
|
|
49
|
+
from .cfgstr import normaliseCfg
|
|
50
|
+
c = normaliseCfg( cfgstr )
|
|
51
|
+
else:
|
|
52
|
+
c = ' '.join(str(cfgstr).split())
|
|
53
|
+
if not c:
|
|
54
|
+
raise _nc_core.NCBadInput('empty cfg-string used to define secondary phase')
|
|
55
|
+
if '#' in c:
|
|
56
|
+
raise _nc_core.NCBadInput('cfg-strings used to define secondary phases in NCMAT data can not contain "#" characters')
|
|
57
|
+
if len(c.splitlines())>1 or '\n' in c or '\r' in c:
|
|
58
|
+
raise _nc_core.NCBadInput('cfg-strings used to define secondary phases in NCMAT data can not contain newline characters')
|
|
59
|
+
if not c.isascii():
|
|
60
|
+
raise _nc_core.NCBadInput('cfg-strings used to define secondary phases in NCMAT data must be pure ASCII.')
|
|
61
|
+
p = self.__params.get('secondary_phases',[])[:]
|
|
62
|
+
p.append( ( float(fraction), c) )
|
|
63
|
+
ftot = math.fsum([_f for _f,_ in p])
|
|
64
|
+
if ftot >= 1.0-1e-14:
|
|
65
|
+
raise _nc_core.NCBadInput('Secondary phases have too high a fraction (a total of {ftot*100.0}%%)')
|
|
66
|
+
self.__dirty()
|
|
67
|
+
self.__params['secondary_phases'] = p
|
|
68
|
+
|
|
69
|
+
def add_hard_sphere_sans_model( self, sphere_radius ):
|
|
70
|
+
assert sphere_radius > 0.0
|
|
71
|
+
self.__dirty()
|
|
72
|
+
self.__params['custom_hardspheresans'] = float( sphere_radius )
|
|
73
|
+
|
|
74
|
+
def add_raw_content( self, content ):
|
|
75
|
+
if not isinstance(content,str):
|
|
76
|
+
raise _nc_core.NCBadInput('Invalid raw content (must be a string)')
|
|
77
|
+
if not content:
|
|
78
|
+
return
|
|
79
|
+
self.__dirty()
|
|
80
|
+
if 'raw_content' in self.__params:
|
|
81
|
+
self.__params['raw_content'] += str(content)
|
|
82
|
+
else:
|
|
83
|
+
self.__params['raw_content'] = str(content)
|
|
84
|
+
|
|
85
|
+
def get_raw_content( self ):
|
|
86
|
+
return self.__params.get('raw_content','')
|
|
87
|
+
|
|
88
|
+
def clear_raw_content( self ):
|
|
89
|
+
if 'raw_content' in self.__params:
|
|
90
|
+
self.__dirty()
|
|
91
|
+
del self.__params['raw_content']
|
|
92
|
+
|
|
93
|
+
def get_custom_section_data( self, section_name ):
|
|
94
|
+
if section_name is None:
|
|
95
|
+
dd = self.__params.get('custom_sections')
|
|
96
|
+
return copy.deepcopy(dd) if dd else {}
|
|
97
|
+
_check_valid_custom_section_name(section_name)
|
|
98
|
+
if 'custom_sections' in self.__params:
|
|
99
|
+
return self.__params['custom_sections'].get(section_name)
|
|
100
|
+
|
|
101
|
+
def set_custom_section_data( self, section_name, content ):
|
|
102
|
+
if section_name == 'HARDSPHERESANS':
|
|
103
|
+
raise _nc_core.NCBadInput('For HARDSPHERESANS use the'
|
|
104
|
+
' .add_hard_sphere_sans_model() method'
|
|
105
|
+
' instead of set_custom_section_data.')
|
|
106
|
+
if section_name == 'UNOFFICIALHACKS':
|
|
107
|
+
raise _nc_core.NCBadInput('Do not set the @CUSTOM_UNOFFICIALHACKS'
|
|
108
|
+
' content directly with '
|
|
109
|
+
'set_custom_section_data.')
|
|
110
|
+
_check_valid_custom_section_name(section_name)
|
|
111
|
+
if not isinstance(content,str):
|
|
112
|
+
raise _nc_core.NCBadInput('Invalid custom section data'
|
|
113
|
+
' content (must be a string)')
|
|
114
|
+
if self.__params.get('custom_sections',{}).get(section_name) == content:
|
|
115
|
+
return
|
|
116
|
+
self.__dirty()
|
|
117
|
+
if 'custom_sections' not in self.__params:
|
|
118
|
+
self.__params['custom_sections'] = {}
|
|
119
|
+
self.__params['custom_sections'][section_name] = content
|
|
120
|
+
|
|
121
|
+
def clear_custom_section_data( self, section_name ):
|
|
122
|
+
if section_name is not None:
|
|
123
|
+
_check_valid_custom_section_name(section_name)
|
|
124
|
+
if 'custom_sections' not in self.__params:
|
|
125
|
+
return
|
|
126
|
+
if section_name is None:
|
|
127
|
+
self.__dirty()
|
|
128
|
+
del self.__params['custom_sections']
|
|
129
|
+
else:
|
|
130
|
+
if section_name not in self.__params['custom_sections']:
|
|
131
|
+
return
|
|
132
|
+
self.__dirty()
|
|
133
|
+
del self.__params['custom_sections'][section_name]
|
|
134
|
+
|
|
135
|
+
def __init__(self, *, data, fmt, quiet ):
|
|
136
|
+
self._ichange = 0#increment on all changes, for downstream caching
|
|
137
|
+
self.__loadcache = None
|
|
138
|
+
self.__params = {}
|
|
139
|
+
assert not (fmt is not None and data is None)
|
|
140
|
+
if data is None:
|
|
141
|
+
return
|
|
142
|
+
if isinstance( data, NCMATComposerImpl ):
|
|
143
|
+
self.__params = data.clone().__params
|
|
144
|
+
return
|
|
145
|
+
if fmt == 'via_ase':
|
|
146
|
+
data= NCMATComposerImpl._ase_io_read( data, quiet = quiet )
|
|
147
|
+
fmt = 'ase'
|
|
148
|
+
if fmt is None and isinstance( data, _nc_core.TextData ) and data.dataType == 'ncmat':
|
|
149
|
+
fmt = 'ncmat'
|
|
150
|
+
if fmt is None:
|
|
151
|
+
if isinstance(data,dict) or ( hasattr(data,'keys') and hasattr(data,'get') ):
|
|
152
|
+
pass#Might be a dictionary from to_dict() but to be safe we do not autodetect this!
|
|
153
|
+
elif isinstance(data,_nc_core.Info):
|
|
154
|
+
fmt = 'info'
|
|
155
|
+
elif hasattr(data,'__fspath__'):
|
|
156
|
+
import pathlib
|
|
157
|
+
_ = pathlib.Path(data).name.lower()
|
|
158
|
+
if _.endswith('.ncmat'):
|
|
159
|
+
fmt='ncmat'
|
|
160
|
+
if _.endswith('.cif'):
|
|
161
|
+
fmt='cif'
|
|
162
|
+
elif hasattr(data,'startswith') and hasattr(data,'__contains__'):
|
|
163
|
+
if '\n' in data:
|
|
164
|
+
if data.startswith('NCMAT'):
|
|
165
|
+
fmt='ncmat'
|
|
166
|
+
elif 'loop_' in data and '_atom_site' in data:
|
|
167
|
+
fmt='cif'
|
|
168
|
+
elif data.startswith('codid::') or data.startswith('mpid::') or data.lower().endswith('.cif'):
|
|
169
|
+
fmt='cif'
|
|
170
|
+
elif data.lower().endswith('.ncmat'):
|
|
171
|
+
fmt='cfgstr'
|
|
172
|
+
else:
|
|
173
|
+
fmt='cfgstr'
|
|
174
|
+
elif NCMATComposerImpl._is_ase_like_object( data ):
|
|
175
|
+
fmt = 'ase'
|
|
176
|
+
|
|
177
|
+
if fmt=='__internal_state__':
|
|
178
|
+
self.__params = dict( copy.deepcopy( (k,v) ) for k,v in data.items() )
|
|
179
|
+
elif fmt=='cif':
|
|
180
|
+
o = NCMATComposerImpl.from_cif(data,quiet=quiet)
|
|
181
|
+
self.__params = o.__params
|
|
182
|
+
elif fmt=='ncmat':
|
|
183
|
+
o = NCMATComposerImpl.from_ncmat(data)
|
|
184
|
+
self.__params = o.__params
|
|
185
|
+
elif fmt=='cfgstr':
|
|
186
|
+
o = NCMATComposerImpl.from_cfgstr(data)
|
|
187
|
+
self.__params = o.__params
|
|
188
|
+
elif fmt=='info':
|
|
189
|
+
o = NCMATComposerImpl.from_info(data)
|
|
190
|
+
self.__params = o.__params
|
|
191
|
+
elif fmt=='ase':
|
|
192
|
+
o = NCMATComposerImpl.from_ase(data,quiet=quiet)
|
|
193
|
+
self.__params = o.__params
|
|
194
|
+
else:
|
|
195
|
+
raise _nc_core.NCBadInput('Unknown data type (specify with the fmt keyword if this is just an auto-detection issue))')
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def from_cif( cifsrc, quiet, mp_apikey = None, uiso_temperature = None, override_spacegroup = None, **kwargs ):
|
|
199
|
+
from . import cifutils as nc_cifutils
|
|
200
|
+
lc = nc_cifutils.CIFLoader( cifsrc, quiet = quiet, mp_apikey = mp_apikey,
|
|
201
|
+
override_spacegroup = override_spacegroup )
|
|
202
|
+
return lc.create_ncmat_composer( quiet = quiet,
|
|
203
|
+
uiso_temperature = uiso_temperature,
|
|
204
|
+
**kwargs )._get_impl_obj()
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def from_cfgstr( cfgstr ):
|
|
208
|
+
info = _nc_core.createInfo( cfgstr )
|
|
209
|
+
return NCMATComposerImpl.from_info( info )
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _is_ase_like_object( o ):
|
|
213
|
+
return all( hasattr( o, e ) for e in ( 'cell', 'get_scaled_positions', 'get_atomic_numbers' ) )
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _ase_io_read( obj, quiet, fmt = None ):
|
|
217
|
+
if not quiet:
|
|
218
|
+
_nc_common.print('Trying to load object via ase.io.read')
|
|
219
|
+
ase, ase_io = _import_ase()
|
|
220
|
+
return ase_io.read( obj, format = fmt )
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def from_ase( ase_obj, quiet, ase_format = None, **kwargs ):
|
|
224
|
+
_nc_common.warn('Trying to load ASE object. Note that such objects might lack'
|
|
225
|
+
' information including space group and atomic displacements.')
|
|
226
|
+
if not NCMATComposerImpl._is_ase_like_object( ase_obj ):
|
|
227
|
+
arg = _nc_common.extract_path( ase_obj )
|
|
228
|
+
if arg is None:
|
|
229
|
+
if ase_format is None:
|
|
230
|
+
_nc_common.warn('Anonymous text data might not be recognised by ASE.'
|
|
231
|
+
' Consider specifying the format explicitly in this case (e.g. supply ase_format="cif").')
|
|
232
|
+
from .misc import AnyTextData
|
|
233
|
+
td = AnyTextData(ase_obj)
|
|
234
|
+
import io
|
|
235
|
+
with io.BytesIO(td.content.encode()) as memfile:
|
|
236
|
+
ase_obj = NCMATComposerImpl._ase_io_read( memfile, quiet = quiet, fmt = ase_format )
|
|
237
|
+
else:
|
|
238
|
+
ase_obj = NCMATComposerImpl._ase_io_read( arg, quiet = quiet, fmt = ase_format )
|
|
239
|
+
|
|
240
|
+
assert NCMATComposerImpl._is_ase_like_object( ase_obj )
|
|
241
|
+
|
|
242
|
+
# Note1: We support this through CIF, since site occupancies are
|
|
243
|
+
# currently not available in a standard way through the OO interface
|
|
244
|
+
# (cf. https://gitlab.com/ase/ase/-/issues/263).
|
|
245
|
+
#
|
|
246
|
+
# Note2: Atomic displacement info seems to be ignored.
|
|
247
|
+
#
|
|
248
|
+
# Note3: D is detected as H (cf. https://matsci.org/t/how-to-load-structures-including-deuterium-with-ase/41950/2).
|
|
249
|
+
cifdata = _cifdata_via_ase( ase_obj, ase_format = 'ase', quiet = quiet )
|
|
250
|
+
if 'no_formula_check' not in kwargs:
|
|
251
|
+
#ASE cif writer puts formula which does not take site occupancies into account:
|
|
252
|
+
kwargs['no_formula_check'] = True
|
|
253
|
+
o = NCMATComposerImpl.from_cif( cifsrc = cifdata, quiet=quiet, **kwargs )
|
|
254
|
+
o.add_comments('NOTICE: Data here is based on an ASE (Atoms)'
|
|
255
|
+
' structure through a conversion to CIF format.',
|
|
256
|
+
add_empty_line_divider=True)
|
|
257
|
+
return o
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def from_info( info_obj ):
|
|
261
|
+
return _composerimpl_from_info( info_obj )
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def from_ncmat( data, keep_header = False ):
|
|
265
|
+
from .misc import AnyTextData
|
|
266
|
+
td = AnyTextData(data)
|
|
267
|
+
if '\n' not in td.content and not td.content.startswith('NCMAT'):
|
|
268
|
+
return NCMATComposerImpl.from_cfgstr( td.content )
|
|
269
|
+
o = _nc_core.directLoad( td, doScatter = False, doAbsorption = False )
|
|
270
|
+
c = NCMATComposerImpl.from_info( o.info )
|
|
271
|
+
comments = None
|
|
272
|
+
if keep_header:
|
|
273
|
+
comments = _extractInitialHeaderCommentsFromNCMATData(td)
|
|
274
|
+
if comments:
|
|
275
|
+
_hr='---------------------------------------------------------'
|
|
276
|
+
comments = [
|
|
277
|
+
'>'*70,
|
|
278
|
+
'Comments found in NCMAT data from which the NCMATComposer',
|
|
279
|
+
'was initialised. Subsequent updates to the NCMATComposer',
|
|
280
|
+
'object might have invalidated some or all of these:'
|
|
281
|
+
]+ list('>>> '+e for e in comments) + ['<'*70]
|
|
282
|
+
if comments:
|
|
283
|
+
c.add_comments( comments, add_empty_line_divider = True )
|
|
284
|
+
return c
|
|
285
|
+
|
|
286
|
+
def update_atomdb( self, element_or_isotope, data, *, mass=None,
|
|
287
|
+
coh_scat_len=None, incoh_xs=None, abs_xs=None ):
|
|
288
|
+
label,res=_decode_update_atomdb( element_or_isotope,
|
|
289
|
+
data=data,mass=mass,
|
|
290
|
+
coh_scat_len=coh_scat_len,
|
|
291
|
+
incoh_xs=incoh_xs,
|
|
292
|
+
abs_xs=abs_xs )
|
|
293
|
+
self.__dirty()
|
|
294
|
+
if 'atomdb' not in self.__params:
|
|
295
|
+
self.__params['atomdb'] = {}
|
|
296
|
+
self.__params['atomdb'][label] = res
|
|
297
|
+
|
|
298
|
+
def find_label( self, element, allow_multi ):
|
|
299
|
+
import numbers
|
|
300
|
+
from .atomdata import elementNameToZValue
|
|
301
|
+
def _name2z( name ):
|
|
302
|
+
return elementNameToZValue( name, allow_isotopes = True ) or None
|
|
303
|
+
if isinstance(element,numbers.Integral):
|
|
304
|
+
search_Z = int( element )
|
|
305
|
+
else:
|
|
306
|
+
search_Z = _name2z( element ) or 0
|
|
307
|
+
if not 1<=search_Z<=150:
|
|
308
|
+
return [] if allow_multi else None
|
|
309
|
+
|
|
310
|
+
compos = self.__params.get('compositions',{})
|
|
311
|
+
def lbl_zval_iter( lbl ):
|
|
312
|
+
c = compos.get(lbl)
|
|
313
|
+
for _,name in ( c or [(None,lbl)] ):
|
|
314
|
+
z = _name2z(name)
|
|
315
|
+
if z:
|
|
316
|
+
yield z
|
|
317
|
+
ll = []
|
|
318
|
+
for lbl in self.get_labels():
|
|
319
|
+
if any( search_Z == z for z in lbl_zval_iter(lbl) ):
|
|
320
|
+
ll.append( lbl )
|
|
321
|
+
|
|
322
|
+
return ( list(sorted(ll))
|
|
323
|
+
if allow_multi
|
|
324
|
+
else ( ll[0] if len(ll)==1 else None ) )
|
|
325
|
+
|
|
326
|
+
def get_state_of_matter( self ):
|
|
327
|
+
return self.__params.get('state_of_matter')
|
|
328
|
+
|
|
329
|
+
def set_state_of_matter( self, state_of_matter ):
|
|
330
|
+
if not state_of_matter:
|
|
331
|
+
state_of_matter = None
|
|
332
|
+
allowed = ['solid','liquid','gas']
|
|
333
|
+
if state_of_matter not in allowed and state_of_matter is not None:
|
|
334
|
+
s='", "'.join(allowed)
|
|
335
|
+
raise _nc_core.NCBadInput(f'Invalid state of matter value "{state_of_matter}" (allowed: "{s}")')
|
|
336
|
+
if state_of_matter is None:
|
|
337
|
+
if 'state_of_matter' not in self.__params:
|
|
338
|
+
return
|
|
339
|
+
self.__dirty()
|
|
340
|
+
del self.__params['state_of_matter']
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
if self.__params.get('state_of_matter') == state_of_matter:
|
|
344
|
+
return
|
|
345
|
+
self.__dirty()
|
|
346
|
+
self.__params['state_of_matter'] = state_of_matter
|
|
347
|
+
|
|
348
|
+
def lock_temperature( self, value ):
|
|
349
|
+
if value is None:
|
|
350
|
+
if 'temperature' not in self.__params:
|
|
351
|
+
return
|
|
352
|
+
self.__params.pop('temperature',None)
|
|
353
|
+
self.__dirty()
|
|
354
|
+
return
|
|
355
|
+
v=float(value)
|
|
356
|
+
assert v>0.0
|
|
357
|
+
self.__dirty()
|
|
358
|
+
self.__params['temperature'] = { 'value' : v, 'lock' : True }
|
|
359
|
+
|
|
360
|
+
def set_default_temperature( self, value ):
|
|
361
|
+
if value is None:
|
|
362
|
+
self.__params.pop('temperature',None)
|
|
363
|
+
return
|
|
364
|
+
v=float(value)
|
|
365
|
+
assert v>0.0
|
|
366
|
+
self.__dirty()
|
|
367
|
+
self.__params['temperature'] = { 'value' : v, 'lock' : False }
|
|
368
|
+
|
|
369
|
+
def get_temperature_setting( self ):
|
|
370
|
+
_ = self.__params.get('temperature')
|
|
371
|
+
return (_['value'],_['lock']) if _ else (None,None)
|
|
372
|
+
|
|
373
|
+
def set_composition( self, label, *composition ):
|
|
374
|
+
label, composition = _decode_composition(label,*composition)
|
|
375
|
+
self.__dirty()
|
|
376
|
+
if 'compositions' not in self.__params:
|
|
377
|
+
self.__params['compositions'] = {}
|
|
378
|
+
self.__params['compositions'][ label ] = composition
|
|
379
|
+
|
|
380
|
+
def remap_atom( self, element_or_isotope, *composition ):
|
|
381
|
+
elem = _nc_common.check_elem_or_isotope_marker( element_or_isotope )
|
|
382
|
+
if not elem:
|
|
383
|
+
raise _nc_core.NCBadInput('Invalid element/isotope marker: "%s"'%element_or_isotope)
|
|
384
|
+
|
|
385
|
+
elem, composition = _decode_composition(elem,*composition)
|
|
386
|
+
compos = self.__params.get('compositions',{})
|
|
387
|
+
set_compos_args = []
|
|
388
|
+
for lbl in self.get_labels():
|
|
389
|
+
lblcompos = compos.get(lbl,None)
|
|
390
|
+
if lblcompos:
|
|
391
|
+
frac_elem = sum( fr for fr,en in lblcompos if elem==en )
|
|
392
|
+
if frac_elem > 0.0:
|
|
393
|
+
#update existing composition:
|
|
394
|
+
_ = [ (fr,en) for fr,en in lblcompos if elem!=en ]
|
|
395
|
+
_ += [ (frac_elem*fr,en) for fr,en in composition ]
|
|
396
|
+
set_compos_args.append( (lbl,_) )
|
|
397
|
+
else:
|
|
398
|
+
if lbl == elem:
|
|
399
|
+
set_compos_args.append( (lbl,composition) )
|
|
400
|
+
for lbl, _compos in set_compos_args:
|
|
401
|
+
self.set_composition( lbl, _compos )
|
|
402
|
+
|
|
403
|
+
def clear_comments( self ):
|
|
404
|
+
self.__dirty()#not strictly needed but to be safe
|
|
405
|
+
self.__params['top_comments'] = []
|
|
406
|
+
|
|
407
|
+
def add_comments( self, comments, add_empty_line_divider ):
|
|
408
|
+
is_str = isinstance(comments,str) or hasattr(comments,'split')
|
|
409
|
+
ll = comments if not is_str else list(comments.splitlines())
|
|
410
|
+
|
|
411
|
+
self.__dirty()#not strictly needed as comments do not affect loading
|
|
412
|
+
|
|
413
|
+
if 'top_comments' not in self.__params:
|
|
414
|
+
self.__params['top_comments'] = []
|
|
415
|
+
elif ( add_empty_line_divider
|
|
416
|
+
and self.__params['top_comments']
|
|
417
|
+
and self.__params['top_comments'][-1] ):
|
|
418
|
+
self.__params['top_comments'].append('')
|
|
419
|
+
|
|
420
|
+
for e in ll:
|
|
421
|
+
self.__params['top_comments'].append( self.__prunecomment(e) )
|
|
422
|
+
|
|
423
|
+
def __prunecomment(self,c):
|
|
424
|
+
return str(c).rstrip().replace('\t',' ')
|
|
425
|
+
|
|
426
|
+
def clone(self):
|
|
427
|
+
o = NCMATComposerImpl( data = self.__params, fmt = '__internal_state__', quiet = True )
|
|
428
|
+
o._ichange = self._ichange
|
|
429
|
+
o.__loadcache = self.__loadcache
|
|
430
|
+
return o
|
|
431
|
+
|
|
432
|
+
def __dirty(self):
|
|
433
|
+
self._ichange += 1
|
|
434
|
+
self.__loadcache = None
|
|
435
|
+
|
|
436
|
+
def get_cache_key( self ):
|
|
437
|
+
return ( int(id(self), self._ichange) )
|
|
438
|
+
|
|
439
|
+
def to_dict(self):
|
|
440
|
+
return copy.deepcopy(self.__params)
|
|
441
|
+
|
|
442
|
+
def set_cellsg( self, *, a,b,c, alpha,beta,gamma, spacegroup ):
|
|
443
|
+
#TODO: input validation! Also ensure high precision in numbers below
|
|
444
|
+
self.__dirty()
|
|
445
|
+
self.__params['cellsg'] = dict(a=a,b=b,c=c,alpha=alpha,beta=beta,gamma=gamma,spacegroup=spacegroup)
|
|
446
|
+
|
|
447
|
+
def set_cellsg_cubic( self, a, *, spacegroup ):
|
|
448
|
+
if not (spacegroup is None or 195<=int(spacegroup)<=230):
|
|
449
|
+
raise _nc_core.NCBadInput(f'Spacegroup {spacegroup} is not a cubic spacegroup (must be integer in range 195..230)')
|
|
450
|
+
self.set_cellsg( a=a,b=a,c=a,alpha=90.,beta=90.,gamma=90.,
|
|
451
|
+
spacegroup = ( int(spacegroup) if spacegroup else None ) )
|
|
452
|
+
|
|
453
|
+
def set_density( self, value, unit ):
|
|
454
|
+
allowed = ['g/cm3','kg/m3','atoms/Aa3']
|
|
455
|
+
if unit not in allowed:
|
|
456
|
+
s='", "'.join(allowed)
|
|
457
|
+
raise _nc_core.NCBadInput(f'Invalid density unit "{unit}" (allowed: "{s}")')
|
|
458
|
+
if not ( value>0.0 ):
|
|
459
|
+
raise _nc_core.NCBadInput(f'Invalid density value (must be >0): {value} {unit}')
|
|
460
|
+
self.__dirty()
|
|
461
|
+
self.__params['density'] = ( value, unit )
|
|
462
|
+
|
|
463
|
+
def set_fraction( self, label, value ):
|
|
464
|
+
_checklabel(label)
|
|
465
|
+
if not 0 < value <= 1.0:
|
|
466
|
+
raise _nc_core.NCBadInput(f'Invalid component fraction is not in (0,1]: {value}')
|
|
467
|
+
self.__dirty()
|
|
468
|
+
if 'fractions' not in self.__params:
|
|
469
|
+
self.__params['fractions'] = {}
|
|
470
|
+
self.__params['fractions'][label] = value
|
|
471
|
+
|
|
472
|
+
def __lines_cellsg(self,cellsg):
|
|
473
|
+
if not cellsg:
|
|
474
|
+
return ''
|
|
475
|
+
c = cellsg
|
|
476
|
+
spacegroup = c.get('spacegroup',None)
|
|
477
|
+
_sa = f"{c['a']:g}"
|
|
478
|
+
_sb = f"{c['b']:g}"
|
|
479
|
+
_sc = f"{c['c']:g}"
|
|
480
|
+
if spacegroup and 195<=spacegroup<=230:
|
|
481
|
+
if not ( _sa == _sb and _sb == _sc ):
|
|
482
|
+
raise _nc_core.NCBadInput(f'Invalid lattice parameters for cubic spacegroup ({spacegroup}): a={_sa}, b={_sb}, c={_sc}')
|
|
483
|
+
if not ( c['alpha']==90 and c['beta']==90 and c['gamma']==90 ):
|
|
484
|
+
raise _nc_core.NCBadInput(f"Invalid lattice angles for cubic spacegroup ({spacegroup}): alpha={c['alpha']}, beta={c['beta']}, gamma={c['gamma']}")
|
|
485
|
+
ll = f"""
|
|
486
|
+
@CELL
|
|
487
|
+
cubic {_sa}
|
|
488
|
+
"""
|
|
489
|
+
else:
|
|
490
|
+
if _sa == _sb:
|
|
491
|
+
_sb = '!!'
|
|
492
|
+
if _sc == _sa:
|
|
493
|
+
_sc = '!!'
|
|
494
|
+
elif _sb == _sc:
|
|
495
|
+
_sc = '!!'
|
|
496
|
+
|
|
497
|
+
ll = f"""
|
|
498
|
+
@CELL
|
|
499
|
+
lengths {_sa} {_sb} {_sc}
|
|
500
|
+
angles {c['alpha']:g} {c['beta']:g} {c['gamma']:g}
|
|
501
|
+
"""
|
|
502
|
+
if spacegroup:
|
|
503
|
+
ll+=f"""
|
|
504
|
+
@SPACEGROUP
|
|
505
|
+
{spacegroup}
|
|
506
|
+
"""
|
|
507
|
+
return ll
|
|
508
|
+
|
|
509
|
+
def __add_dyninfo( self, label, fraction, dyninfo ):
|
|
510
|
+
_checklabel(label)
|
|
511
|
+
self.__dirty()
|
|
512
|
+
if 'dyninfos' not in self.__params:
|
|
513
|
+
self.__params['dyninfos'] = { label : dyninfo }
|
|
514
|
+
else:
|
|
515
|
+
self.__params['dyninfos'][label] = dyninfo
|
|
516
|
+
if fraction is not None:
|
|
517
|
+
self.set_fraction( label, fraction )
|
|
518
|
+
|
|
519
|
+
def allow_fallback_dyninfo( self, debye_temp ):
|
|
520
|
+
dt = _nc_common._decodeflt( debye_temp )
|
|
521
|
+
if not ( dt and dt>0.0 ):
|
|
522
|
+
raise _nc_core.NCBadInput(f'Invalid Debye temperature value: {debye_temp}')
|
|
523
|
+
self.__dirty()
|
|
524
|
+
self.__params['fallback_debye_temp'] = dt
|
|
525
|
+
|
|
526
|
+
def set_dyninfo_vdosdebye( self, label, debye_temp, *, comment, fraction ):
|
|
527
|
+
self.__add_dyninfo( label, fraction, dict( ditype='vdosdebye',
|
|
528
|
+
debye_temp = float(debye_temp),
|
|
529
|
+
comment = None if not comment else self.__prunecomment(comment) ) )
|
|
530
|
+
|
|
531
|
+
def set_dyninfo_freegas( self, label, *, comment, fraction ):
|
|
532
|
+
self.__add_dyninfo( label, fraction, dict( ditype='freegas',
|
|
533
|
+
comment = None if not comment else self.__prunecomment(comment) ) )
|
|
534
|
+
|
|
535
|
+
def set_dyninfo_sterile( self, label, *, comment, fraction ):
|
|
536
|
+
self.__add_dyninfo( label, fraction, dict( ditype='sterile',
|
|
537
|
+
comment = None if not comment else self.__prunecomment(comment) ) )
|
|
538
|
+
|
|
539
|
+
def set_dyninfo_vdos( self, label, vdos_egrid, vdos, *, comment, fraction ):
|
|
540
|
+
self.__add_dyninfo( label, fraction, dict( ditype='vdos',
|
|
541
|
+
vdos_egrid = _copyarray_or_None(vdos_egrid),
|
|
542
|
+
vdos = _copyarray_or_None(vdos),
|
|
543
|
+
comment = None if not comment else self.__prunecomment(comment) ) )
|
|
544
|
+
|
|
545
|
+
def set_dyninfo_scatknl( self, label, *, alphagrid, betagrid, temperature,
|
|
546
|
+
sab = None, sab_scaled = None, egrid = None,
|
|
547
|
+
comment = None, fraction = None ):
|
|
548
|
+
def present( x ):
|
|
549
|
+
return x is not None and _is_nonempty_array(x)
|
|
550
|
+
if not present(sab) and not present(sab_scaled):
|
|
551
|
+
raise _nc_core.NCBadInput('Missing either sab or sab_scaled arguments')
|
|
552
|
+
if present(sab) and present(sab_scaled):
|
|
553
|
+
raise _nc_core.NCBadInput('Do not specify both sab and sab_scaled')
|
|
554
|
+
self.__add_dyninfo( label, fraction, dict( ditype='scatknl',
|
|
555
|
+
temperature = float(temperature),
|
|
556
|
+
egrid = _copyarray_or_None(egrid),
|
|
557
|
+
sab = _copyarray_or_None(sab),
|
|
558
|
+
sab_scaled = _copyarray_or_None(sab_scaled),
|
|
559
|
+
alphagrid = _copyarray_or_None(alphagrid),
|
|
560
|
+
betagrid = _copyarray_or_None(betagrid),
|
|
561
|
+
comment = None if not comment else self.__prunecomment(comment) ) )
|
|
562
|
+
|
|
563
|
+
def set_dyninfo_msd( self, label, *, msd, temperature, comment, fraction ):
|
|
564
|
+
self.__add_dyninfo( label, fraction, dict( ditype='msd',
|
|
565
|
+
msd_value = float(msd),
|
|
566
|
+
msd_temperature = float(temperature),
|
|
567
|
+
comment = None if not comment else self.__prunecomment(comment) ) )
|
|
568
|
+
|
|
569
|
+
def set_dyninfo_from_object( self, label, source_dyninfo, comment = None, fraction = None ):
|
|
570
|
+
di = source_dyninfo
|
|
571
|
+
if not isinstance(di,_nc_core.Info.DynamicInfo):
|
|
572
|
+
raise _nc_core.NCBadInput('source_dyninfo object must be an object derived from NCrystal.Info.DynamicInfo')
|
|
573
|
+
_infoobj = di._info_wr() if di._info_wr else None
|
|
574
|
+
if not _infoobj:
|
|
575
|
+
raise _nc_core.NCBadInput('source_dyninfo object seems to be associated with expired Info object (or it was improperly initialised!)')
|
|
576
|
+
|
|
577
|
+
if comment is None:
|
|
578
|
+
comment = f'Transferred from "{di.atomData.displayLabel()}" in existing NCrystal.DynamicInfo object'
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
if isinstance(di,_nc_core.Info.DI_VDOSDebye):
|
|
582
|
+
self.set_dyninfo_vdosdebye( label,
|
|
583
|
+
debye_temp=di.debyeTemperature(),
|
|
584
|
+
comment=comment,
|
|
585
|
+
fraction=fraction )
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
if isinstance(di,_nc_core.Info.DI_VDOS):
|
|
589
|
+
self.set_dyninfo_vdos( label,
|
|
590
|
+
vdos_egrid = di.vdosOrigEgrid(),
|
|
591
|
+
vdos = di.vdosOrigDensity(),
|
|
592
|
+
comment = comment,
|
|
593
|
+
fraction = fraction)
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
if isinstance(di,_nc_core.Info.DI_Sterile):
|
|
597
|
+
self.set_dyninfo_sterile( label,
|
|
598
|
+
comment = comment,
|
|
599
|
+
fraction = fraction )
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
if isinstance(di,_nc_core.Info.DI_FreeGas):
|
|
603
|
+
self.set_dyninfo_freegas( label,
|
|
604
|
+
comment = comment,
|
|
605
|
+
fraction = fraction )
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
if isinstance(di,_nc_core.Info.DI_ScatKnlDirect):
|
|
609
|
+
knl=di.loadKernel()
|
|
610
|
+
#NB: Ignores knl['suggestedEmax'].
|
|
611
|
+
#NB: From DI object we can only extract the sab, even if the input
|
|
612
|
+
#file originally had e.g. sab_scaled.
|
|
613
|
+
self.set_dyninfo_scatknl( label,
|
|
614
|
+
temperature = di.temperature,
|
|
615
|
+
alphagrid = knl['alpha'],
|
|
616
|
+
betagrid = knl['beta'],
|
|
617
|
+
sab = knl['sab'],
|
|
618
|
+
egrid = knl['egrid'],
|
|
619
|
+
comment = comment,
|
|
620
|
+
fraction = fraction )
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
from .exceptions import nc_assert
|
|
624
|
+
nc_assert(False,'set_dyninfo_from_object not implemented yet'
|
|
625
|
+
' for %s type DynamicInfo'%di.__class__.__name__ )
|
|
626
|
+
|
|
627
|
+
def transfer_dyninfo_objects( self, source, mapping, allow_none ):
|
|
628
|
+
lbls = set(mapping.keys()) if mapping else self.get_labels()
|
|
629
|
+
from ._ncmatimpl import extract_dyninfo_objects
|
|
630
|
+
_keepalive, lbl2dyninfos = extract_dyninfo_objects( lbls,
|
|
631
|
+
self.__params.get('compositions',{}),
|
|
632
|
+
source,
|
|
633
|
+
mapping )
|
|
634
|
+
if not lbl2dyninfos and not allow_none:
|
|
635
|
+
raise _nc_core.NCBadInput('No dynamic info was transferred from source')
|
|
636
|
+
for lbl, di in sorted( lbl2dyninfos.items() ):
|
|
637
|
+
self.set_dyninfo_from_object( lbl, di['obj'], comment = di['comment'] )
|
|
638
|
+
|
|
639
|
+
def as_spglib_cell( self ):
|
|
640
|
+
cellsg = self.__params.get('cellsg',None)
|
|
641
|
+
atomposlist = self.__params.get('atompos',{}).get('pos',[])
|
|
642
|
+
if not cellsg or not atomposlist:
|
|
643
|
+
return None, None
|
|
644
|
+
lattice = _cellparams_to_spglib_lattice(cellsg)
|
|
645
|
+
lbl2atomidx = {}
|
|
646
|
+
atomic_points, atomic_types = [], []
|
|
647
|
+
for lbl,x,y,z in sorted(atomposlist):
|
|
648
|
+
if lbl not in lbl2atomidx:
|
|
649
|
+
lbl2atomidx[lbl] = len(lbl2atomidx)
|
|
650
|
+
#idx = lbl2atomidx[lbl]
|
|
651
|
+
atomic_points.append( (x,y,z) )
|
|
652
|
+
atomic_types.append( lbl2atomidx[lbl] )
|
|
653
|
+
return ( lattice, atomic_points, atomic_types ), dict( (v,k) for k,v in lbl2atomidx.items())
|
|
654
|
+
|
|
655
|
+
def refine_crystal_structure( self, symprec, quiet ):
|
|
656
|
+
self._impl_refine( mode_refine = True, symprec = symprec, quiet=quiet )
|
|
657
|
+
|
|
658
|
+
def verify_crystal_structure( self, symprec, quiet ):
|
|
659
|
+
self._impl_refine( mode_refine = False, symprec = symprec, quiet=quiet )
|
|
660
|
+
|
|
661
|
+
def _impl_refine( self, *, mode_refine, symprec, quiet ):
|
|
662
|
+
cellsg = self.__params.get('cellsg',None)
|
|
663
|
+
atompos = self.__params.get('atompos',None)
|
|
664
|
+
#atomposlist = atompos.get('pos',None) if atompos else None
|
|
665
|
+
if not cellsg and not atompos:
|
|
666
|
+
return#do nothing if not crystal
|
|
667
|
+
if int(bool(cellsg))+int(bool(atompos)) == 1:
|
|
668
|
+
raise _nc_core.NCBadInput('Must set both unit cell parameters and atom'
|
|
669
|
+
' positions before trying to refine or verify a crystal structure')
|
|
670
|
+
assert cellsg and atompos
|
|
671
|
+
sgnumber = cellsg.get('spacegroup',None)
|
|
672
|
+
if sgnumber is None and not mode_refine:
|
|
673
|
+
raise _nc_core.NCBadInput('Must provide a space group number (or invoke'
|
|
674
|
+
' .refine_crystal_structure()) before it is'
|
|
675
|
+
' possible to verify a crystal structure')
|
|
676
|
+
spglib_cell, spglib_idx2lbl = self.as_spglib_cell()
|
|
677
|
+
d = _spglib_refine_cell( spglib_cell, symprec = symprec ) if spglib_cell else None
|
|
678
|
+
if not d:
|
|
679
|
+
raise _nc_core.NCBadInput(f'Failed to {"refine" if mode_refine else "verify"} crystal structure with spglib.')
|
|
680
|
+
assert len(d)==7
|
|
681
|
+
if mode_refine:
|
|
682
|
+
if not quiet:
|
|
683
|
+
for m in d['msgs']:
|
|
684
|
+
_nc_common.print(m)
|
|
685
|
+
for w in d['warnings']:
|
|
686
|
+
_nc_common.warn(w)
|
|
687
|
+
if mode_refine:
|
|
688
|
+
#NB: We do not (yet) have anisotropic atomic properties (like
|
|
689
|
+
#anisotropic displacements), so we can simply update just the
|
|
690
|
+
#positions. This is different from the case in cifutils.py where
|
|
691
|
+
#d['can_keep_anisotropic_properties'] is important!
|
|
692
|
+
new_cellsg = dict(d['cellparams_snapped'].items())
|
|
693
|
+
new_cellsg['spacegroup'] = d['sgno']#NB: ignoring sgsymb_hm here!
|
|
694
|
+
#Could we have spacegroup_hm as
|
|
695
|
+
#optional field in the cellsg
|
|
696
|
+
#array?
|
|
697
|
+
ll = []
|
|
698
|
+
for pos, atomidx in zip(d['refined_cell'][1],d['refined_cell'][2]):
|
|
699
|
+
lbl = spglib_idx2lbl[atomidx]
|
|
700
|
+
ll.append( (lbl,
|
|
701
|
+
_remap_fract_pos(pos[0]),
|
|
702
|
+
_remap_fract_pos(pos[1]),
|
|
703
|
+
_remap_fract_pos(pos[2]) ) )
|
|
704
|
+
ll.sort()
|
|
705
|
+
new_atompos = dict( (k,copy.deepcopy(v))
|
|
706
|
+
for k,v in atompos.items()
|
|
707
|
+
if k!='pos' )
|
|
708
|
+
new_atompos['pos'] = ll
|
|
709
|
+
self.__dirty()
|
|
710
|
+
self.__params['cellsg'] = new_cellsg
|
|
711
|
+
self.__params['atompos'] = new_atompos
|
|
712
|
+
else:
|
|
713
|
+
if sgnumber != d['sgno']:
|
|
714
|
+
raise _nc_core.NCBadInput(f'Failed to verify crystal structure with spglib. Expected SG-{sgnumber}, got SG-{d["sgno"]} ({d["sgsymb_hm"]}).')
|
|
715
|
+
rdl = _reldiff_cellparams( cellsg, d['cellparams_snapped'] )
|
|
716
|
+
rda = _reldiff_atompos( spglib_cell, d['refined_cell'] )
|
|
717
|
+
#rd = None if (rdl is None or rda is None) else max(rdl,rda)
|
|
718
|
+
if rdl is None or rda is None or max(rdl,rda) > 0.01:
|
|
719
|
+
raise _nc_core.NCBadInput('Failed to verify crystal structure with spglib.')
|
|
720
|
+
|
|
721
|
+
def set_atompos( self, atompos ):
|
|
722
|
+
pos,occumap = [],{}
|
|
723
|
+
for e in atompos:
|
|
724
|
+
assert len(e) in (4,5)
|
|
725
|
+
lbl,x,y,z = str(e[0]),_remap_fract_pos(e[1]),_remap_fract_pos(e[2]),_remap_fract_pos(e[3])
|
|
726
|
+
_checklabel(lbl)
|
|
727
|
+
pos.append( (lbl,x,y,z) )
|
|
728
|
+
occu = float(e[4]) if len(e)==5 else 1.0
|
|
729
|
+
if not ( 0.0 < occu <= 1.0 ):
|
|
730
|
+
raise _nc_core.NCBadInput('site_occupancy values must be in (0.0,1.0]')
|
|
731
|
+
if lbl not in occumap:
|
|
732
|
+
occumap[lbl]=occu
|
|
733
|
+
else:
|
|
734
|
+
if not occumap[lbl] == occu:
|
|
735
|
+
raise _nc_core.NCBadInput('All site occupancies for label "%s" are not identical'%lbl)
|
|
736
|
+
|
|
737
|
+
#remove occu==1.0 entries from map (and sort):
|
|
738
|
+
occumap = dict( sorted( (k,v) for k,v in occumap.items() if v!=1.0 ) )
|
|
739
|
+
if occumap:
|
|
740
|
+
_nc_common.warn('Support for site_occupancy is highly experimental. Do *not* attempt to directly override'
|
|
741
|
+
+' value with the "density" cfg-parameter for this material (unless it is to simply scale it)')
|
|
742
|
+
self.__dirty()
|
|
743
|
+
self.__params['atompos'] = dict( pos=pos, occumap=occumap )
|
|
744
|
+
#
|
|
745
|
+
def get_labels( self ):
|
|
746
|
+
#from positions:
|
|
747
|
+
atompos = self.__params.get('atompos',None)
|
|
748
|
+
s = set( lbl for lbl,x,y,z in atompos['pos'] ) if atompos else set()
|
|
749
|
+
#from dyninfo:
|
|
750
|
+
for lbl in self.__params.get('dyninfos',{}).keys():
|
|
751
|
+
s.add(lbl)
|
|
752
|
+
#from composition:
|
|
753
|
+
for lbl in self.__params.get('compositions',{}).keys():
|
|
754
|
+
s.add(lbl)
|
|
755
|
+
#from fractions:
|
|
756
|
+
for lbl in self.__params.get('fractions',{}).keys():
|
|
757
|
+
s.add(lbl)
|
|
758
|
+
return s
|
|
759
|
+
|
|
760
|
+
def __lines_atompos(self, lbl_map, atompos ):
|
|
761
|
+
if not atompos:
|
|
762
|
+
return ''
|
|
763
|
+
ll=[]
|
|
764
|
+
fmt = _nc_common.prettyFmtValue
|
|
765
|
+
#NB: We sort on the val->string->val values, not the original
|
|
766
|
+
#values. Because otherwise we get irreproducibilities when in
|
|
767
|
+
#principle two x values are almost identical and their order might
|
|
768
|
+
#vary slightly.
|
|
769
|
+
def extractval(s):
|
|
770
|
+
if '/' not in s:
|
|
771
|
+
return float(s)
|
|
772
|
+
p=s.split('/')
|
|
773
|
+
assert len(p)==2
|
|
774
|
+
return float(p[0]) / float(p[1])
|
|
775
|
+
for lbl,x,y,z in atompos['pos']:
|
|
776
|
+
fx,fy,fz = fmt(x), fmt(y), fmt(z)
|
|
777
|
+
ll.append( ( lbl_map.get(lbl,lbl),
|
|
778
|
+
extractval(fx), extractval(fy), extractval(fz),
|
|
779
|
+
fx, fy, fz ) )
|
|
780
|
+
ll.sort()
|
|
781
|
+
out="@ATOMPOSITIONS\n"
|
|
782
|
+
for lbl,_,_,_,fx,fy,fz in ll:
|
|
783
|
+
out += f'{lbl} {fx} {fy} {fz}\n'
|
|
784
|
+
return out
|
|
785
|
+
|
|
786
|
+
def __lines_dyninfo(self,lbl_map,fractions):
|
|
787
|
+
dyninfos = self.__params.get('dyninfos',{})
|
|
788
|
+
|
|
789
|
+
lines=''
|
|
790
|
+
natoms_with_fallback_dyninfo = 0
|
|
791
|
+
|
|
792
|
+
def formatVector(name,values):
|
|
793
|
+
return formatVectorForNCMAT(name,values,_magic_two_space)
|
|
794
|
+
|
|
795
|
+
def transform_msd_to_vdosdebye( lbl, dyninfo ):
|
|
796
|
+
if not dyninfo or not dyninfo.get('ditype','') == 'msd':
|
|
797
|
+
return dyninfo
|
|
798
|
+
d = {}
|
|
799
|
+
d['ditype'] = 'vdosdebye'
|
|
800
|
+
mass = calc_mass(lbl,self.__params.get('compositions',{}).get(lbl,None))
|
|
801
|
+
if not mass:
|
|
802
|
+
raise _nc_core.NCBadInput( f'Failed to calculate the atomic mass associated with the label {lbl}'
|
|
803
|
+
' (needed for dyninfo of type "msd"). Please ensure that the label has a'
|
|
804
|
+
' proper composition set (with .set_composition(..)) and that all component'
|
|
805
|
+
' atoms have known masses (otherwise use .update_atomdb(..) to provide them)')
|
|
806
|
+
assert mass>0.0
|
|
807
|
+
from .vdos import debyeTempFromIsotropicMSD
|
|
808
|
+
msd = dyninfo['msd_value']
|
|
809
|
+
msd_temp = dyninfo['msd_temperature']
|
|
810
|
+
dt = debyeTempFromIsotropicMSD( msd = msd, mass = mass,
|
|
811
|
+
temperature = msd_temp )
|
|
812
|
+
assert dt > 0.0
|
|
813
|
+
d['debye_temp'] = dt
|
|
814
|
+
c = f'Debye temperature value derived from msd={msd:g}Aa^2 @ T={msd_temp:g}K (mass={mass:g}u)'
|
|
815
|
+
oldcomment = dyninfo.get('comment',None)
|
|
816
|
+
d['comment'] = c if not oldcomment else f'{oldcomment.strip()}\n{c}'
|
|
817
|
+
return d
|
|
818
|
+
|
|
819
|
+
for lbl,fracinfo in sorted(fractions.items()):
|
|
820
|
+
if not isinstance(fracinfo,float) and len(fracinfo)==3:
|
|
821
|
+
_fracval_flt,frac_prettyprint,_ = fracinfo
|
|
822
|
+
else:
|
|
823
|
+
_fracval_flt,frac_prettyprint = fracinfo, _nc_common.prettyFmtValue( fracinfo )
|
|
824
|
+
lines += '@DYNINFO\n'
|
|
825
|
+
dyninfo = dyninfos.get(lbl,None)
|
|
826
|
+
dyninfo = transform_msd_to_vdosdebye( lbl, dyninfo )
|
|
827
|
+
|
|
828
|
+
if not dyninfo:
|
|
829
|
+
fbdt = self.__params.get('fallback_debye_temp',None)
|
|
830
|
+
if fbdt is not None:
|
|
831
|
+
natoms_with_fallback_dyninfo += 1
|
|
832
|
+
dyninfo = dict( ditype='vdosdebye',
|
|
833
|
+
debye_temp = fbdt,
|
|
834
|
+
comment = 'WARNING: Using fallback Debye temperature value!' )
|
|
835
|
+
if not dyninfo:
|
|
836
|
+
raise _nc_core.NCMissingInfo(f'Missing dyninfo for atom label "{lbl}" (add via call to set_dyninfo_...)')
|
|
837
|
+
comment = dyninfo['comment']
|
|
838
|
+
if comment:
|
|
839
|
+
for e in comment.splitlines():
|
|
840
|
+
lines += f' # {e.strip()}\n'
|
|
841
|
+
|
|
842
|
+
lines += f'element {lbl_map.get(lbl,lbl)}\n'
|
|
843
|
+
lines += f'fraction {frac_prettyprint}\n'
|
|
844
|
+
ditype = dyninfo['ditype']
|
|
845
|
+
lines += f'type {ditype}\n'
|
|
846
|
+
if ditype == 'vdosdebye':
|
|
847
|
+
dt=dyninfo['debye_temp']
|
|
848
|
+
lines += f'debye_temp {dt:g}\n'
|
|
849
|
+
elif ditype == 'vdos':
|
|
850
|
+
x = dyninfo['vdos_egrid']
|
|
851
|
+
from ._common import _grid_is_linspace
|
|
852
|
+
if len(x)>2 and _grid_is_linspace(x):
|
|
853
|
+
lines += f'vdos_egrid {_fmtprecisenum(x[0])} {_fmtprecisenum(x[-1])}\n'
|
|
854
|
+
else:
|
|
855
|
+
lines += formatVector('vdos_egrid',x)
|
|
856
|
+
lines += formatVector('vdos_density',dyninfo['vdos'])
|
|
857
|
+
elif ditype in ('sterile','freegas'):
|
|
858
|
+
pass
|
|
859
|
+
elif ditype == 'scatknl':
|
|
860
|
+
lines += f'temperature {_fmtprecisenum(dyninfo["temperature"])}\n'
|
|
861
|
+
if dyninfo.get('egrid') is not None:
|
|
862
|
+
from ._common import _grid_is_linspace
|
|
863
|
+
x = dyninfo['egrid']
|
|
864
|
+
if ( len(x) > 3 and _grid_is_linspace(x) ):
|
|
865
|
+
lines += f'egrid {_fmtprecisenum(x[0])} {_fmtprecisenum(x[-1])} {len(x)}\n'
|
|
866
|
+
elif len(x) <= 3:
|
|
867
|
+
if len(x)==3 and x[0]==0.0 and x[2]==0.0:
|
|
868
|
+
x = [ x[1] ]#single Emax value
|
|
869
|
+
_=' '.join(_fmtprecisenum(e) for e in x)
|
|
870
|
+
lines += f'egrid {_}\n'
|
|
871
|
+
else:
|
|
872
|
+
lines += formatVector('egrid',x)
|
|
873
|
+
lines += formatVector('alphagrid',dyninfo['alphagrid'])
|
|
874
|
+
lines += formatVector('betagrid',dyninfo['betagrid'])
|
|
875
|
+
if dyninfo['sab'] is not None:
|
|
876
|
+
assert dyninfo['sab_scaled'] is None
|
|
877
|
+
lines += formatVector('sab',dyninfo['sab'])
|
|
878
|
+
else:
|
|
879
|
+
assert dyninfo['sab_scaled'] is not None
|
|
880
|
+
lines += formatVector('sab_scaled',dyninfo['sab_scaled'])
|
|
881
|
+
else:
|
|
882
|
+
assert False,f'Unexpected dyninfo type : "{ditype}"'
|
|
883
|
+
return lines, natoms_with_fallback_dyninfo
|
|
884
|
+
|
|
885
|
+
def register_as( self, virtual_filename, cfg_params ):
|
|
886
|
+
from .datasrc import registerInMemoryFileData
|
|
887
|
+
registerInMemoryFileData( virtual_filename, self.create_ncmat( cfg_params = cfg_params ) )
|
|
888
|
+
return virtual_filename
|
|
889
|
+
|
|
890
|
+
def write( self, path, cfg_params ):
|
|
891
|
+
import pathlib
|
|
892
|
+
p = pathlib.Path( path )
|
|
893
|
+
assert p.name.endswith('.ncmat') and len(p.name) > 6
|
|
894
|
+
_nc_common.write_text( p,
|
|
895
|
+
self.create_ncmat( cfg_params = cfg_params ) )
|
|
896
|
+
return p
|
|
897
|
+
|
|
898
|
+
def load(self,cfg_params,force ):
|
|
899
|
+
if force or self.__loadcache is None or self.__loadcache[0] != cfg_params:
|
|
900
|
+
self.__loadcache = ( cfg_params, _nc_core.directLoad( self.create_ncmat(), cfg_params ) )
|
|
901
|
+
return self.__loadcache[1]
|
|
902
|
+
|
|
903
|
+
def plot_xsect( self, composer, cfg_params, kwargs_plot_xsect ):
|
|
904
|
+
from .misc import MaterialSource
|
|
905
|
+
from .plot import plot_xsect
|
|
906
|
+
ms = MaterialSource( composer, cfg_params = cfg_params )
|
|
907
|
+
return plot_xsect( ms, **kwargs_plot_xsect )
|
|
908
|
+
|
|
909
|
+
def inspect( self, composer, cfg_params, kwargs_plot_xsect ):
|
|
910
|
+
from .misc import MaterialSource
|
|
911
|
+
from .plot import plot_xsect
|
|
912
|
+
ms = MaterialSource( composer, cfg_params = cfg_params )
|
|
913
|
+
loadedmat = ms.load()
|
|
914
|
+
import sys
|
|
915
|
+
def flush():
|
|
916
|
+
( sys.stdout.flush(),sys.stderr.flush() )
|
|
917
|
+
flush()
|
|
918
|
+
loadedmat.info.dump()
|
|
919
|
+
flush()
|
|
920
|
+
_nc_common.print("Absorption process (objects):")
|
|
921
|
+
flush()
|
|
922
|
+
loadedmat.absorption.dump(prefix=' ')
|
|
923
|
+
flush()
|
|
924
|
+
_nc_common.print("Scattering process (objects):")
|
|
925
|
+
flush()
|
|
926
|
+
loadedmat.scatter.dump(prefix=' ')
|
|
927
|
+
flush()
|
|
928
|
+
return plot_xsect( ms, **kwargs_plot_xsect )
|
|
929
|
+
|
|
930
|
+
def __determine_atompos_fractions( self, atompos ):
|
|
931
|
+
natoms = len(atompos['pos']) if atompos else 0
|
|
932
|
+
if not natoms:
|
|
933
|
+
return
|
|
934
|
+
out = {}
|
|
935
|
+
from collections import Counter as _collections_Counter
|
|
936
|
+
counts = _collections_Counter(list(e[0] for e in atompos['pos']))
|
|
937
|
+
for lbl,count in counts.items():
|
|
938
|
+
frac = float(count) / natoms
|
|
939
|
+
gcd = math.gcd(count,natoms)
|
|
940
|
+
out[ lbl ] = ( frac, ( '1' if count==natoms else '%i/%i'%( count // gcd, natoms // gcd ) ), count )
|
|
941
|
+
return out
|
|
942
|
+
|
|
943
|
+
def get_chemical_composition( self, as_str ):
|
|
944
|
+
atompos = self.__params.get('atompos')
|
|
945
|
+
_lbl_counts = None
|
|
946
|
+
if atompos:
|
|
947
|
+
_afr = self.__determine_atompos_fractions( atompos )
|
|
948
|
+
_lbl_counts = list( (lbl,count) for lbl,(_,_,count) in _afr.items() )
|
|
949
|
+
elif self.__params.get('cellsg'):
|
|
950
|
+
#We could in principle proceed below, but for crystals, we promise that the
|
|
951
|
+
#return value is always numbers per unit cell!
|
|
952
|
+
raise _nc_core.NCMissingInfo("Can not provide chemical composition of incomplete crystalline materials (missing atomic positions).")
|
|
953
|
+
if _lbl_counts is None:
|
|
954
|
+
fractions = self.__params.get('fractions')
|
|
955
|
+
dyninfos = self.__params.get('dyninfos')
|
|
956
|
+
if not fractions and not atompos and len(dyninfos)==1:
|
|
957
|
+
fractions = {list(dyninfos.keys())[0] : 1.0 }
|
|
958
|
+
if fractions:
|
|
959
|
+
_lbl_counts = list( sorted( fractions.items()) )
|
|
960
|
+
if not _lbl_counts:
|
|
961
|
+
return None
|
|
962
|
+
|
|
963
|
+
compos = self.__params.get('compositions')
|
|
964
|
+
occumap = atompos['occumap'] if atompos else None
|
|
965
|
+
ll = []
|
|
966
|
+
for lbl, weight in _lbl_counts:
|
|
967
|
+
c = compos.get(lbl) if compos else None
|
|
968
|
+
_occufactor = occumap.get(lbl,1.0) if occumap else 1.0
|
|
969
|
+
if not c:
|
|
970
|
+
assert _nc_common.check_elem_or_isotope_marker( lbl ) is not None
|
|
971
|
+
ll.append( (lbl, weight * _occufactor ) )
|
|
972
|
+
else:
|
|
973
|
+
for frac,elemisomarker in c:
|
|
974
|
+
ll.append( (elemisomarker, weight * frac * _occufactor ) )
|
|
975
|
+
d = {}
|
|
976
|
+
for k,v in ll:
|
|
977
|
+
if k in d:
|
|
978
|
+
d[k] += v
|
|
979
|
+
else:
|
|
980
|
+
d[k] = v
|
|
981
|
+
res = list( sorted( (k,v) for k,v in d.items() ) )
|
|
982
|
+
if as_str:
|
|
983
|
+
return _nc_common.format_chemform( res )
|
|
984
|
+
else:
|
|
985
|
+
return res
|
|
986
|
+
|
|
987
|
+
def _unofficial_vdos2sab_ignore( self, *, order_low, order_high = None, mode = None ):
|
|
988
|
+
assert mode is None or mode in ('coherent','incoherent')
|
|
989
|
+
import numbers
|
|
990
|
+
assert isinstance(order_low,numbers.Integral)
|
|
991
|
+
assert order_high is None or isinstance(order_low,numbers.Integral)
|
|
992
|
+
order_high = int(order_high) if order_high is not None else None
|
|
993
|
+
order_low = int(order_low)
|
|
994
|
+
assert order_high is None or order_high >= order_low
|
|
995
|
+
line = ['vdos2sab_ignorecontrib',str(order_low)]
|
|
996
|
+
if order_high is not None:
|
|
997
|
+
line.append( str(order_high) )
|
|
998
|
+
if mode is not None:
|
|
999
|
+
line.append(mode)
|
|
1000
|
+
old = [e for e in self.__params.get('unofficial_hacks',[]) if ( e and not e[0]=='vdos2sab_ignorecontrib' ) ]
|
|
1001
|
+
self.__dirty()
|
|
1002
|
+
self.__params['unofficial_hacks'] = old + [ line ]
|
|
1003
|
+
|
|
1004
|
+
def create_ncmat( self, *, cfg_params='', meta_data = False,
|
|
1005
|
+
verify_crystal_structure = True ):
|
|
1006
|
+
#docstr: set meta_data to also return a dictionary with a bit of high
|
|
1007
|
+
#level metadata like chemical composition, spacegroup, etc.
|
|
1008
|
+
md = {} if meta_data else None
|
|
1009
|
+
|
|
1010
|
+
cellsg = self.__params.get('cellsg')
|
|
1011
|
+
atompos = self.__params.get('atompos')
|
|
1012
|
+
|
|
1013
|
+
is_crystal = bool(cellsg or atompos)
|
|
1014
|
+
did_verify_xtal_struct = False
|
|
1015
|
+
if verify_crystal_structure and cellsg and atompos:
|
|
1016
|
+
self.verify_crystal_structure(symprec = 0.01, quiet = False)
|
|
1017
|
+
did_verify_xtal_struct = True
|
|
1018
|
+
|
|
1019
|
+
if verify_crystal_structure and cellsg and cellsg.get('spacegroup',None):
|
|
1020
|
+
_check_cell_sg_consistency( cellsg['spacegroup'],
|
|
1021
|
+
cellsg['a'], cellsg['b'], cellsg['c'],
|
|
1022
|
+
cellsg['alpha'], cellsg['beta'], cellsg['gamma'] )
|
|
1023
|
+
|
|
1024
|
+
dyninfos = self.__params.get('dyninfos',None)
|
|
1025
|
+
|
|
1026
|
+
if md is not None:
|
|
1027
|
+
md['cellsg'] = dict( cellsg.items() ) if cellsg else None
|
|
1028
|
+
|
|
1029
|
+
if atompos and not cellsg:
|
|
1030
|
+
raise _nc_core.NCMissingInfo("Atom positions in unit cell added but unit cell info missing (add via call to set_cellsg)")
|
|
1031
|
+
if cellsg and not atompos:
|
|
1032
|
+
raise _nc_core.NCMissingInfo("Cell/spacegroup information added but atom positions in unit cell are missing (add via call to set_atompos)")
|
|
1033
|
+
is_crystal = bool(cellsg or atompos)
|
|
1034
|
+
if md is not None:
|
|
1035
|
+
md['is_crystal'] = is_crystal
|
|
1036
|
+
|
|
1037
|
+
density = self.__params.get('density',None)
|
|
1038
|
+
fractions = self.__params.get('fractions',None)
|
|
1039
|
+
if is_crystal and density:
|
|
1040
|
+
raise _nc_core.NCBadInput('Density should not be added explicitly for crystalline materials (if'
|
|
1041
|
+
+' needed, it can be modified via the "density" cfg-string parameter)')
|
|
1042
|
+
|
|
1043
|
+
if is_crystal and dyninfos:
|
|
1044
|
+
for lbl,di in sorted(dyninfos.items()):
|
|
1045
|
+
_ditype=di.get('ditype',None)
|
|
1046
|
+
if _ditype not in ('vdos','vdosdebye','msd'):
|
|
1047
|
+
raise _nc_core.NCBadInput('Crystalline material are currently only supported with dynamic info type of'
|
|
1048
|
+
+f' either "vdos", "vdosdebye", or "msd" (offending type "{_ditype}" for "{lbl}")')
|
|
1049
|
+
|
|
1050
|
+
if not is_crystal and len(dyninfos)==1 and not fractions:
|
|
1051
|
+
#in case of a single dyninfo we can assume the fraction is 1.0
|
|
1052
|
+
fractions = {list(dyninfos.keys())[0] : 1.0 }
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
atompos_fractions = None if not is_crystal else self.__determine_atompos_fractions( atompos )
|
|
1056
|
+
if atompos_fractions and fractions:
|
|
1057
|
+
for lbl,frac in sorted(fractions.items()):
|
|
1058
|
+
afrac,_,_ = atompos_fractions.get(lbl,None)
|
|
1059
|
+
if afrac is None:
|
|
1060
|
+
raise _nc_core.NCBadInput(f'Label "{lbl}" has fraction explicitly set, but the material'
|
|
1061
|
+
+' is crystalline and the label has no atomic positions sets')
|
|
1062
|
+
if abs(afrac-frac)>1e-5:
|
|
1063
|
+
raise _nc_core.NCBadInput(f'Label "{lbl}" has both explicit and implicit (via atomic'
|
|
1064
|
+
+f' position count) fractions provided, and they do NOT match up ({afrac} vs. {frac})')
|
|
1065
|
+
if not is_crystal:
|
|
1066
|
+
if not density:
|
|
1067
|
+
raise _nc_core.NCBadInput('Density must be set explicitly for non-crystalline materials (add via call to set_density)')
|
|
1068
|
+
if not dyninfos:
|
|
1069
|
+
raise _nc_core.NCBadInput('Material incompletely specified.')
|
|
1070
|
+
if set(dyninfos.keys()) != set(lbl for lbl,fv in (fractions or {}).items() if fv is not None):
|
|
1071
|
+
raise _nc_core.NCBadInput('For non-crystalline materials with more than one component, all components must have fractions specified.')
|
|
1072
|
+
_fracsum = math.fsum(val for lbl,val in sorted((fractions or {}).items()))
|
|
1073
|
+
if abs( 1.0 - _fracsum ) > 1e-10:
|
|
1074
|
+
raise _nc_core.NCBadInput(f'Invalid material - sum of dyninfo fractions is not 1 (it is {_fracsum})')
|
|
1075
|
+
|
|
1076
|
+
lbl_map,atomdb_lines = determine_labels_and_atomdb( self.__params, fractions = fractions )
|
|
1077
|
+
|
|
1078
|
+
ll = self.__lines_cellsg( cellsg )
|
|
1079
|
+
ll += self.__lines_atompos( lbl_map, atompos )
|
|
1080
|
+
if density:
|
|
1081
|
+
_dval,_dunit = density
|
|
1082
|
+
_ncmat_dunit = {'g/cm3': 'g_per_cm3',
|
|
1083
|
+
'kg/m3': 'kg_per_m3',
|
|
1084
|
+
'atoms/Aa3': 'atoms_per_aa3' }[_dunit]
|
|
1085
|
+
ll+=f'@DENSITY\n{_dval} {_ncmat_dunit}\n'
|
|
1086
|
+
|
|
1087
|
+
som = self.get_state_of_matter()
|
|
1088
|
+
if som is not None:
|
|
1089
|
+
assert som in ('solid','liquid','gas')
|
|
1090
|
+
ll += f'@STATEOFMATTER\n{som}\n'
|
|
1091
|
+
|
|
1092
|
+
_t = self.__params.get('temperature',None)
|
|
1093
|
+
_v = '%.14g'%_t['value'] if _t else None
|
|
1094
|
+
if _t and ( _t['lock'] or _v != '293.15' ):
|
|
1095
|
+
ll += '@TEMPERATURE\n'
|
|
1096
|
+
if not _t['lock']:
|
|
1097
|
+
ll += 'default '
|
|
1098
|
+
ll += f'{_v}\n'
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
if atomdb_lines:
|
|
1102
|
+
ll += '@ATOMDB\n'
|
|
1103
|
+
ll += '\n'.join(atomdb_lines)
|
|
1104
|
+
ll += '\n'
|
|
1105
|
+
|
|
1106
|
+
secondary_phases = self.__params.get('secondary_phases')
|
|
1107
|
+
if secondary_phases:
|
|
1108
|
+
ll += '@OTHERPHASES\n'
|
|
1109
|
+
for frac,cfgstr in secondary_phases:
|
|
1110
|
+
ll += f'{frac} {cfgstr}\n'
|
|
1111
|
+
|
|
1112
|
+
custom_hardspheresans = self.__params.get('custom_hardspheresans')
|
|
1113
|
+
if custom_hardspheresans:
|
|
1114
|
+
if not secondary_phases:
|
|
1115
|
+
raise _nc_core.NCBadInput('Material with hard-sphere SANS'
|
|
1116
|
+
' enabled must have at least one'
|
|
1117
|
+
' secondary phase added.')
|
|
1118
|
+
ll += '@CUSTOM_HARDSPHERESANS\n'
|
|
1119
|
+
ll += f'{custom_hardspheresans} #sphere radius in angstrom.\n'
|
|
1120
|
+
|
|
1121
|
+
unofficial_hacks = self.__params.get('unofficial_hacks')
|
|
1122
|
+
if unofficial_hacks:
|
|
1123
|
+
ll += '@CUSTOM_UNOFFICIALHACKS\n'
|
|
1124
|
+
for e in unofficial_hacks:
|
|
1125
|
+
ll += ' '.join(e)+'\n'
|
|
1126
|
+
|
|
1127
|
+
for sn,cnt in sorted(self.__params.get('custom_sections',{}).items()):
|
|
1128
|
+
ll += f'@CUSTOM_{sn}\n'
|
|
1129
|
+
ll += cnt
|
|
1130
|
+
if not cnt.endswith('\n'):
|
|
1131
|
+
ll += '\n'
|
|
1132
|
+
|
|
1133
|
+
#Dyninfo lines last, since they might contain huge arrays of data, and
|
|
1134
|
+
#people might only look at the top of the file:
|
|
1135
|
+
ld, natoms_with_fallback_dyninfo = (
|
|
1136
|
+
self.__lines_dyninfo( lbl_map, atompos_fractions or fractions )
|
|
1137
|
+
)
|
|
1138
|
+
ll += ld
|
|
1139
|
+
|
|
1140
|
+
out=["NCMAT v7"]
|
|
1141
|
+
comments = copy.deepcopy(self.__params.get('top_comments',[]))
|
|
1142
|
+
|
|
1143
|
+
if natoms_with_fallback_dyninfo:
|
|
1144
|
+
_ = 'values were' if natoms_with_fallback_dyninfo>1 else 'value was'
|
|
1145
|
+
if comments and comments[-1]:
|
|
1146
|
+
comments.append('')
|
|
1147
|
+
comments += ['WARNING: Fallback (dummy) Debye temperature %s used for '%_
|
|
1148
|
+
+'%i atom%s!'%(
|
|
1149
|
+
natoms_with_fallback_dyninfo,
|
|
1150
|
+
's' if natoms_with_fallback_dyninfo>1 else ''),'']
|
|
1151
|
+
|
|
1152
|
+
if did_verify_xtal_struct:
|
|
1153
|
+
comments += ['NOTICE: crystal structure was verified with spglib to be self-consistent.']
|
|
1154
|
+
elif is_crystal:
|
|
1155
|
+
comments += ['WARNING: crystal structure was not verified automatically with spglib.']
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
#determine chemical formula
|
|
1159
|
+
if atompos_fractions is not None:
|
|
1160
|
+
_lbl_counts = list( (lbl,count) for lbl,(_,_,count) in atompos_fractions.items() )
|
|
1161
|
+
else:
|
|
1162
|
+
assert fractions
|
|
1163
|
+
_lbl_counts = list( sorted( fractions.items()) )
|
|
1164
|
+
|
|
1165
|
+
_chem_compos = self.get_chemical_composition( as_str = False)
|
|
1166
|
+
_chemform = _nc_common.format_chemform( _chem_compos )
|
|
1167
|
+
|
|
1168
|
+
_title = _chemform
|
|
1169
|
+
if cellsg:
|
|
1170
|
+
_sgno = cellsg['spacegroup']
|
|
1171
|
+
if _sgno:
|
|
1172
|
+
_title += f' ({_nc_common._classifySG(_sgno)}, SG-{_sgno})'
|
|
1173
|
+
|
|
1174
|
+
if md is not None:
|
|
1175
|
+
md['chemform'] = _chemform
|
|
1176
|
+
#md['chemform_per_unit_cell'] = _chemform_per_cell
|
|
1177
|
+
md['title'] = _title
|
|
1178
|
+
|
|
1179
|
+
disable_autotitle = False
|
|
1180
|
+
if comments and comments[0]=='<<disableautotitle>>':
|
|
1181
|
+
#hidden feature (might not actually be used yet)
|
|
1182
|
+
comments = comments[1:]
|
|
1183
|
+
disable_autotitle = True
|
|
1184
|
+
|
|
1185
|
+
if comments and comments[0]=='<<disableautogennotice>>':
|
|
1186
|
+
#hidden feature for our command-line scripts.
|
|
1187
|
+
comments = comments[1:]
|
|
1188
|
+
else:
|
|
1189
|
+
_ = self.__class__.__name__
|
|
1190
|
+
if _.endswith('Impl') and len(_)>4:
|
|
1191
|
+
_ = _[:-4]
|
|
1192
|
+
out.append('# Autogenerated by %s'%_)
|
|
1193
|
+
|
|
1194
|
+
if not disable_autotitle:
|
|
1195
|
+
out += [ '#',f'# {_title}','#' ]
|
|
1196
|
+
if is_crystal:
|
|
1197
|
+
_ = '+'.join(f'{count:g}x{lbl}' for lbl,count in _chem_compos)
|
|
1198
|
+
out += [ f'# Atoms per unit cell: {_}','#' ]
|
|
1199
|
+
|
|
1200
|
+
for tc in comments:
|
|
1201
|
+
out.append(('# %s'%tc).rstrip())
|
|
1202
|
+
|
|
1203
|
+
if cfg_params:
|
|
1204
|
+
if out and out[-1] != '#':
|
|
1205
|
+
out += [ '#' ]
|
|
1206
|
+
out += [ f'# NCRYSTALMATCFG[{str(cfg_params).strip()}]', '#']
|
|
1207
|
+
|
|
1208
|
+
if out and out[-1] != '#':
|
|
1209
|
+
out += [ '#' ]
|
|
1210
|
+
|
|
1211
|
+
for e in ll.splitlines():
|
|
1212
|
+
e=e.strip()
|
|
1213
|
+
if e.startswith('@'):
|
|
1214
|
+
out.append(e)
|
|
1215
|
+
elif e:
|
|
1216
|
+
out.append(' '+e.replace(_magic_two_space,' '))
|
|
1217
|
+
|
|
1218
|
+
out = '\n'.join(e.rstrip() for e in out)+'\n'
|
|
1219
|
+
|
|
1220
|
+
raw_cnt = self.__params.get('raw_content')
|
|
1221
|
+
if raw_cnt:
|
|
1222
|
+
out += raw_cnt
|
|
1223
|
+
return out if md is None else ( out, md )
|
|
1224
|
+
|
|
1225
|
+
def _determine_dyninfo_mapping( labels, composition, dilist ):
|
|
1226
|
+
#mapping is composer label -> srclbl
|
|
1227
|
+
|
|
1228
|
+
def extract_z2lbl( datalist, extractfct, allow_multi = False ):
|
|
1229
|
+
z2lbl = {}
|
|
1230
|
+
for d in datalist:
|
|
1231
|
+
atomdata_or_z,lbl = extractfct(d)
|
|
1232
|
+
if atomdata_or_z is None:
|
|
1233
|
+
z = None
|
|
1234
|
+
elif hasattr(atomdata_or_z,'isElement'):
|
|
1235
|
+
z = atomdata_or_z.Z() if atomdata_or_z.isElement() else None
|
|
1236
|
+
else:
|
|
1237
|
+
z = int(atomdata_or_z) if atomdata_or_z else None
|
|
1238
|
+
if z is not None:
|
|
1239
|
+
if z in z2lbl:
|
|
1240
|
+
z2lbl[z].append(lbl)
|
|
1241
|
+
else:
|
|
1242
|
+
z2lbl[z] = [ lbl ]
|
|
1243
|
+
if allow_multi:
|
|
1244
|
+
return z2lbl
|
|
1245
|
+
return dict( (k,v[0]) for k,v in z2lbl.items() if len(v)==1 )
|
|
1246
|
+
|
|
1247
|
+
def lookup_zval_for_label(lbl):
|
|
1248
|
+
c = composition.get(lbl,None)
|
|
1249
|
+
if not c:
|
|
1250
|
+
return _atomdb(lbl)
|
|
1251
|
+
zvals = set()
|
|
1252
|
+
for frac,s in c:
|
|
1253
|
+
ad = _atomdb(s)
|
|
1254
|
+
zvals.add(ad.Z() if (ad and ad.isElement()) else None)
|
|
1255
|
+
if len(zvals)==1:
|
|
1256
|
+
return zvals.pop()
|
|
1257
|
+
|
|
1258
|
+
z_2_dilbl = extract_z2lbl( dilist, lambda di : (di.atomData, di.atomData.displayLabel()) )
|
|
1259
|
+
z_2_atomlbls = extract_z2lbl( labels, lambda lbl : ( lookup_zval_for_label(lbl), lbl ), allow_multi = True )
|
|
1260
|
+
d = {}
|
|
1261
|
+
for z,lbls in z_2_atomlbls.items():
|
|
1262
|
+
dilbl = z_2_dilbl.get(z,None)
|
|
1263
|
+
if dilbl:
|
|
1264
|
+
for lbl in lbls:
|
|
1265
|
+
d[lbl] = dilbl
|
|
1266
|
+
return d
|
|
1267
|
+
|
|
1268
|
+
def extract_dyninfo_objects( labels, compositions, source, mapping ):
|
|
1269
|
+
|
|
1270
|
+
keepalive = None
|
|
1271
|
+
if isinstance( source, _nc_core.Info.DynamicInfo ):
|
|
1272
|
+
description, dilist = 'DynamicInfo object', [source]
|
|
1273
|
+
elif ( hasattr(source,'__len__')
|
|
1274
|
+
and not hasattr(source,'keys') #NB: '__len__' but not 'keys': list-like but not dict-like.
|
|
1275
|
+
and ( len(source)==0 or all(isinstance(e, _nc_core.Info.DynamicInfo) for e in source ) ) ):
|
|
1276
|
+
#sequence of dyninfo objects
|
|
1277
|
+
description, dilist = 'list of DynamicInfo objects', list( e for e in source )
|
|
1278
|
+
else:
|
|
1279
|
+
from .misc import MaterialSource
|
|
1280
|
+
ms = MaterialSource(source)
|
|
1281
|
+
description = ms.description
|
|
1282
|
+
_loadedmat = ms.load( doScatter = False, doAbsorption = False )
|
|
1283
|
+
keepalive = _loadedmat
|
|
1284
|
+
if not _loadedmat.info:
|
|
1285
|
+
raise _nc_core.NCBadInput('Material source has no Info object'
|
|
1286
|
+
' (and therefore no DynamicInfo objects)')
|
|
1287
|
+
dilist = _loadedmat.info.dyninfos
|
|
1288
|
+
|
|
1289
|
+
if not mapping:
|
|
1290
|
+
#mapping is srclbl -> label
|
|
1291
|
+
mapping = _determine_dyninfo_mapping( labels, compositions, dilist )
|
|
1292
|
+
|
|
1293
|
+
out = {}
|
|
1294
|
+
dilbl_2_di = dict( (di.atomData.displayLabel(),di) for di in dilist )
|
|
1295
|
+
for lbl in labels:
|
|
1296
|
+
target_dilbl = mapping.get(lbl,None)
|
|
1297
|
+
if not target_dilbl:
|
|
1298
|
+
continue
|
|
1299
|
+
di = dilbl_2_di.get(target_dilbl,None)
|
|
1300
|
+
if not di:
|
|
1301
|
+
_='", "'.join(sorted(dilbl_2_di.keys()))
|
|
1302
|
+
raise _nc_core.NCBadInput(f'No display label "{target_dilbl}" found in source (source has display labels "{_}").')
|
|
1303
|
+
comment = f'Transferred from "{target_dilbl}" in "{description}"'
|
|
1304
|
+
out[lbl] = dict( obj=di, comment=comment )
|
|
1305
|
+
|
|
1306
|
+
return keepalive, out
|
|
1307
|
+
|
|
1308
|
+
def _atomdb(lbl):
|
|
1309
|
+
from .atomdata import atomDB
|
|
1310
|
+
return atomDB( lbl, throwOnErrors=False )
|
|
1311
|
+
|
|
1312
|
+
def calc_mass( label, composition ):
|
|
1313
|
+
if not composition:
|
|
1314
|
+
_ = _atomdb( label )
|
|
1315
|
+
return _.averageMassAMU() if _ else None
|
|
1316
|
+
elif len( composition )==1:
|
|
1317
|
+
from .atomdata import atomDB
|
|
1318
|
+
return atomDB( composition[0][1] ).averageMassAMU()
|
|
1319
|
+
else:
|
|
1320
|
+
ll=[]
|
|
1321
|
+
fracs = []
|
|
1322
|
+
for frac, _lbl in composition:
|
|
1323
|
+
_ = _atomdb( _lbl )
|
|
1324
|
+
if not _:
|
|
1325
|
+
return None
|
|
1326
|
+
fracs.append( frac )
|
|
1327
|
+
ll.append( frac * _.averageMassAMU() )
|
|
1328
|
+
return math.fsum( ll ) / math.fsum( fracs) #NB: sum(fracs) is likely
|
|
1329
|
+
#already guaranteed 1 here,
|
|
1330
|
+
#playing it safe
|
|
1331
|
+
|
|
1332
|
+
def _composerimpl_from_info( infoobj ):
|
|
1333
|
+
if isinstance(infoobj,_nc_core.Info):
|
|
1334
|
+
i = infoobj
|
|
1335
|
+
elif hasattr(infoobj,'info') and isinstance(infoobj.info,_nc_core.Info):
|
|
1336
|
+
i = infoobj.info
|
|
1337
|
+
else:
|
|
1338
|
+
raise _nc_core.NCBadInput('NCMATComposer.from_info got unexpected argument of wrong type')
|
|
1339
|
+
|
|
1340
|
+
if not i.isSinglePhase():
|
|
1341
|
+
raise _nc_core.NCBadInput('NCMATComposer can only be initialsed from single-phase materials')
|
|
1342
|
+
o = NCMATComposerImpl(data=None,fmt=None,quiet=True)
|
|
1343
|
+
top_atomdata = []
|
|
1344
|
+
|
|
1345
|
+
#Transfer temperature. In case of gasses we also lock the value, since
|
|
1346
|
+
#density depends very strongly on temperature for such materials (it is of
|
|
1347
|
+
#course true for any state of matter, but for gasses it is extreme):
|
|
1348
|
+
if i.stateOfMatter() == _nc_core.StateOfMatter.Gas:
|
|
1349
|
+
o.lock_temperature( i.getTemperature() )
|
|
1350
|
+
else:
|
|
1351
|
+
o.set_default_temperature( i.getTemperature() )
|
|
1352
|
+
|
|
1353
|
+
if i.isCrystalline():
|
|
1354
|
+
si = i.structure_info
|
|
1355
|
+
si_natoms = si['n_atoms']
|
|
1356
|
+
o.set_cellsg( a = si['a'], b = si['b'], c = si['c'],
|
|
1357
|
+
alpha = si['alpha'], beta = si['beta'], gamma = si['gamma'],
|
|
1358
|
+
spacegroup = ( si['spacegroup'] or None ) )
|
|
1359
|
+
all_atompos = []
|
|
1360
|
+
for ai in i.atominfos:
|
|
1361
|
+
ad = ai.atomData
|
|
1362
|
+
top_atomdata.append(ad)
|
|
1363
|
+
lbl = ad.displayLabel()
|
|
1364
|
+
di = ai.dyninfo
|
|
1365
|
+
o.set_dyninfo_from_object( lbl, di )
|
|
1366
|
+
for x,y,z in ai.positions:
|
|
1367
|
+
all_atompos.append( (lbl,x,y,z) )#NB: site_occupancy=1 here!
|
|
1368
|
+
from .exceptions import nc_assert
|
|
1369
|
+
nc_assert(len(all_atompos)==si_natoms)
|
|
1370
|
+
o.set_atompos( all_atompos )
|
|
1371
|
+
else:
|
|
1372
|
+
o.set_density( i.density, unit='g/cm3' )
|
|
1373
|
+
if i.stateOfMatter() == _nc_core.StateOfMatter.Solid:
|
|
1374
|
+
o.set_state_of_matter( 'solid' )
|
|
1375
|
+
elif i.stateOfMatter() == _nc_core.StateOfMatter.Liquid:
|
|
1376
|
+
o.set_state_of_matter( 'liquid' )
|
|
1377
|
+
elif i.stateOfMatter() == _nc_core.StateOfMatter.Gas:
|
|
1378
|
+
o.set_state_of_matter( 'gas' )
|
|
1379
|
+
else:
|
|
1380
|
+
assert i.stateOfMatter() == _nc_core.StateOfMatter.Unknown
|
|
1381
|
+
for di in i.dyninfos:
|
|
1382
|
+
ad = di.atomData
|
|
1383
|
+
top_atomdata.append(ad)
|
|
1384
|
+
lbl = ad.displayLabel()
|
|
1385
|
+
o.set_dyninfo_from_object( lbl, di )
|
|
1386
|
+
o.set_fraction( lbl, di.fraction )
|
|
1387
|
+
|
|
1388
|
+
#Handle ATOMDB:
|
|
1389
|
+
def _extractad( ad ):
|
|
1390
|
+
if not ad.isComposite():
|
|
1391
|
+
return [ ( 1.0, ad )], [ ad ]
|
|
1392
|
+
basicads = []
|
|
1393
|
+
compos = []
|
|
1394
|
+
for comp in ad.components:
|
|
1395
|
+
_frac, _ad = comp.fraction, comp.data
|
|
1396
|
+
if not _ad.isComposite():
|
|
1397
|
+
compos.append( ( _frac, _ad ) )
|
|
1398
|
+
basicads.append( _ad )
|
|
1399
|
+
else:
|
|
1400
|
+
_subcompos, _subbasicads = _extractad( _ad )
|
|
1401
|
+
basicads += _subbasicads
|
|
1402
|
+
for _subfrac, _subad in _subcompos:
|
|
1403
|
+
compos.append( (_frac*_subfrac, _subad) )
|
|
1404
|
+
return compos, basicads
|
|
1405
|
+
|
|
1406
|
+
basic_atomdata = []#all non-composite atomdatas
|
|
1407
|
+
for ad in top_atomdata:
|
|
1408
|
+
_compos, _bads = _extractad( ad )
|
|
1409
|
+
basic_atomdata += _bads
|
|
1410
|
+
o.set_composition( ad.displayLabel(), [(frac,ad.description(False)) for frac,ad in _compos] )
|
|
1411
|
+
|
|
1412
|
+
#any non-builtin atom data:
|
|
1413
|
+
_seen = {}
|
|
1414
|
+
for ad in basic_atomdata:
|
|
1415
|
+
assert ad.isElement() or ad.isIsotope()
|
|
1416
|
+
key = (ad.Z(),ad.A())
|
|
1417
|
+
_adstr = ad.to_atomdb_str()
|
|
1418
|
+
_seenstr = _seen.get(key,None)
|
|
1419
|
+
if _seenstr is not None:
|
|
1420
|
+
if _adstr != _seenstr:
|
|
1421
|
+
raise _nc_core.NCBadInput('Atom with (Z,A)=(%i,%i) appears in multiple roles with different data values in material. Such materials are not supported by the NCMATComposer.'%key)
|
|
1422
|
+
continue
|
|
1423
|
+
_seen[ key ] = _adstr
|
|
1424
|
+
from .atomdata import atomDB
|
|
1425
|
+
_builtin = atomDB(Z=key[0],A=key[1],throwOnErrors=False)
|
|
1426
|
+
if not _builtin or _adstr != _builtin.to_atomdb_str():
|
|
1427
|
+
o.update_atomdb( ad.description(False), _adstr )
|
|
1428
|
+
|
|
1429
|
+
#Custom sections:
|
|
1430
|
+
for nm,cnt_lists in (i.customsections or []):
|
|
1431
|
+
if nm == 'HARDSPHERESANS':
|
|
1432
|
+
_nc_common.warn('Ignoring @CUSTOM_HARDSPHERESANS sections in input.')
|
|
1433
|
+
continue
|
|
1434
|
+
if nm == 'UNOFFICIALHACKS':
|
|
1435
|
+
_nc_common.warn('Ignoring @CUSTOM_UNOFFICIALHACKS sections in input.')
|
|
1436
|
+
continue
|
|
1437
|
+
if o.get_custom_section_data(nm) is not None:
|
|
1438
|
+
raise _nc_core.NCBadInput(f'Multiple @CUSTOM_{nm} sections in input'
|
|
1439
|
+
'is not supported by NCMATComposer.')
|
|
1440
|
+
cnt=''
|
|
1441
|
+
for linedata in cnt_lists:
|
|
1442
|
+
cnt += ' '.join(linedata)
|
|
1443
|
+
cnt += '\n'
|
|
1444
|
+
o.set_custom_section_data(nm,cnt)
|
|
1445
|
+
|
|
1446
|
+
return o
|
|
1447
|
+
|
|
1448
|
+
def _checklabel(lbl):
|
|
1449
|
+
if not lbl:
|
|
1450
|
+
raise ValueError(f'invalid label "{lbl}"')
|
|
1451
|
+
|
|
1452
|
+
def _decode_composition(label,*composition):
|
|
1453
|
+
_checklabel(label)
|
|
1454
|
+
|
|
1455
|
+
label = ' '.join(str(label).split())
|
|
1456
|
+
errmsg='Invalid composition syntax'
|
|
1457
|
+
|
|
1458
|
+
if ' is ' in label:
|
|
1459
|
+
_=label.split()
|
|
1460
|
+
if len(_)>2 and _[1]=='is':
|
|
1461
|
+
if composition:
|
|
1462
|
+
raise _nc_core.NCBadInput(errmsg)
|
|
1463
|
+
return _decode_composition( _[0], _[2:] )
|
|
1464
|
+
|
|
1465
|
+
if not composition:
|
|
1466
|
+
raise _nc_core.NCBadInput(errmsg)
|
|
1467
|
+
|
|
1468
|
+
_decodeflt = _nc_common._decodeflt
|
|
1469
|
+
#We want composition to end up in the final form: [(f0,name0),(f1,name1),...].
|
|
1470
|
+
def is_final_form(c):
|
|
1471
|
+
return ( all(hasattr(e,'__len__') for e in c)
|
|
1472
|
+
and all(len(e)==2 for e in c)
|
|
1473
|
+
and all(_decodeflt(e[0]) for e in c)
|
|
1474
|
+
and all(hasattr(e[1],'split') for e in c) )
|
|
1475
|
+
|
|
1476
|
+
single_arg = composition[0] if len(composition)==1 else None
|
|
1477
|
+
if single_arg and is_final_form(single_arg):
|
|
1478
|
+
single_arg,composition = None,single_arg
|
|
1479
|
+
|
|
1480
|
+
if single_arg:
|
|
1481
|
+
if hasattr(single_arg,'split'):
|
|
1482
|
+
#assume string form like: 'Al' or '0.2 Al 0.8 Cr'
|
|
1483
|
+
p = single_arg.split()
|
|
1484
|
+
if len(p)!=1:
|
|
1485
|
+
return _decode_composition( label, *p )
|
|
1486
|
+
#single element form like: 'Al'
|
|
1487
|
+
norm_ident = _nc_common.check_elem_or_isotope_marker( p[0] )
|
|
1488
|
+
if not norm_ident:
|
|
1489
|
+
raise _nc_core.NCBadInput(errmsg+': invalid element/isotope marker "%s"'%p[0])
|
|
1490
|
+
return label, [(1.0,norm_ident)]
|
|
1491
|
+
elif ( hasattr(single_arg,'__len__')
|
|
1492
|
+
and len(single_arg)>=2 and len(single_arg)%2==0
|
|
1493
|
+
and all(_decodeflt(e) for e in single_arg[::2])
|
|
1494
|
+
and all(hasattr(e,'split') for e in single_arg[1::2]) ):
|
|
1495
|
+
#assume single_arg was a a list form like: [0.2,'Al',0.8,Cr]
|
|
1496
|
+
return _decode_composition( label, list(zip(single_arg[::2],single_arg[1::2])) )
|
|
1497
|
+
#unknown single arg form
|
|
1498
|
+
raise _nc_core.NCBadInput(errmsg)
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
assert len(composition)>1 or (len(composition)==1 and is_final_form(composition))
|
|
1502
|
+
|
|
1503
|
+
if ( not is_final_form(composition)
|
|
1504
|
+
and len(composition)%2 == 0
|
|
1505
|
+
and all(hasattr(e,'split') for e in composition[1::2])
|
|
1506
|
+
and all(_decodeflt(e) for e in composition[::2]) ):
|
|
1507
|
+
#assume form [f0,name0,f1,name1,...]
|
|
1508
|
+
composition = list(zip(composition[::2],composition[1::2]))
|
|
1509
|
+
|
|
1510
|
+
if not is_final_form(composition):
|
|
1511
|
+
raise _nc_core.NCBadInput(errmsg)
|
|
1512
|
+
|
|
1513
|
+
#composition is now in the form: [(f0,name0),(f1,name1),...].
|
|
1514
|
+
|
|
1515
|
+
ll=[]
|
|
1516
|
+
|
|
1517
|
+
for frac_orig, ident in composition:
|
|
1518
|
+
frac_val = _decodeflt( frac_orig )
|
|
1519
|
+
if frac_val is None:
|
|
1520
|
+
raise _nc_core.NCBadInput(errmsg+': invalid fraction specification "%s"'%frac_orig)
|
|
1521
|
+
if not (0<frac_val<=1.0):
|
|
1522
|
+
raise _nc_core.NCBadInput(errmsg+': fraction specification "%s" is not in (0,1]'%frac_orig)
|
|
1523
|
+
norm_ident = _nc_common.check_elem_or_isotope_marker( ident )
|
|
1524
|
+
if not norm_ident:
|
|
1525
|
+
raise _nc_core.NCBadInput(errmsg+': invalid element or isotope identifier "%s"'%ident)
|
|
1526
|
+
ll.append( ( frac_val, norm_ident ) )
|
|
1527
|
+
fractot = math.fsum(f for f,lbl in ll)
|
|
1528
|
+
if abs(fractot-1.0)>1e-5:
|
|
1529
|
+
raise _nc_core.NCBadInput(errmsg+': fractions do not sum to 1')
|
|
1530
|
+
|
|
1531
|
+
#Now merge identical entries and snap fraction sum to 1:
|
|
1532
|
+
d = {}
|
|
1533
|
+
for fr,nme in ll:
|
|
1534
|
+
if nme not in d:
|
|
1535
|
+
d[nme] = [fr]
|
|
1536
|
+
else:
|
|
1537
|
+
d[nme] += [fr]
|
|
1538
|
+
ll = []
|
|
1539
|
+
for mfr, nme in sorted( (-math.fsum(frs)/fractot,nme) for nme,frs in d.items()):
|
|
1540
|
+
if mfr:
|
|
1541
|
+
ll.append( ( -mfr, nme ) )
|
|
1542
|
+
return label, ll
|
|
1543
|
+
|
|
1544
|
+
def determine_labels_and_atomdb( _self_params, *, fractions, allow_siteoccu_ncmatv5_hack = True ):
|
|
1545
|
+
compositions = _self_params.get('compositions',{})
|
|
1546
|
+
#compositions = copy.deepcopy(compositions)#we will modify a bit below and need to retain immutability
|
|
1547
|
+
atomdb = _self_params.get('atomdb',{})
|
|
1548
|
+
atompos = _self_params.get('atompos',None)
|
|
1549
|
+
occumap = atompos['occumap'] if atompos else None
|
|
1550
|
+
dyninfos = _self_params.get('dyninfos',{})
|
|
1551
|
+
param_fallback_debye_temp = _self_params.get('fallback_debye_temp',None)
|
|
1552
|
+
|
|
1553
|
+
#labels:
|
|
1554
|
+
dyninfo_lbls = set( dyninfos.keys() )
|
|
1555
|
+
atompos_lbls = set( lbl for lbl,x,y,z in atompos['pos'] ) if atompos else set()
|
|
1556
|
+
fractions_lbls = set( (fractions or {}).keys() )
|
|
1557
|
+
direct_lbls = dyninfo_lbls.union( atompos_lbls ).union( fractions_lbls )
|
|
1558
|
+
|
|
1559
|
+
#fill expand implicit compositions to be included explicitly:
|
|
1560
|
+
def _expand_implicit_compositions( direct_lbls, compositions ):
|
|
1561
|
+
for lbl,_ in compositions.items():
|
|
1562
|
+
if lbl not in direct_lbls:
|
|
1563
|
+
raise _nc_core.NCBadInput('Label "%s" was given a composition but is not actually used in the material'%lbl)
|
|
1564
|
+
|
|
1565
|
+
compositions = copy.deepcopy(compositions)#retain immutability
|
|
1566
|
+
for lbl in direct_lbls:
|
|
1567
|
+
if lbl not in compositions:
|
|
1568
|
+
_ = _nc_common.check_elem_or_isotope_marker( lbl )
|
|
1569
|
+
if not _:
|
|
1570
|
+
raise _nc_core.NCBadInput('Not able to determine composition associated with label '
|
|
1571
|
+
f'"{lbl}" since it was not an element or isotope. Please add a definition with .set_composition(..).')
|
|
1572
|
+
compositions[lbl] = [(1.0,_)]
|
|
1573
|
+
return compositions
|
|
1574
|
+
compositions = _expand_implicit_compositions( direct_lbls, compositions )
|
|
1575
|
+
|
|
1576
|
+
#check completeness of labels in other sections:
|
|
1577
|
+
if atompos:
|
|
1578
|
+
if (dyninfo_lbls - atompos_lbls):
|
|
1579
|
+
raise _nc_core.NCBadInput('Some atoms with dynamic information are missing atomic positions: "%s"'%('", "'.join(dyninfo_lbls - atompos_lbls)))
|
|
1580
|
+
if not param_fallback_debye_temp and (atompos_lbls-dyninfo_lbls):
|
|
1581
|
+
raise _nc_core.NCBadInput('Missing dynamic information for some atoms: "%s"'%('", "'.join(atompos_lbls-dyninfo_lbls)))
|
|
1582
|
+
else:
|
|
1583
|
+
if (dyninfo_lbls-fractions_lbls):
|
|
1584
|
+
raise _nc_core.NCBadInput('Some atoms with dynamic information are missing fractions: "%s"'%('", "'.join(dyninfo_lbls - fractions_lbls)))
|
|
1585
|
+
|
|
1586
|
+
lbls_with_nonunit_occu = set( lbl for lbl,occu in occumap.items() if occu != 1.0 ) if occumap else set()
|
|
1587
|
+
|
|
1588
|
+
#SPECIAL HACK BEGIN
|
|
1589
|
+
if lbls_with_nonunit_occu and not allow_siteoccu_ncmatv5_hack:
|
|
1590
|
+
raise _nc_core.NCBadInput('Can not support site occupancies != 1.0 without the special hack for NCMAT v5 (and allow_siteoccu_ncmatv5_hack was set to False)')
|
|
1591
|
+
|
|
1592
|
+
if lbls_with_nonunit_occu:
|
|
1593
|
+
#Handle via (non-persistent) modifications of compositions + atomdb:
|
|
1594
|
+
atomdb = copy.deepcopy(atomdb)
|
|
1595
|
+
atomdb_hack_comments = []
|
|
1596
|
+
for i,lbl in enumerate(sorted(lbls_with_nonunit_occu)):
|
|
1597
|
+
origelem_mass = calc_mass(lbl,compositions[lbl])
|
|
1598
|
+
compos = compositions[lbl][:]
|
|
1599
|
+
origelem_lbl = lbl if compos is None else _nc_common.format_chemform(list((v,k) for k,v in compos))
|
|
1600
|
+
assert 0.0 < origelem_mass < 2000.0
|
|
1601
|
+
#In principle I should check this is not already used, but if anyone
|
|
1602
|
+
#actually needs Og999 or thereabouts, they are obviously just trying
|
|
1603
|
+
#to break our hack by being smartasses :-)
|
|
1604
|
+
hijackedIsotope = 'Og%i'%(299-i)
|
|
1605
|
+
assert hijackedIsotope not in atomdb
|
|
1606
|
+
atomdb[hijackedIsotope] = _nc_core.AtomData.fmt_atomdb_str( origelem_mass, 0.0, 0.0, 0.0 )
|
|
1607
|
+
occu = occumap[lbl]
|
|
1608
|
+
compositions[ lbl ] = [ (occu*frac,elem) for frac,elem in compos ] + [ ( 1.0 - occu, hijackedIsotope ) ]
|
|
1609
|
+
atomdb_hack_comments.append ( f'#Note: Emulating site_occupancy={occu:g} for {origelem_lbl} by hijacking {hijackedIsotope} to play the role as "sterile {origelem_lbl}".' )
|
|
1610
|
+
#SPECIAL HACK END
|
|
1611
|
+
|
|
1612
|
+
atomdb_lines = []
|
|
1613
|
+
atomdb_lines += atomdb_hack_comments
|
|
1614
|
+
for lbl,paramstr in sorted( atomdb.items() ):
|
|
1615
|
+
atomdb_lines.append('%s %s'%(lbl,paramstr))
|
|
1616
|
+
|
|
1617
|
+
#We must remap exactly those labels which are not a single element or
|
|
1618
|
+
#isotopes, or those single/element isotopes that have more than one
|
|
1619
|
+
#role. So first we fill the composition for ALL:
|
|
1620
|
+
lblmap = {}
|
|
1621
|
+
Xidx_next = 1
|
|
1622
|
+
for lbl in sorted(direct_lbls):
|
|
1623
|
+
compos = compositions[lbl]
|
|
1624
|
+
if len(compos) == 1 and sum(1 for lbl,e in compositions.items() if (len(e)==1 and e[0][1]==compos[0][1]) ) == 1:
|
|
1625
|
+
lblmap[ lbl ] = compos[0][1]
|
|
1626
|
+
continue
|
|
1627
|
+
#must use special label X1..X99/X:
|
|
1628
|
+
if Xidx_next>100:
|
|
1629
|
+
raise _nc_core.NCBadInput('Material requires more than 100 special labels which is'
|
|
1630
|
+
+' not supported by NCMAT (allowed are only X1,X2,...,X99 and X')
|
|
1631
|
+
speciallbl = 'X' if Xidx_next==100 else 'X%i'%Xidx_next
|
|
1632
|
+
Xidx_next += 1
|
|
1633
|
+
lblmap[lbl] = speciallbl
|
|
1634
|
+
if len(compos)==1:
|
|
1635
|
+
assert compos[0][0]==1.0
|
|
1636
|
+
atomdb_lines.append( '%s is %s'%(speciallbl,compos[0][1]))
|
|
1637
|
+
else:
|
|
1638
|
+
s=''
|
|
1639
|
+
for frac,name in compos:
|
|
1640
|
+
s += f' {frac:.12g} {name}'
|
|
1641
|
+
atomdb_lines.append( '%s is%s'%(speciallbl,s))
|
|
1642
|
+
return lblmap,atomdb_lines
|
|
1643
|
+
|
|
1644
|
+
def _is_nonempty_array( x ):
|
|
1645
|
+
return hasattr(x,'__len__') and len(x)>0
|
|
1646
|
+
|
|
1647
|
+
def _fmtprecisenum( v, strip_leading_zero = True ):
|
|
1648
|
+
_ = f'{v:.14g}' if v else '0'
|
|
1649
|
+
return _[1:] if (strip_leading_zero and _.startswith('0.')) else _
|
|
1650
|
+
|
|
1651
|
+
def _copyarray_or_None( x ):
|
|
1652
|
+
if x is None or (hasattr(x,'__len__') and len(x)==0):
|
|
1653
|
+
return None
|
|
1654
|
+
from ._numpy import _np
|
|
1655
|
+
return copy.deepcopy(_np.asarray(x,dtype=float) if _np else x)
|
|
1656
|
+
|
|
1657
|
+
def _decode_update_atomdb(element_or_isotope, data, *, mass=None, coh_scat_len=None, incoh_xs=None, abs_xs=None ):
|
|
1658
|
+
e = _nc_common.check_elem_or_isotope_marker( element_or_isotope )
|
|
1659
|
+
if not e:
|
|
1660
|
+
raise _nc_core.NCBadInput(f'Invalid element or isotope label (must be of form "Al", "O16", "D", ...): {e}')
|
|
1661
|
+
label = e
|
|
1662
|
+
|
|
1663
|
+
nphys = int(bool(mass))+int(bool(coh_scat_len))+int(bool(incoh_xs))+int(bool(abs_xs))
|
|
1664
|
+
hasdata = data is not None
|
|
1665
|
+
if not hasdata and not nphys:
|
|
1666
|
+
raise _nc_core.NCBadInput('Missing data parameter(s).')
|
|
1667
|
+
if (hasdata and nphys) or nphys not in (0,4):
|
|
1668
|
+
raise _nc_core.NCBadInput('Inconsistent set of parameters provided.')
|
|
1669
|
+
if hasdata:
|
|
1670
|
+
if hasattr(data,'to_atomdb_str'):
|
|
1671
|
+
#Handle AtomData objects (and others with to_atomdb_str method)
|
|
1672
|
+
res = data.to_atomdb_str()
|
|
1673
|
+
else:
|
|
1674
|
+
res = ' '.join(str(data).replace(':',' ').strip().split())
|
|
1675
|
+
else:
|
|
1676
|
+
assert nphys==4
|
|
1677
|
+
res = _nc_core.AtomData.fmt_atomdb_str( mass = mass,
|
|
1678
|
+
coh_scat_len = coh_scat_len,
|
|
1679
|
+
incoh_xs = incoh_xs,
|
|
1680
|
+
abs_xs = abs_xs )
|
|
1681
|
+
assert res and 'fm ' in res and 'u ' in res and 'b ' in res
|
|
1682
|
+
return label,res
|
|
1683
|
+
|
|
1684
|
+
def _lattice_params_to_vectors( a, b, c, alpha, beta, gamma ):
|
|
1685
|
+
if max(max(alpha,beta),gamma)<=math.pi:
|
|
1686
|
+
raise _nc_core.NCBadInput("angles are expected in degrees not radians")
|
|
1687
|
+
def _sincos( x ):
|
|
1688
|
+
if x == 0:
|
|
1689
|
+
return 0.0, 1.0
|
|
1690
|
+
if x == 90.0:
|
|
1691
|
+
return 1.0, 0.0
|
|
1692
|
+
if x == 120.0:
|
|
1693
|
+
return 0.86602540378443864676372317075293618347140262690519, -0.5
|
|
1694
|
+
return math.sin( x*(math.pi/180) ), math.cos( x*(math.pi/180) )
|
|
1695
|
+
sa,ca = _sincos( alpha )
|
|
1696
|
+
sb,cb = _sincos( beta )
|
|
1697
|
+
sg,cg = _sincos( gamma )
|
|
1698
|
+
assert sg > 0.0
|
|
1699
|
+
vect_a = tuple( a*e for e in ( 1, 0.0, 0.0 ) )
|
|
1700
|
+
vect_b = tuple( b*e for e in ( cg, sg, 0.0 ) )
|
|
1701
|
+
ux = cb
|
|
1702
|
+
assert sg > 0.0
|
|
1703
|
+
assert sg**2 > 0.0
|
|
1704
|
+
uy = (ca-cb*cg)/sg
|
|
1705
|
+
uzsq = sb**2 - uy**2
|
|
1706
|
+
if uzsq < -1e-99:
|
|
1707
|
+
raise _nc_core.NCBadInput('Inconsistent cell parameters detected')
|
|
1708
|
+
vect_c = tuple( c*e for e in ( ux, uy, math.sqrt(max(0.0,uzsq)) ) )#sign of third component positive since we want the three vectors to form a right handed basis.
|
|
1709
|
+
return ( vect_a, vect_b, vect_c )
|
|
1710
|
+
|
|
1711
|
+
def _lattice_vectors_to_params( vect_a, vect_b, vect_c, as_dict = True ):
|
|
1712
|
+
def mag( v ):
|
|
1713
|
+
return math.sqrt( math.fsum(e**2 for e in v) )
|
|
1714
|
+
def dot( v1,v2 ):
|
|
1715
|
+
return math.fsum(e1*e2 for e1,e2 in zip(v1,v2))
|
|
1716
|
+
def ang( v1,v2 ):
|
|
1717
|
+
return math.acos( dot(v1,v2)/(mag(v1)*mag(v2)) )*180/math.pi
|
|
1718
|
+
a,b,c = mag(vect_a), mag(vect_b), mag(vect_c)
|
|
1719
|
+
alpha,beta,gamma = ang(vect_b,vect_c), ang(vect_a,vect_c), ang(vect_a,vect_b)
|
|
1720
|
+
if as_dict:
|
|
1721
|
+
return dict( a=a, b=b, c=c, alpha=alpha, beta=beta, gamma=gamma )
|
|
1722
|
+
else:
|
|
1723
|
+
return a, b, c, alpha, beta, gamma
|
|
1724
|
+
|
|
1725
|
+
def _snap_lattice_params( params_dict, relative_tolerance = 1e-6 ):
|
|
1726
|
+
log = []
|
|
1727
|
+
p = copy.deepcopy(params_dict)
|
|
1728
|
+
def rd( x,y ):
|
|
1729
|
+
return abs(x-y)/(max(1e-300,abs(x)+abs(y)))
|
|
1730
|
+
super_tolerance = 1e-14#almost 64 bit FP prec
|
|
1731
|
+
assert relative_tolerance > super_tolerance
|
|
1732
|
+
def is_near( x,y ):
|
|
1733
|
+
return rd(x,y) < relative_tolerance
|
|
1734
|
+
def is_super_near( x,y ):
|
|
1735
|
+
return rd(x,y) < super_tolerance
|
|
1736
|
+
|
|
1737
|
+
#"snap" b,c to a,b and angles to 60/90/120:
|
|
1738
|
+
for l1,l2 in (('a','b'),('a','c'),('b','c')):
|
|
1739
|
+
if is_near(p[l1],p[l2]):
|
|
1740
|
+
if not is_super_near(p[l1],p[l2]):
|
|
1741
|
+
log.append(f'Snapping lattice parameter {l2} to {l1} since it was very near.')
|
|
1742
|
+
p[l2] = p[l1]
|
|
1743
|
+
for commonangle in (45,60,90,120):
|
|
1744
|
+
for angname in ['alpha','beta','gamma']:
|
|
1745
|
+
if is_near(p[angname],commonangle):
|
|
1746
|
+
if not is_super_near(p[angname],commonangle):
|
|
1747
|
+
log.append(f'Snapping lattice angle {angname} to {commonangle} since it was very near.')
|
|
1748
|
+
p[angname] = commonangle
|
|
1749
|
+
return log, p
|
|
1750
|
+
|
|
1751
|
+
def _cellparams_to_spglib_lattice( cellsg ):
|
|
1752
|
+
return _lattice_params_to_vectors( cellsg['a'], cellsg['b'], cellsg['c'],
|
|
1753
|
+
cellsg['alpha'], cellsg['beta'], cellsg['gamma'] )
|
|
1754
|
+
|
|
1755
|
+
def _reldiff_cellparams( c1, c2 ):
|
|
1756
|
+
#compares a,b,c,alpha,beta,gamma in the two cellsg arrays.
|
|
1757
|
+
def rd( x,y ):
|
|
1758
|
+
return abs(x-y)/(max(1e-300,abs(x)+abs(y)))
|
|
1759
|
+
def rdf( s ):
|
|
1760
|
+
return rd(c1[s],c2[s])
|
|
1761
|
+
return max( rdf(s) for s in ('a','b','c','alpha','beta','gamma') )
|
|
1762
|
+
|
|
1763
|
+
def _reldiff_atompos( spglib_cell1, spglib_cell2 ):
|
|
1764
|
+
#input in spglib cell format, returns largest dist between positions, or
|
|
1765
|
+
#None if not the same type+number of atoms.
|
|
1766
|
+
|
|
1767
|
+
for c in ( spglib_cell1, spglib_cell2 ):
|
|
1768
|
+
assert c is not None
|
|
1769
|
+
assert len(c) >= 3
|
|
1770
|
+
assert c[0] is not None
|
|
1771
|
+
_,allpos1,idxlist1 = spglib_cell1
|
|
1772
|
+
_,allpos2,idxlist2 = spglib_cell2
|
|
1773
|
+
assert len(idxlist1) == len(allpos1)
|
|
1774
|
+
assert len(idxlist2) == len(allpos2)
|
|
1775
|
+
if list(sorted(idxlist1)) != list(sorted(idxlist2)) or len(idxlist1)!=len(idxlist2):
|
|
1776
|
+
return None
|
|
1777
|
+
|
|
1778
|
+
def calc_maxdistsq( l1, l2 ):
|
|
1779
|
+
n = len(l1)
|
|
1780
|
+
assert n == len(l2)
|
|
1781
|
+
distsqmax = 0.0
|
|
1782
|
+
for e1 in l1:
|
|
1783
|
+
dsqmin_e1 = None
|
|
1784
|
+
for e2 in l2:
|
|
1785
|
+
_ = _unit_cell_point_distsq(e1,e2)
|
|
1786
|
+
if dsqmin_e1 is None or _ < dsqmin_e1:
|
|
1787
|
+
dsqmin_e1 = _
|
|
1788
|
+
if _ == 0.0:
|
|
1789
|
+
break
|
|
1790
|
+
assert dsqmin_e1 is not None
|
|
1791
|
+
distsqmax = max( dsqmin_e1, distsqmax )
|
|
1792
|
+
return distsqmax
|
|
1793
|
+
|
|
1794
|
+
max_distsq_seen = None
|
|
1795
|
+
for idx in sorted(set(idxlist1)):
|
|
1796
|
+
def fixp( p ):
|
|
1797
|
+
return (_remap_fract_pos(p[0]),
|
|
1798
|
+
_remap_fract_pos(p[1]),
|
|
1799
|
+
_remap_fract_pos(p[2]))
|
|
1800
|
+
l1 = list( sorted( fixp(p) for i,p in zip(idxlist1,allpos1) if i == idx ) )#nb: fragile sort order!
|
|
1801
|
+
l2 = list( sorted( fixp(p) for i,p in zip(idxlist2,allpos2) if i == idx ) )#nb: fragile sort order!
|
|
1802
|
+
d = calc_maxdistsq( l1, l2 )
|
|
1803
|
+
if d is None:
|
|
1804
|
+
return None
|
|
1805
|
+
if max_distsq_seen is None or d > max_distsq_seen:
|
|
1806
|
+
max_distsq_seen = d
|
|
1807
|
+
return math.sqrt(max_distsq_seen) if max_distsq_seen is not None else None
|
|
1808
|
+
|
|
1809
|
+
def _import_spglib( *, sysexit = False ):
|
|
1810
|
+
try:
|
|
1811
|
+
import spglib#both available on pypi and conda-forge
|
|
1812
|
+
except ImportError:
|
|
1813
|
+
m = ( 'Could not import spglib module needed to standardise and verify crystal structures.'
|
|
1814
|
+
+' The spglib package is available on both PyPI ("python3 -mpip install'
|
|
1815
|
+
+' spglib") and conda ("conda install -c conda-forge spglib")' )
|
|
1816
|
+
if sysexit:
|
|
1817
|
+
raise SystemExit(m)
|
|
1818
|
+
else:
|
|
1819
|
+
raise ImportError(m)
|
|
1820
|
+
return spglib.spglib
|
|
1821
|
+
|
|
1822
|
+
def _import_ase( *, sysexit = False ):
|
|
1823
|
+
try:
|
|
1824
|
+
import ase#both available on pypi and conda-forge
|
|
1825
|
+
import ase.io
|
|
1826
|
+
except ImportError:
|
|
1827
|
+
m = ( 'Could not import ase module.'
|
|
1828
|
+
+' The ase package is available on both PyPI ("python3 -mpip install'
|
|
1829
|
+
+' ase") and conda ("conda install -c conda-forge ase")' )
|
|
1830
|
+
if sysexit:
|
|
1831
|
+
raise SystemExit(m)
|
|
1832
|
+
else:
|
|
1833
|
+
raise ImportError(m)
|
|
1834
|
+
return ase, ase.io
|
|
1835
|
+
|
|
1836
|
+
def _spglib_extractsg( spglib_symdata ):
|
|
1837
|
+
sd = spglib_symdata
|
|
1838
|
+
sgno = ( getattr(sd,'number')
|
|
1839
|
+
if hasattr(sd,'number')
|
|
1840
|
+
else sd['number'] )
|
|
1841
|
+
sgsymb_hermann_mauguin = ( getattr(sd,'international')
|
|
1842
|
+
if hasattr(sd,'international')
|
|
1843
|
+
else sd['international'] )
|
|
1844
|
+
c = ( getattr(sd,'choice')
|
|
1845
|
+
if hasattr(sd,'choice')
|
|
1846
|
+
else sd.get('choice',None))
|
|
1847
|
+
if c:
|
|
1848
|
+
sgsymb_hermann_mauguin += f':{c}'
|
|
1849
|
+
return sgno, sgsymb_hermann_mauguin
|
|
1850
|
+
|
|
1851
|
+
def _check_cell_sg_consistency( sgnumber, a, b, c, alpha, beta, gamma ):
|
|
1852
|
+
sgclass = _nc_common._classifySG(int(sgnumber))
|
|
1853
|
+
if sgclass in ( 'orthorhombic', 'tetragonal', 'cubic'):
|
|
1854
|
+
if sgclass=='cubic':
|
|
1855
|
+
if a!=b or a!=c:
|
|
1856
|
+
raise _nc_core.NCBadInput(f'Cubic space group {sgnumber} requires cell parameters a==b==c (found a={a:g}, b={b:g}, c={c:g})')
|
|
1857
|
+
if not all( (_ == 90) for _ in (alpha, beta, gamma) ):
|
|
1858
|
+
raise _nc_core.NCBadInput(f'Space group {sgnumber} requires cell parameters alpha=beta=gamma=90 '
|
|
1859
|
+
+f'(found alpha={alpha}, beta={beta}, gamma={gamma})')
|
|
1860
|
+
elif sgclass in ('trigonal','hexagonal'):
|
|
1861
|
+
if not ( alpha == 90 and beta == 90 and gamma == 120 ):
|
|
1862
|
+
raise _nc_core.NCBadInput(f'Space group {sgnumber} requires cell parameters alpha=beta=90 and gamma=120 '
|
|
1863
|
+
+f'(found alpha={alpha}, beta={beta}, gamma={gamma})')
|
|
1864
|
+
else:
|
|
1865
|
+
if not all( (0<a<180) for a in ( alpha, beta, gamma ) ):
|
|
1866
|
+
raise _nc_core.NCBadInput('Unit cell must have all angles between 0 and 180'
|
|
1867
|
+
+f' (found alpha={alpha}, beta={beta}, gamma={gamma})')
|
|
1868
|
+
if int(sgnumber) >= 75:
|
|
1869
|
+
if a!=b:
|
|
1870
|
+
raise _nc_core.NCBadInput(f'Space group {sgnumber} requires cell parameters a==b (found a={a:g}, b={b:g}, c={c:g})')
|
|
1871
|
+
|
|
1872
|
+
def _spglib_refine_cell( spglib_cell, symprec = 0.01 ):
|
|
1873
|
+
assert spglib_cell
|
|
1874
|
+
p_orig = _lattice_vectors_to_params( *spglib_cell[0], as_dict = True )
|
|
1875
|
+
|
|
1876
|
+
warnings, msgs = [], []
|
|
1877
|
+
refined_cell, symdata = _impl_spglib_refine_cell( spglib_cell, symprec=symprec, warnings=warnings, msgs=msgs )
|
|
1878
|
+
|
|
1879
|
+
#check difference from first to final, to warn/msg about corrections:
|
|
1880
|
+
p_final = _lattice_vectors_to_params( *refined_cell[0], as_dict = True )
|
|
1881
|
+
_,p_final = _snap_lattice_params( p_final )
|
|
1882
|
+
sgno, sgsymb_hm = _spglib_extractsg( symdata )
|
|
1883
|
+
rdl = _reldiff_cellparams( p_orig, p_final )
|
|
1884
|
+
rda = _reldiff_atompos( spglib_cell, refined_cell )
|
|
1885
|
+
rd = None if (rdl is None or rda is None) else max(rdl,rda)
|
|
1886
|
+
discard_aniso = False
|
|
1887
|
+
if rd is None or rd > 1e-2:
|
|
1888
|
+
discard_aniso = True
|
|
1889
|
+
_ = 'possibly due to conversion from primitive cell' if rd is None else f'at the {100.0*rd:g}% level'
|
|
1890
|
+
warnings.append(f'Structure received major corrections by spglib ({_})')
|
|
1891
|
+
elif rd > 1e-6:
|
|
1892
|
+
warnings.append(f'Structure received minor corrections by spglib (at the {100.0*rd:g}% level)')
|
|
1893
|
+
else:
|
|
1894
|
+
msgs.append('Self-consistency of structure was verified by spglib')
|
|
1895
|
+
|
|
1896
|
+
#remove all but the last of any kind of error message (otherwise we might get many repetitions of 'snapping b=a' kind of messages, due to the recursion.
|
|
1897
|
+
|
|
1898
|
+
warnrev = []
|
|
1899
|
+
for w in warnings[::-1]:
|
|
1900
|
+
if w not in warnrev:
|
|
1901
|
+
warnrev.append(w)
|
|
1902
|
+
warnings = warnrev[::-1]
|
|
1903
|
+
|
|
1904
|
+
return dict( refined_cell = refined_cell,
|
|
1905
|
+
cellparams_snapped = p_final,
|
|
1906
|
+
sgno = sgno,
|
|
1907
|
+
sgsymb_hm = sgsymb_hm,
|
|
1908
|
+
warnings = warnings,
|
|
1909
|
+
msgs = msgs,
|
|
1910
|
+
can_keep_anisotropic_properties = not discard_aniso )
|
|
1911
|
+
|
|
1912
|
+
def _impl_spglib_refine_cell( spglib_cell, *, symprec, warnings, msgs, nrepeat = 0 ):
|
|
1913
|
+
spglib = _import_spglib()
|
|
1914
|
+
|
|
1915
|
+
orig_cell = spglib_cell
|
|
1916
|
+
|
|
1917
|
+
orig_cell_copy = copy.deepcopy( orig_cell )#copy is just a safeguard
|
|
1918
|
+
refined_cell = spglib.standardize_cell( orig_cell_copy, symprec = symprec )
|
|
1919
|
+
symdata = spglib.get_symmetry_dataset( orig_cell_copy )
|
|
1920
|
+
if not refined_cell or not symdata or refined_cell[0] is None:
|
|
1921
|
+
raise _nc_core.NCBadInput('Could not standardize provided crystal structure with spglib')
|
|
1922
|
+
|
|
1923
|
+
p = _lattice_vectors_to_params( *refined_cell[0], as_dict = True )
|
|
1924
|
+
|
|
1925
|
+
snaplog, p = _snap_lattice_params( p )
|
|
1926
|
+
if snaplog or nrepeat == 0:
|
|
1927
|
+
#This is either the first try, of we snapped some parameter
|
|
1928
|
+
#(e.g. alpha=89.99999 -> alpha=90.0). After this a second refinement
|
|
1929
|
+
#might find a different (higher) symmetry:
|
|
1930
|
+
warnings += snaplog[:]
|
|
1931
|
+
refined_cell = tuple( [ _cellparams_to_spglib_lattice( p ) ] + list( e for e in refined_cell[1:] ) )
|
|
1932
|
+
if nrepeat <= 3:
|
|
1933
|
+
return _impl_spglib_refine_cell( refined_cell, symprec = symprec, warnings=warnings, msgs=msgs, nrepeat = nrepeat + 1 )
|
|
1934
|
+
|
|
1935
|
+
return refined_cell, symdata
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
def _remap_fract_pos_pt(xyz):
|
|
1939
|
+
return ( _remap_fract_pos(xyz[0]),
|
|
1940
|
+
_remap_fract_pos(xyz[1]),
|
|
1941
|
+
_remap_fract_pos(xyz[2]) )
|
|
1942
|
+
|
|
1943
|
+
def _remap_fract_pos(x):
|
|
1944
|
+
x = float(x)
|
|
1945
|
+
if -1.0<=x<0.0:
|
|
1946
|
+
x += 1.0
|
|
1947
|
+
if 1.0<=x<2.0:
|
|
1948
|
+
x -= 1.0
|
|
1949
|
+
return x
|
|
1950
|
+
|
|
1951
|
+
def _unit_cell_point_dist( p1, p2 ):
|
|
1952
|
+
return math.sqrt( _unit_cell_point_distsq( p1, p2 ) )
|
|
1953
|
+
|
|
1954
|
+
def _unit_cell_point_distsq( p1, p2 ):
|
|
1955
|
+
#take into account unit cell periodicity, so e.g. (a,b,epsilon) and
|
|
1956
|
+
#(a,b,1.0-epsilon) are actually only 2*epsilon from each other, not
|
|
1957
|
+
#1.0-epsilon.
|
|
1958
|
+
def remap( x ):
|
|
1959
|
+
return x-math.floor(x)
|
|
1960
|
+
d = [ abs(remap(e1)-remap(e2)) for e1,e2 in zip(p1,p2)]
|
|
1961
|
+
return math.fsum( min(e,1.0-e)**2 for e in d )
|
|
1962
|
+
|
|
1963
|
+
def _cifdata_via_ase( data_or_file, ase_format = None, quiet = False ):
|
|
1964
|
+
import io
|
|
1965
|
+
ase,ase_io = _import_ase()
|
|
1966
|
+
ase_obj = data_or_file if (ase_format=='ase' or NCMATComposerImpl._is_ase_like_object( data_or_file )) else None
|
|
1967
|
+
prfct = ( lambda *a, **kwa : None ) if quiet else _nc_common.print
|
|
1968
|
+
|
|
1969
|
+
if not ase_obj and hasattr( data_or_file, 'load_data' ) and hasattr( data_or_file, 'codid' ) and hasattr( data_or_file, 'mpid' ):
|
|
1970
|
+
#assume object is CIFSource object:
|
|
1971
|
+
prfct('Attempting to load data via ASE')
|
|
1972
|
+
ase_obj = ase_io.read( io.StringIO( data_or_file.load_data( quiet = quiet) ),
|
|
1973
|
+
format = ase_format or 'cif' )
|
|
1974
|
+
|
|
1975
|
+
isbytes = hasattr( data_or_file, 'decode' )
|
|
1976
|
+
str_to_sb = ( lambda s : s ) if not isbytes else ( lambda s : s.encode() )
|
|
1977
|
+
if not ase_obj and ( hasattr(data_or_file,'__fspath__')
|
|
1978
|
+
or str_to_sb('\n') not in data_or_file ):
|
|
1979
|
+
#on-disk data:
|
|
1980
|
+
_ = 'data'
|
|
1981
|
+
if ( hasattr(data_or_file,'__fspath__') or isinstance(data_or_file,str) ):
|
|
1982
|
+
import pathlib
|
|
1983
|
+
_ = pathlib.Path(data_or_file).name
|
|
1984
|
+
prfct(f'Attempting to load {_} via ASE')
|
|
1985
|
+
ase_obj = ase_io.read( data_or_file, format = ase_format )
|
|
1986
|
+
if not ase_obj:
|
|
1987
|
+
#fall back to in-mem data
|
|
1988
|
+
if isbytes:
|
|
1989
|
+
dof_bytes = data_or_file
|
|
1990
|
+
try:
|
|
1991
|
+
dof_str = data_or_file.decode('utf8')
|
|
1992
|
+
except UnicodeDecodeError:
|
|
1993
|
+
dof_str = None
|
|
1994
|
+
else:
|
|
1995
|
+
dof_str = data_or_file
|
|
1996
|
+
try:
|
|
1997
|
+
dof_bytes = data_or_file.encode('utf8')
|
|
1998
|
+
except UnicodeEncodeError:
|
|
1999
|
+
dof_bytes = None
|
|
2000
|
+
|
|
2001
|
+
if not ase_format:
|
|
2002
|
+
import ase.io.formats
|
|
2003
|
+
if dof_bytes is not None:
|
|
2004
|
+
try:
|
|
2005
|
+
ase_format = ase.io.formats.match_magic( dof_bytes ).name
|
|
2006
|
+
except ase.io.formats.UnknownFileTypeError:
|
|
2007
|
+
ase_format = None
|
|
2008
|
+
if ase_format is None and dof_str is not None and '\n' not in dof_str:
|
|
2009
|
+
try:
|
|
2010
|
+
ase_format = ase.io.formats.filetype( dof_str ).name
|
|
2011
|
+
except ase.io.formats.UnknownFileTypeError:
|
|
2012
|
+
ase_format = None
|
|
2013
|
+
if not ase_format:
|
|
2014
|
+
_nc_common.warn('Could not determine ASE format of'
|
|
2015
|
+
' input data. Assuming CIF.')
|
|
2016
|
+
ase_format = 'cif'
|
|
2017
|
+
assert isinstance(ase_format,str)
|
|
2018
|
+
prfct('Attempting to load data via ASE')
|
|
2019
|
+
if dof_str:
|
|
2020
|
+
assert isinstance(dof_str,str)
|
|
2021
|
+
with io.StringIO(dof_str) as memfile:
|
|
2022
|
+
ase_obj = ase_io.read( memfile, format = ase_format )
|
|
2023
|
+
else:
|
|
2024
|
+
assert isinstance(dof_bytes,bytes)
|
|
2025
|
+
with io.BytesIO(dof_bytes) as memfile:
|
|
2026
|
+
ase_obj = ase_io.read( memfile, format = ase_format )
|
|
2027
|
+
|
|
2028
|
+
assert ase_obj is not None
|
|
2029
|
+
|
|
2030
|
+
with io.BytesIO() as s:
|
|
2031
|
+
ase_io.write(s, ase_obj, format='cif')
|
|
2032
|
+
return s.getvalue().decode()
|
|
2033
|
+
|
|
2034
|
+
def formatVectorForNCMAT(name,values,indent):
|
|
2035
|
+
def provideFormattedEntries():
|
|
2036
|
+
def _fmtnum(num):
|
|
2037
|
+
_ = '%g'%num if num else '0'#avoid 0.0, -0, etc.
|
|
2038
|
+
if _.startswith('0.'):
|
|
2039
|
+
_=_[1:]
|
|
2040
|
+
return _
|
|
2041
|
+
from ._numpy import _np
|
|
2042
|
+
v = _np.asarray(values,dtype=float).flatten() if _np else values
|
|
2043
|
+
i, nv = 0, len(v)
|
|
2044
|
+
while i < nv:
|
|
2045
|
+
fmt_vi=_fmtnum(v[i])
|
|
2046
|
+
#check if is repeated:
|
|
2047
|
+
irepeat=i
|
|
2048
|
+
while irepeat+1<nv:
|
|
2049
|
+
if _fmtnum(v[irepeat+1])==fmt_vi:
|
|
2050
|
+
irepeat+=1
|
|
2051
|
+
else:
|
|
2052
|
+
break
|
|
2053
|
+
yield '%sr%i'%(fmt_vi,1+irepeat-i) if irepeat>i else '%s'%fmt_vi
|
|
2054
|
+
i=irepeat+1#advance
|
|
2055
|
+
out=''
|
|
2056
|
+
line=' %s'%name
|
|
2057
|
+
collim=80
|
|
2058
|
+
for e in provideFormattedEntries():
|
|
2059
|
+
snext=' %s'%e
|
|
2060
|
+
line_next=line+snext
|
|
2061
|
+
if len(line_next)>collim:
|
|
2062
|
+
out += line
|
|
2063
|
+
out += '\n'
|
|
2064
|
+
line = indent+snext
|
|
2065
|
+
else:
|
|
2066
|
+
line = line_next
|
|
2067
|
+
if line:
|
|
2068
|
+
out += line
|
|
2069
|
+
out += '\n'
|
|
2070
|
+
return out
|
|
2071
|
+
|
|
2072
|
+
def _stripCommentsAndPackNCMATData( ncmat_data ):
|
|
2073
|
+
if hasattr( ncmat_data, 'rawData' ):
|
|
2074
|
+
return _stripCommentsAndPackNCMATData( ncmat_data.rawData )
|
|
2075
|
+
if not ncmat_data.startswith('NCMAT'):
|
|
2076
|
+
return ncmat_data#pass through broken data unchanged!
|
|
2077
|
+
out = ['']
|
|
2078
|
+
def addline( s ):
|
|
2079
|
+
if s:
|
|
2080
|
+
out[0] += ( s + '\n' )
|
|
2081
|
+
for e in ncmat_data.splitlines():
|
|
2082
|
+
p = e.split('#',1)
|
|
2083
|
+
if not p:
|
|
2084
|
+
continue
|
|
2085
|
+
p0 = ' '.join(p[0].split())
|
|
2086
|
+
if len(p)==2 and 'NCRYSTALMATCFG[' in p[1]:
|
|
2087
|
+
c = p[1].split('NCRYSTALMATCFG[',1)
|
|
2088
|
+
if len(c)==2:
|
|
2089
|
+
i = c[1].rfind(']')
|
|
2090
|
+
if i == -1:
|
|
2091
|
+
#seems to be a syntax error, just keep everything:
|
|
2092
|
+
addline(p0 + '#' + p[1])
|
|
2093
|
+
else:
|
|
2094
|
+
addline(p0 + '#NCRYSTALMATCFG[' + c[1][0:i].strip() + ']')
|
|
2095
|
+
else:
|
|
2096
|
+
addline(p0)
|
|
2097
|
+
return out[0]
|
|
2098
|
+
|
|
2099
|
+
def _extractInitialHeaderCommentsFromNCMATData( ncmat_data, dedent = True ):
|
|
2100
|
+
#Extracts initial comments, dedented appropriately. Returns None if data
|
|
2101
|
+
#does not look like NCMAT data (an empty list returned is not a failure, it
|
|
2102
|
+
#just means no comments).
|
|
2103
|
+
from .misc import AnyTextData
|
|
2104
|
+
td = AnyTextData(ncmat_data)
|
|
2105
|
+
if '\n' not in td.content and not td.content.startswith('NCMAT'):
|
|
2106
|
+
return None
|
|
2107
|
+
comments = []
|
|
2108
|
+
first = True
|
|
2109
|
+
for line in td.content.splitlines():
|
|
2110
|
+
if first:
|
|
2111
|
+
first = False
|
|
2112
|
+
continue
|
|
2113
|
+
if line.strip().startswith('@'):
|
|
2114
|
+
break
|
|
2115
|
+
line=line.lstrip()
|
|
2116
|
+
if line.startswith('#'):
|
|
2117
|
+
comments.append(line[1:].rstrip())
|
|
2118
|
+
#dedent and return:
|
|
2119
|
+
if dedent:
|
|
2120
|
+
while all( ( e.startswith(' ') or not e) for e in comments ):
|
|
2121
|
+
comments = [ e[1:] for e in comments ]
|
|
2122
|
+
return comments
|
|
2123
|
+
|
|
2124
|
+
def _check_valid_custom_section_name( name ):
|
|
2125
|
+
if ( not name
|
|
2126
|
+
or not isinstance(name,str)
|
|
2127
|
+
or not name.isalpha()
|
|
2128
|
+
or not name.isupper() ):
|
|
2129
|
+
raise _nc_core.NCBadInput(f'Invalid custom section name: "{name}" '
|
|
2130
|
+
'(must be non-empty and contain only'
|
|
2131
|
+
' capitalised letters A-Z)')
|