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/core.py
ADDED
|
@@ -0,0 +1,1910 @@
|
|
|
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
|
+
Module with core NCrystal functionality, including the OO classes (Info,
|
|
25
|
+
Scatter, Absorption, TextData, AtomData) and related factory methods.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .exceptions import ( NCrystalUserWarning, # noqa F401
|
|
30
|
+
NCException,
|
|
31
|
+
NCFileNotFound, # noqa F401
|
|
32
|
+
NCDataLoadError, # noqa F401
|
|
33
|
+
NCMissingInfo, # noqa F401
|
|
34
|
+
NCCalcError,
|
|
35
|
+
NCLogicError,
|
|
36
|
+
NCBadInput,
|
|
37
|
+
nc_assert )
|
|
38
|
+
|
|
39
|
+
from ._msg import _setDefaultPyMsgHandlerIfNotSet as _
|
|
40
|
+
_()
|
|
41
|
+
_=None
|
|
42
|
+
|
|
43
|
+
from ._chooks import _cstr2str, _get_raw_cfcts, _str2cstr, _get_build_namespace # noqa E402
|
|
44
|
+
from . import constants as _nc_constants # noqa E402
|
|
45
|
+
from ._numpy import _np,_ensure_numpy,_np_linspace # noqa E402
|
|
46
|
+
from . import _coreimpl as _impl # noqa E402
|
|
47
|
+
import enum as _enum # noqa E402
|
|
48
|
+
import ctypes as _ctypes # noqa E402
|
|
49
|
+
import weakref as _weakref # noqa E402
|
|
50
|
+
_rawfct = _get_raw_cfcts()
|
|
51
|
+
|
|
52
|
+
def get_version():
|
|
53
|
+
"""Get NCrystal version (same as NCrystal.__version__)"""
|
|
54
|
+
from . import __version__ as _v
|
|
55
|
+
return _v
|
|
56
|
+
|
|
57
|
+
def get_version_num():
|
|
58
|
+
"""Encode version in single integer (same as NCRYSTAL_VERSION C++ macro)
|
|
59
|
+
for easier version comparison. This is also available as a variable named
|
|
60
|
+
version_num in the main NCrystal module.
|
|
61
|
+
"""
|
|
62
|
+
return sum(int(i)*j for i,j in zip(get_version().split('.'),(1000000,1000,1)))
|
|
63
|
+
|
|
64
|
+
def get_version_tuple():
|
|
65
|
+
"""Get NCrystal version as a tuple like (3,9,3). This is also available
|
|
66
|
+
as a variable named version_tuple in the main NCrystal module.
|
|
67
|
+
"""
|
|
68
|
+
return tuple( int(i) for i in get_version().split('.') )
|
|
69
|
+
|
|
70
|
+
def get_build_namespace():
|
|
71
|
+
"""If compiled with NCRYSTAL_NAMESPACE_PROTECTION, return the namespace here
|
|
72
|
+
(will be an empty string in default installations).
|
|
73
|
+
"""
|
|
74
|
+
return _get_build_namespace()
|
|
75
|
+
|
|
76
|
+
class RCBase:
|
|
77
|
+
"""Base class for all NCrystal objects"""
|
|
78
|
+
def __init__(self, rawobj):
|
|
79
|
+
"""internal usage only"""
|
|
80
|
+
self._rawobj = rawobj
|
|
81
|
+
#do not ref here, since ncrystal_create_xxx functions in C-interface already did so.
|
|
82
|
+
self._rawunref = _rawfct['ncrystal_unref']#keep fct reference
|
|
83
|
+
self.__rawobj_byref = _ctypes.byref(rawobj)#keep byref(rawobj), since ctypes might
|
|
84
|
+
#disappear before __del__ is called.
|
|
85
|
+
def __del__(self):
|
|
86
|
+
if hasattr(self,'_rawunref') and self._rawunref:
|
|
87
|
+
self._rawunref(self.__rawobj_byref)
|
|
88
|
+
def refCount(self):
|
|
89
|
+
"""Access reference count of wrapped C++ object"""
|
|
90
|
+
return _rawfct['ncrystal_refcount'](self._rawobj)
|
|
91
|
+
|
|
92
|
+
class AtomData(RCBase):
|
|
93
|
+
"""Class providing physical constants related to a particular mix of
|
|
94
|
+
isotopes. This can be used to represent elements (i.e. all isotopes having
|
|
95
|
+
same Z) in either natural or enriched form, but can also be used to
|
|
96
|
+
represent atoms in doped crystals. E.g. if a small fraction (0.1%) of
|
|
97
|
+
Cr-ions replace some Al-ions in a Al2O3 lattice, the AtomData could
|
|
98
|
+
represent a mix of 0.1% Cr and 99.9% Al.
|
|
99
|
+
"""
|
|
100
|
+
def __init__(self,rawobj):
|
|
101
|
+
"""internal usage only"""
|
|
102
|
+
super(AtomData, self).__init__(rawobj)
|
|
103
|
+
f=_rawfct['ncrystal_atomdata_getfields'](rawobj)
|
|
104
|
+
self.__m = f['m']
|
|
105
|
+
self.__incxs = f['incxs']
|
|
106
|
+
self.__cohsl_fm = f['cohsl_fm']
|
|
107
|
+
self.__absxs = f['absxs']
|
|
108
|
+
self.__dl = f['dl']
|
|
109
|
+
self.__descr = f['descr']
|
|
110
|
+
self.__ncomp = f['ncomp']
|
|
111
|
+
self.__z = f['z']
|
|
112
|
+
self.__a = f['a']
|
|
113
|
+
self.__b2f = (self.__m/(self.__m+_nc_constants.const_neutron_mass_amu))**2
|
|
114
|
+
self.__comp = [None]*self.__ncomp
|
|
115
|
+
self.__compalldone = (self.__ncomp==0)
|
|
116
|
+
|
|
117
|
+
def averageMassAMU(self):
|
|
118
|
+
"""Atomic mass in Daltons (averaged appropriately over constituents)"""
|
|
119
|
+
return self.__m
|
|
120
|
+
def coherentScatLen(self):
|
|
121
|
+
"""Coherent scattering length in sqrt(barn)=10fm"""
|
|
122
|
+
return self.__cohsl_fm*0.1#0.1 is fm/sqrt(barn)
|
|
123
|
+
def coherentScatLenFM(self):
|
|
124
|
+
"""Coherent scattering length in fm"""
|
|
125
|
+
return self.__cohsl_fm
|
|
126
|
+
def coherentXS(self):
|
|
127
|
+
"""Bound coherent cross section in barn. Same as 4*pi*coherentScatLen()**2"""
|
|
128
|
+
return _nc_constants.k4Pidiv100*self.__cohsl_fm**2
|
|
129
|
+
def incoherentXS(self):
|
|
130
|
+
"""Bound incoherent cross section in barn"""
|
|
131
|
+
return self.__incxs
|
|
132
|
+
def scatteringXS(self):
|
|
133
|
+
"""Bound scattering cross section in barn (same as coherentXS()+incoherentXS())"""
|
|
134
|
+
return self.__incxs+self.coherentXS()
|
|
135
|
+
def captureXS(self):
|
|
136
|
+
"""Absorption cross section in barn"""
|
|
137
|
+
return self.__absxs
|
|
138
|
+
|
|
139
|
+
def freeScatteringXS(self):
|
|
140
|
+
"""Free scattering cross section in barn (same as freeCoherentXS()+freeIncoherentXS())"""
|
|
141
|
+
return self.__b2f * self.scatteringXS()
|
|
142
|
+
def freeCoherentXS(self):
|
|
143
|
+
"""Free coherent cross section in barn."""
|
|
144
|
+
return self.__b2f * self.coherentXS()
|
|
145
|
+
def freeIncoherentXS(self):
|
|
146
|
+
"""Free incoherent cross section in barn."""
|
|
147
|
+
return self.__b2f * self.incoherentXS()
|
|
148
|
+
|
|
149
|
+
def isNaturalElement(self):
|
|
150
|
+
"""Natural element with no composition."""
|
|
151
|
+
return self.__z!=0 and self.__ncomp==0 and self.__a==0
|
|
152
|
+
|
|
153
|
+
def isSingleIsotope(self):
|
|
154
|
+
"""Single isotope with no composition."""
|
|
155
|
+
return self.__a!=0
|
|
156
|
+
|
|
157
|
+
def isComposite(self):
|
|
158
|
+
"""Composite definition. See nComponents(), getComponent() and components property"""
|
|
159
|
+
return self.__ncomp!=0
|
|
160
|
+
|
|
161
|
+
def isElement(self):
|
|
162
|
+
"""If number of protons per nuclei is well defined. This is true for natural
|
|
163
|
+
elements, single isotopes, and composites where all components
|
|
164
|
+
have the same number of protons per nuclei."""
|
|
165
|
+
return self.__z!=0
|
|
166
|
+
|
|
167
|
+
def Z(self):
|
|
168
|
+
"""Number of protons per nuclei (0 if not well defined)."""
|
|
169
|
+
return self.__z
|
|
170
|
+
|
|
171
|
+
def elementName(self):
|
|
172
|
+
"""If Z()!=0, this returns the corresponding element name ('H', 'He', ...).
|
|
173
|
+
Returns empty string when Z() is 0."""
|
|
174
|
+
if not self.__z:
|
|
175
|
+
return ''
|
|
176
|
+
#NB: We are relying on natural elements to return their element names in
|
|
177
|
+
#description(false). This is promised by a comment in NCAtomData.hh!
|
|
178
|
+
if self.isNaturalElement():
|
|
179
|
+
return self.__descr
|
|
180
|
+
from .atomdata import elementZToName
|
|
181
|
+
return elementZToName(self.__z)
|
|
182
|
+
|
|
183
|
+
def A(self):
|
|
184
|
+
"""Number of nucleons per nuclei (0 if not well defined or natural element)."""
|
|
185
|
+
return self.__a
|
|
186
|
+
|
|
187
|
+
class Component:
|
|
188
|
+
def __init__(self,fr,ad):
|
|
189
|
+
"""internal usage only"""
|
|
190
|
+
self.__fr = fr
|
|
191
|
+
self.__ad = ad
|
|
192
|
+
assert not ad.isTopLevel()
|
|
193
|
+
@property
|
|
194
|
+
def fraction(self):
|
|
195
|
+
"""Fraction (by count) of component in mixture"""
|
|
196
|
+
return self.__fr
|
|
197
|
+
@property
|
|
198
|
+
def data(self):
|
|
199
|
+
"""AtomData of component"""
|
|
200
|
+
return self.__ad
|
|
201
|
+
def __str__(self):
|
|
202
|
+
return '%g*AtomData(%s)'%(self.__fr,self.__ad.description(True))
|
|
203
|
+
def __repr__(self):
|
|
204
|
+
return self.__str__()
|
|
205
|
+
|
|
206
|
+
def nComponents(self):
|
|
207
|
+
"""Number of sub-components in a mixture"""
|
|
208
|
+
return self.__ncomp
|
|
209
|
+
def getComponent(self,icomponent):
|
|
210
|
+
"""Get component in a mixture"""
|
|
211
|
+
c=self.__comp[icomponent]
|
|
212
|
+
if c:
|
|
213
|
+
return c
|
|
214
|
+
rawobj_subc,fraction=_rawfct['ncrystal_atomdata_createsubcomp'](self._rawobj,icomponent)
|
|
215
|
+
ad = AtomData(rawobj_subc)
|
|
216
|
+
c = AtomData.Component(fraction,ad)
|
|
217
|
+
self.__comp[icomponent] = c
|
|
218
|
+
return c
|
|
219
|
+
def getAllComponents(self):
|
|
220
|
+
"""Get list of all components"""
|
|
221
|
+
if self.__compalldone:
|
|
222
|
+
return self.__comp
|
|
223
|
+
for i,c in enumerate(self.__comp):
|
|
224
|
+
if not c:
|
|
225
|
+
self.getComponent(i)
|
|
226
|
+
self.__compalldone=True
|
|
227
|
+
return self.__comp
|
|
228
|
+
components = property(getAllComponents)
|
|
229
|
+
|
|
230
|
+
def displayLabel(self):
|
|
231
|
+
"""Short label which unique identifies an atom role within a particular material."""
|
|
232
|
+
return self.__dl
|
|
233
|
+
|
|
234
|
+
def isTopLevel(self):
|
|
235
|
+
"""Whether or not AtomData appears directly on an Info object. If not,
|
|
236
|
+
it will most likely either be a component (direct or indirect) of a top
|
|
237
|
+
level AtomData object, or be taken from the composition list of a
|
|
238
|
+
multi-phase object.
|
|
239
|
+
"""
|
|
240
|
+
return bool(self.__dl)
|
|
241
|
+
|
|
242
|
+
def description(self,includeValues=True):
|
|
243
|
+
"""Returns description of material as a string, with or without values."""
|
|
244
|
+
if includeValues:
|
|
245
|
+
zstr=' Z=%i'%self.__z if self.__z else ''
|
|
246
|
+
astr=' A=%i'%self.__a if self.__a else ''
|
|
247
|
+
_=(self.__descr,self.__cohsl_fm,self.coherentXS(),self.__incxs,
|
|
248
|
+
self.__absxs,self.__m,zstr,astr)
|
|
249
|
+
return'%s(cohSL=%gfm cohXS=%gbarn incXS=%gbarn absXS=%gbarn mass=%gamu%s%s)'%_
|
|
250
|
+
return self.__descr
|
|
251
|
+
|
|
252
|
+
def __str__(self):
|
|
253
|
+
descr=self.description()
|
|
254
|
+
return '%s=%s'%(self.__dl,descr) if self.__dl else descr
|
|
255
|
+
|
|
256
|
+
def __repr__(self):
|
|
257
|
+
return self.__str__()
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def fmt_atomdb_str( mass, coh_scat_len, incoh_xs, abs_xs, sep=' ' ):
|
|
261
|
+
"""Takes mass value (amu), coherent scattering length (fm), incoherent cross
|
|
262
|
+
section (barn), and absorption cross section @v_n=2200m/s (barn) and return
|
|
263
|
+
data formatted in a string in a format like suitable for the the @ATOMDB
|
|
264
|
+
section of ncmat files, or the atomdb cfg-string parameter. For instance,
|
|
265
|
+
for Si such a data string might look like "28.09u 4.1491fm 0.004b 0.171b" By
|
|
266
|
+
default the four fields are separated by spaces, but the sep parameter can
|
|
267
|
+
be used to change this.
|
|
268
|
+
"""
|
|
269
|
+
assert 0.0 < mass < 1e99
|
|
270
|
+
assert -1e99 < coh_scat_len < 1e99
|
|
271
|
+
assert 0.0 <= incoh_xs < 1e99
|
|
272
|
+
assert 0.0 <= abs_xs < 1e99
|
|
273
|
+
return f'{mass}u{sep}{coh_scat_len or 0:g}fm{sep}{incoh_xs or 0:g}b{sep}{abs_xs or 0:g}b'
|
|
274
|
+
|
|
275
|
+
def to_atomdb_str( self, sep=' ' ):
|
|
276
|
+
"""Return data formatted in a string in a format like suitable for the
|
|
277
|
+
the @ATOMDB section of ncmat files, or the atomdb cfg-string parameter
|
|
278
|
+
(see the static method "fmt_atomdb_str" for details).
|
|
279
|
+
"""
|
|
280
|
+
return AtomData.fmt_atomdb_str( mass=self.__m,
|
|
281
|
+
coh_scat_len = self.__cohsl_fm,
|
|
282
|
+
incoh_xs = self.__incxs,
|
|
283
|
+
abs_xs = self.__absxs,
|
|
284
|
+
sep = sep )
|
|
285
|
+
|
|
286
|
+
class StateOfMatter(_enum.Enum):
|
|
287
|
+
"""State of matter. Note that Solid's might be either amorphous or crystalline."""
|
|
288
|
+
#NB: List here must be synchronized with list and values in NCInfo.hh:
|
|
289
|
+
Unknown = 0
|
|
290
|
+
Solid = 1
|
|
291
|
+
Gas = 2
|
|
292
|
+
Liquid = 3
|
|
293
|
+
|
|
294
|
+
class HKLInfoType(_enum.Enum):
|
|
295
|
+
"""Describes the kind of information about plane normals and Miller (hkl)
|
|
296
|
+
indices available on each entry in the HKLList."""
|
|
297
|
+
#NB: List here must be synchronized with list and values in NCInfoTypes.hh:
|
|
298
|
+
SymEqvGroup = 0
|
|
299
|
+
ExplicitHKLs = 1
|
|
300
|
+
ExplicitNormals = 2
|
|
301
|
+
Minimal = 3
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class Info(RCBase):
|
|
305
|
+
"""Class representing information about a given material.
|
|
306
|
+
|
|
307
|
+
Objects might represent either multi- or single phase
|
|
308
|
+
materials. Multi-phase objects contain a list of phases (which might
|
|
309
|
+
themselves be either single or multi-phase objects). Most other fields
|
|
310
|
+
(structure, hkl lists, dynamics, etc.) are single-phase specific and will
|
|
311
|
+
be unavailable on multiphase-phase objects. Exceptions are phase-structure
|
|
312
|
+
information (todo) which is only available on multi-phase objects, and
|
|
313
|
+
fields which are available for both multi- and single-phase objects such as
|
|
314
|
+
density, composition, temperature, and state of matter (where such are well
|
|
315
|
+
defined).
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
def __init__(self, cfgstr):
|
|
319
|
+
"""create Info object based on cfg-string (same as using createInfo(cfgstr))"""
|
|
320
|
+
if isinstance(cfgstr,tuple) and len(cfgstr)==2 and cfgstr[0]=='_rawobj_':
|
|
321
|
+
#Already got an ncrystal_info_t object:
|
|
322
|
+
rawobj = cfgstr[1]
|
|
323
|
+
else:
|
|
324
|
+
rawobj = _rawfct['ncrystal_create_info'](_str2cstr(cfgstr))
|
|
325
|
+
super(Info, self).__init__(rawobj)
|
|
326
|
+
self.__dyninfo=None
|
|
327
|
+
self.__atominfo=None
|
|
328
|
+
self.__custom=None
|
|
329
|
+
self.__atomdatas=[]
|
|
330
|
+
self.__comp=None
|
|
331
|
+
self._som=None
|
|
332
|
+
self._nphases = int(_rawfct['ncrystal_info_nphases'](rawobj))
|
|
333
|
+
assert self._nphases == 0 or self._nphases >= 2
|
|
334
|
+
self._phases = tuple() if self._nphases == 0 else None
|
|
335
|
+
|
|
336
|
+
def getUniqueID(self):
|
|
337
|
+
"""Unique identifier of object (UID)."""
|
|
338
|
+
return _rawfct['infouid'](self._rawobj)
|
|
339
|
+
uid = property(getUniqueID)
|
|
340
|
+
|
|
341
|
+
def _getUnderlyingUniqueID(self):
|
|
342
|
+
"""Unique identifier of underlying object, which does not change on simple
|
|
343
|
+
density or cfg-data overrides (expert usage only!)."""
|
|
344
|
+
return _rawfct['infouid_underlying'](self._rawobj)
|
|
345
|
+
|
|
346
|
+
def isSinglePhase(self):
|
|
347
|
+
"""Single phase object."""
|
|
348
|
+
return self._nphases == 0
|
|
349
|
+
|
|
350
|
+
def isMultiPhase(self):
|
|
351
|
+
"""Multi phase object."""
|
|
352
|
+
return self._nphases != 0
|
|
353
|
+
|
|
354
|
+
def __initPhases(self):
|
|
355
|
+
assert self._phases is None and self._nphases > 1
|
|
356
|
+
ll=[]
|
|
357
|
+
for i in range(self._nphases):
|
|
358
|
+
fraction = _ctypes.c_double()
|
|
359
|
+
ph_info_raw = _rawfct['ncrystal_info_getphase'](self._rawobj,i,fraction)
|
|
360
|
+
ph_info = Info( ('_rawobj_',ph_info_raw) )
|
|
361
|
+
ll.append( ( float(fraction.value), ph_info ) )
|
|
362
|
+
self._phases = tuple(ll)
|
|
363
|
+
return self._phases
|
|
364
|
+
|
|
365
|
+
def getPhases(self):
|
|
366
|
+
"""Daughter phases in a multi-phase object. Returns a list of fractions and Info
|
|
367
|
+
objects of the daughter phases, in the format [(fraction_i,daughter_i),...]
|
|
368
|
+
"""
|
|
369
|
+
return self.__initPhases() if self._phases is None else self._phases
|
|
370
|
+
phases=property(getPhases)
|
|
371
|
+
|
|
372
|
+
def _initComp(self):
|
|
373
|
+
assert self.__comp is None
|
|
374
|
+
nc = _rawfct['ncrystal_info_ncomponents'](self._rawobj)
|
|
375
|
+
ll = []
|
|
376
|
+
for icomp in range(nc):
|
|
377
|
+
atomidx,fraction = _rawfct['ncrystal_info_getcomp'](self._rawobj,icomp)
|
|
378
|
+
#NB: atomidx will be invalid in case of multiphase objects!
|
|
379
|
+
if atomidx < 65535:
|
|
380
|
+
#Most likely a single-phase object with valid atomidx, we can
|
|
381
|
+
#use self._provideAtomData and share the AtomData objects also here on the python side:
|
|
382
|
+
ll += [(fraction,self._provideAtomData(atomidx))]
|
|
383
|
+
else:
|
|
384
|
+
#Most likely a multi-phase object with invalid atomidx, we must
|
|
385
|
+
#create new AtomData objects, based on ncrystal_create_component_atomdata:
|
|
386
|
+
raw_ad = _rawfct['ncrystal_create_component_atomdata'](self._rawobj,icomp)
|
|
387
|
+
obj = AtomData(raw_ad)
|
|
388
|
+
assert not obj.isTopLevel()#does not appear directly on Info object
|
|
389
|
+
ll += [(fraction,obj)]
|
|
390
|
+
self.__comp = ll
|
|
391
|
+
return self.__comp
|
|
392
|
+
|
|
393
|
+
def stateOfMatter(self):
|
|
394
|
+
"""State of matter, i.e. Solid, Liquid, Gas, ... as per the options in the
|
|
395
|
+
StateOfMatter class. Note that the .isCrystalline() method can be used
|
|
396
|
+
to additionally distinguish between amorphous and crystalline
|
|
397
|
+
solids. Return value is an enum object, whose .name() method can be used
|
|
398
|
+
in case a string value is desired.
|
|
399
|
+
"""
|
|
400
|
+
if self._som is None:
|
|
401
|
+
self._som = StateOfMatter(_rawfct['ncrystal_info_getstateofmatter'](self._rawobj))
|
|
402
|
+
return self._som
|
|
403
|
+
|
|
404
|
+
def isCrystalline(self):
|
|
405
|
+
"""Whether or not object is crystalline (i.e. has unit cell structure or list of
|
|
406
|
+
reflection planes)."""
|
|
407
|
+
return self.hasStructureInfo() or self.hasAtomInfo() or self.hasHKLInfo()
|
|
408
|
+
|
|
409
|
+
def hasComposition(self):
|
|
410
|
+
"""OBSOLETE FUNCTION (always available now)."""
|
|
411
|
+
from ._common import warn
|
|
412
|
+
warn('The .hasComposition method is obsolete'
|
|
413
|
+
' (it always returns True now).')
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
def getComposition(self):
|
|
417
|
+
"""Get basic composition as list of (fraction,AtomData). For a single-phase
|
|
418
|
+
object, the list is always consistent with AtomInfo/DynInfo (if
|
|
419
|
+
present).
|
|
420
|
+
"""
|
|
421
|
+
return self._initComp() if self.__comp is None else self.__comp
|
|
422
|
+
composition=property(getComposition)
|
|
423
|
+
|
|
424
|
+
def getFlattenedComposition( self,
|
|
425
|
+
preferNaturalElements = True,
|
|
426
|
+
naturalAbundProvider = None,
|
|
427
|
+
asJSONStr=False ):
|
|
428
|
+
"""Break down the basic composition of the material into elements and
|
|
429
|
+
isotopes. If an element only occurs as a natural element and has no
|
|
430
|
+
specific isotopes, that element will be returned as a natural isotope
|
|
431
|
+
unless preferNaturalElements=False. Generally, it is best if a
|
|
432
|
+
naturalAbundProvider is given, since if a given Z value has a mix of
|
|
433
|
+
isotopes and the natural elements, the natural element must always be
|
|
434
|
+
broken up.
|
|
435
|
+
|
|
436
|
+
If a naturalAbundProvider is given, it must be a function taking a Z
|
|
437
|
+
value and returning the breakdown into isotopes,
|
|
438
|
+
[(A1,frac1),...,(An,fracn)]. It can return None or an empty list to
|
|
439
|
+
indicate missing information.
|
|
440
|
+
|
|
441
|
+
Returns a list of (Z,<breakdown>) tuples, where <breakdown> is again a
|
|
442
|
+
list of tuples of A-values and associated abundances (A=0 indicates
|
|
443
|
+
natural elements). If asJSONStr=true, the data structure will be
|
|
444
|
+
returned as a JSON-encoded string, instead of a Python dictionary.
|
|
445
|
+
"""
|
|
446
|
+
_js = _rawfct['nc_info_getflatcompos'](self._rawobj,naturalAbundProvider,preferNaturalElements)
|
|
447
|
+
import json
|
|
448
|
+
return _js if asJSONStr else json.loads(_js)
|
|
449
|
+
|
|
450
|
+
def dump(self,verbose=0):
|
|
451
|
+
"""Dump contained information to standard output. Use verbose argument to set
|
|
452
|
+
verbosity level to 0 (minimal), 1 (middle), 2 (most verbose)."""
|
|
453
|
+
_flush()
|
|
454
|
+
_rawfct['ncrystal_dump_verbose'](self._rawobj,min(999,max(0,int(verbose))))
|
|
455
|
+
_flush()
|
|
456
|
+
|
|
457
|
+
def dump_str(self, verbose=0):
|
|
458
|
+
"""Return contained information as multi-line string. Use verbose argument to set
|
|
459
|
+
verbosity level to 0 (minimal), 1 (middle), 2 (most verbose)."""
|
|
460
|
+
return _rawfct['nc_dump_tostr'](self._rawobj,min(999,max(0,int(verbose))))
|
|
461
|
+
|
|
462
|
+
def hasTemperature(self):
|
|
463
|
+
"""Whether or not material has a temperature available"""
|
|
464
|
+
return _rawfct['ncrystal_info_gettemperature'](self._rawobj)>-1
|
|
465
|
+
|
|
466
|
+
def getTemperature(self):
|
|
467
|
+
"""Material temperature (in kelvin)"""
|
|
468
|
+
t=_rawfct['ncrystal_info_gettemperature'](self._rawobj)
|
|
469
|
+
nc_assert(t>-1)
|
|
470
|
+
return t
|
|
471
|
+
|
|
472
|
+
def hasGlobalDebyeTemperature(self):
|
|
473
|
+
"""OBSOLETE FUNCTION: The concept of global versus per-element Debye
|
|
474
|
+
temperatures has been removed. Please iterate over AtomInfo objects
|
|
475
|
+
instead (see getAtomInfo() function) and get the Debye Temperature
|
|
476
|
+
from those. This function will be removed in a future release.
|
|
477
|
+
"""
|
|
478
|
+
from ._common import warn
|
|
479
|
+
warn('The .hasGlobalDebyeTemperature method is obsolete'
|
|
480
|
+
' (it always returns False now).')
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
def getGlobalDebyeTemperature(self):
|
|
484
|
+
"""OBSOLETE FUNCTION: The concept of global versus per-element Debye
|
|
485
|
+
temperatures has been removed. Please iterate over AtomInfo objects
|
|
486
|
+
instead (see getAtomInfo() function) and get the Debye Temperature
|
|
487
|
+
from those. Calling this function will always result in an exception
|
|
488
|
+
thrown for now, and the function will be removed in a future release..
|
|
489
|
+
"""
|
|
490
|
+
raise NCLogicError('The concept of global Debye temperatures has been removed. Iterate over'
|
|
491
|
+
+' AtomInfo objects instead and get the Debye temperature values from those.')
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
def hasAtomDebyeTemp(self):
|
|
495
|
+
"""Whether AtomInfo objects are present and have Debye temperatures available
|
|
496
|
+
(they will either all have them available, or none of them will have
|
|
497
|
+
them available).
|
|
498
|
+
"""
|
|
499
|
+
if self.__atominfo is None:
|
|
500
|
+
self.__initAtomInfo()
|
|
501
|
+
return self.__atominfo[3]
|
|
502
|
+
|
|
503
|
+
def hasDebyeTemperature(self):
|
|
504
|
+
"""Alias for hasAtomDebyeTemp()."""
|
|
505
|
+
return self.hasAtomDebyeTemp()
|
|
506
|
+
|
|
507
|
+
def hasAnyDebyeTemperature(self):
|
|
508
|
+
"""OBSOLETE FUNCTION which will be removed in a future release. Please
|
|
509
|
+
call hasDebyeTemperature() instead.
|
|
510
|
+
"""
|
|
511
|
+
from ._common import warn
|
|
512
|
+
warn('The .hasAnyDebyeTemperature() method is deprecated.'
|
|
513
|
+
' Please use .hasAtomDebyeTemp() instead')
|
|
514
|
+
return self.hasAtomDebyeTemp()
|
|
515
|
+
|
|
516
|
+
def getDebyeTemperatureByElement(self,atomdata):
|
|
517
|
+
"""OBSOLETE FUNCTION which will be removed in a future release. Please access
|
|
518
|
+
the AtomInfo objects instead and query the Debye temperature there.
|
|
519
|
+
"""
|
|
520
|
+
from ._common import warn
|
|
521
|
+
warn('The getDebyeTemperatureByElement method is deprecated.'
|
|
522
|
+
' Please access the AtomInfo objects instead and query'
|
|
523
|
+
' the Debye temperature there.')
|
|
524
|
+
if atomdata.isTopLevel():
|
|
525
|
+
for ai in self.atominfos:
|
|
526
|
+
if atomdata is ai.atomData:
|
|
527
|
+
return ai.debyeTemperature
|
|
528
|
+
raise NCBadInput('Invalid atomdata object passed to Info.getDebyeTemperatureByElement'
|
|
529
|
+
+' (must be top-level AtomData from the same Info object)')
|
|
530
|
+
|
|
531
|
+
def hasDensity(self):
|
|
532
|
+
"""OBSOLETE FUNCTION (densities are now always available)."""
|
|
533
|
+
from ._common import warn
|
|
534
|
+
warn('The hasDensity() method is deprecated'
|
|
535
|
+
' (it always returns True now).')
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
def getDensity(self):
|
|
539
|
+
"""Get density in g/cm^3. See also getNumberDensity()."""
|
|
540
|
+
t=_rawfct['ncrystal_info_getdensity'](self._rawobj)
|
|
541
|
+
nc_assert(t>0.0)
|
|
542
|
+
return t
|
|
543
|
+
density = property(getDensity)
|
|
544
|
+
|
|
545
|
+
def hasNumberDensity(self):
|
|
546
|
+
"""OBSOLETE FUNCTION (densities are now always available)."""
|
|
547
|
+
from ._common import warn
|
|
548
|
+
warn('The hasNumberDensity() method is deprecated'
|
|
549
|
+
' (it always returns True now).')
|
|
550
|
+
return True
|
|
551
|
+
|
|
552
|
+
def getNumberDensity(self):
|
|
553
|
+
"""Get number density in atoms/angstrom^3. See also getDensity()."""
|
|
554
|
+
t=_rawfct['ncrystal_info_getnumberdensity'](self._rawobj)
|
|
555
|
+
nc_assert(t>0.0)
|
|
556
|
+
return t
|
|
557
|
+
numberdensity = property(getNumberDensity)
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def factor_macroscopic_xs( self ):
|
|
561
|
+
"""Factor needed to convert cross sections from (barns/atom) to inverse
|
|
562
|
+
penetration depth (1/cm). This is actually just the numberdensity value,
|
|
563
|
+
since (barns/atom) * ( atoms/Aa^3 ) = 1e-28m^2/1e-30m^3 = 1/cm.
|
|
564
|
+
"""
|
|
565
|
+
return self.numberdensity
|
|
566
|
+
|
|
567
|
+
def hasXSectAbsorption(self):
|
|
568
|
+
"""OBSOLETE FUNCTION"""
|
|
569
|
+
from ._common import warn
|
|
570
|
+
warn('The hasXSectAbsorption() method is deprecated'
|
|
571
|
+
' (it always returns True now).')
|
|
572
|
+
return True
|
|
573
|
+
|
|
574
|
+
def getXSectAbsorption(self):
|
|
575
|
+
"""Absorption cross section in barn (at 2200m/s)"""
|
|
576
|
+
t=_rawfct['ncrystal_info_getxsectabsorption'](self._rawobj)
|
|
577
|
+
nc_assert(t>-1)
|
|
578
|
+
return t
|
|
579
|
+
|
|
580
|
+
def hasXSectFree(self):
|
|
581
|
+
"""OBSOLETE FUNCTION"""
|
|
582
|
+
from ._common import warn
|
|
583
|
+
warn('The hasXSectFree() method is deprecated'
|
|
584
|
+
' (it always returns True now).')
|
|
585
|
+
return True
|
|
586
|
+
|
|
587
|
+
def getXSectFree(self):
|
|
588
|
+
"""Saturated (free) scattering cross section in barn in the high-E limit"""
|
|
589
|
+
t=_rawfct['ncrystal_info_getxsectfree'](self._rawobj)
|
|
590
|
+
nc_assert(t>-1)
|
|
591
|
+
return t
|
|
592
|
+
|
|
593
|
+
def getSLD(self):
|
|
594
|
+
"""Get scattering length density in 1e-6/Aa^2"""
|
|
595
|
+
return _rawfct['ncrystal_info_getsld'](self._rawobj)
|
|
596
|
+
sld = property(getSLD)
|
|
597
|
+
|
|
598
|
+
def hasStructureInfo(self):
|
|
599
|
+
"""Whether or not material has crystal structure information available."""
|
|
600
|
+
return bool(_rawfct['ncrystal_info_getstructure'](self._rawobj))
|
|
601
|
+
def getStructureInfo(self):
|
|
602
|
+
"""Information about crystal structure."""
|
|
603
|
+
d=_rawfct['ncrystal_info_getstructure'](self._rawobj)
|
|
604
|
+
nc_assert(d)
|
|
605
|
+
return d
|
|
606
|
+
structure_info = property(getStructureInfo)
|
|
607
|
+
|
|
608
|
+
def _provideAtomData(self,atomindex):
|
|
609
|
+
if atomindex >= len(self.__atomdatas):
|
|
610
|
+
if atomindex >= 65535:
|
|
611
|
+
raise NCLogicError(f'Invalid atomindex ({atomindex}) provided to Info._provideAtomData')
|
|
612
|
+
self.__atomdatas.extend([None,]*(atomindex+1-len(self.__atomdatas)))
|
|
613
|
+
obj = self.__atomdatas[atomindex]
|
|
614
|
+
if obj:
|
|
615
|
+
return obj
|
|
616
|
+
raw_ad = _rawfct['ncrystal_create_atomdata'](self._rawobj,atomindex)
|
|
617
|
+
obj = AtomData(raw_ad)
|
|
618
|
+
assert obj.isTopLevel()
|
|
619
|
+
self.__atomdatas[atomindex] = obj
|
|
620
|
+
return obj
|
|
621
|
+
|
|
622
|
+
class AtomInfo:
|
|
623
|
+
"""Class with information about a particular atom in a unit cell, including the
|
|
624
|
+
composition of atoms, positions, Debye temperature, and mean-squared-displacements.
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
def __init__(self,theinfoobj_wr,atomidx,n,dt,msd,pos):
|
|
628
|
+
"""For internal usage only."""
|
|
629
|
+
assert dt is None or ( isinstance(dt,float) and dt > 0.0 )
|
|
630
|
+
assert msd is None or ( isinstance(msd,float) and msd > 0.0 )
|
|
631
|
+
self._info_wr = theinfoobj_wr
|
|
632
|
+
self._atomidx,self.__n,self.__dt,self.__msd,=atomidx,n,dt,msd
|
|
633
|
+
self.__pos = tuple(pos)#tuple, since it is immutable
|
|
634
|
+
self.__atomdata = None
|
|
635
|
+
self.__correspDI_wp = None
|
|
636
|
+
|
|
637
|
+
def correspondingDynamicInfo(self):
|
|
638
|
+
"""
|
|
639
|
+
Get corresponding DynamicInfo object from the same Info
|
|
640
|
+
object. Returns None if Info object does not have dynamic info
|
|
641
|
+
available
|
|
642
|
+
"""
|
|
643
|
+
if self.__correspDI_wp is not None:
|
|
644
|
+
if self.__correspDI_wp is False:
|
|
645
|
+
return None
|
|
646
|
+
di = self.__correspDI_wp()
|
|
647
|
+
nc_assert(di is not None,"AtomInfo.correspondingDynamicInfo can not"
|
|
648
|
+
+" be used after associated Info object is deleted")
|
|
649
|
+
return di
|
|
650
|
+
_info = self._info_wr()
|
|
651
|
+
nc_assert(_info is not None,"AtomInfo.correspondingDynamicInfo can not"
|
|
652
|
+
+" be used after associated Info object is deleted")
|
|
653
|
+
if not _info.hasDynamicInfo():
|
|
654
|
+
self.__correspDI_wp = False
|
|
655
|
+
return None
|
|
656
|
+
for di in _info.dyninfos:
|
|
657
|
+
if di._atomidx == self._atomidx:
|
|
658
|
+
self.__correspDI_wp = _weakref.ref(di)
|
|
659
|
+
return di
|
|
660
|
+
nc_assert(False,"AtomInfo.correspondingDynamicInfo: inconsistent internal state (bug?)")
|
|
661
|
+
dyninfo = property(correspondingDynamicInfo)
|
|
662
|
+
|
|
663
|
+
@property
|
|
664
|
+
def atomData(self):
|
|
665
|
+
"""Return AtomData object with details about composition and relevant physics constants"""
|
|
666
|
+
if self.__atomdata is None:
|
|
667
|
+
_info = self._info_wr()
|
|
668
|
+
nc_assert(_info is not None,"AtomInfo.atomData can not be used after associated Info object is deleted")
|
|
669
|
+
self.__atomdata = _info._provideAtomData(self._atomidx)
|
|
670
|
+
assert self.__atomdata.isTopLevel()
|
|
671
|
+
return self.__atomdata
|
|
672
|
+
|
|
673
|
+
@property
|
|
674
|
+
def count(self):
|
|
675
|
+
"""Number of atoms of this type per unit cell"""
|
|
676
|
+
return self.__n
|
|
677
|
+
|
|
678
|
+
@property
|
|
679
|
+
def debyeTemperature(self):
|
|
680
|
+
"""The Debye Temperature of the atom (kelvin). Returns None if not available."""
|
|
681
|
+
return self.__dt
|
|
682
|
+
|
|
683
|
+
@property
|
|
684
|
+
def meanSquaredDisplacement(self):
|
|
685
|
+
"""The mean-squared-displacement of the atom (angstrom^2), a.k.a. "U_iso". Returns None if not
|
|
686
|
+
available.
|
|
687
|
+
"""
|
|
688
|
+
return self.__msd
|
|
689
|
+
msd=meanSquaredDisplacement#alias
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def positions(self):
|
|
693
|
+
"""List (tuple actually) of positions of this atom in the unit cell. Each
|
|
694
|
+
entry is given as a tuple of three values, (x,y,z)"""
|
|
695
|
+
return self.__pos
|
|
696
|
+
|
|
697
|
+
@property
|
|
698
|
+
def atomIndex(self):
|
|
699
|
+
"""Index of atom on this material"""
|
|
700
|
+
return self._atomidx
|
|
701
|
+
|
|
702
|
+
def __str__(self):
|
|
703
|
+
ll=[str(self.atomData.displayLabel()),str(self.__n)]
|
|
704
|
+
if self.__dt>0.0:
|
|
705
|
+
ll.append('DebyeT=%gK'%self.__dt if self.__dt else 'DebyeT=n/a')
|
|
706
|
+
if self.__msd>0.0:
|
|
707
|
+
ll.append('MSD=%gAa^2'%self.__msd if self.__msd else 'MSD=n/a')
|
|
708
|
+
ll.append('hasPositions=%s'%('yes' if self.__pos else 'no'))
|
|
709
|
+
return 'AtomInfo(%s)'%(', '.join(ll))
|
|
710
|
+
|
|
711
|
+
def hasAtomInfo(self):
|
|
712
|
+
"""Whether or no getAtomInfo()/atominfos are available"""
|
|
713
|
+
if self.__atominfo is None:
|
|
714
|
+
self.__initAtomInfo()
|
|
715
|
+
return self.__atominfo[0]
|
|
716
|
+
|
|
717
|
+
def hasAtomMSD(self):
|
|
718
|
+
"""Whether AtomInfo objects have mean-square-displacements (a.k.a. "U_iso") available"""
|
|
719
|
+
if self.__atominfo is None:
|
|
720
|
+
self.__initAtomInfo()
|
|
721
|
+
return self.__atominfo[1]
|
|
722
|
+
|
|
723
|
+
def hasAtomPositions(self):
|
|
724
|
+
"""OBSOLETE FUNCTION: AtomInfo objects now always have positions
|
|
725
|
+
available. Returns same as hasAtomInfo(). Will be removed in a future
|
|
726
|
+
release.
|
|
727
|
+
"""
|
|
728
|
+
from ._common import warn
|
|
729
|
+
warn('The hasAtomPositions() method is deprecated'
|
|
730
|
+
' (it always returns the same as .hasAtomInfo() now).')
|
|
731
|
+
return self.hasAtomInfo()
|
|
732
|
+
|
|
733
|
+
def hasPerElementDebyeTemperature(self):
|
|
734
|
+
"""OBSOLETE FUNCTION which will be removed in a future
|
|
735
|
+
release. Please use hasAtomDebyeTemp() instead.
|
|
736
|
+
"""
|
|
737
|
+
from ._common import warn
|
|
738
|
+
warn('The hasPerElementDebyeTemperature() method is deprecated.'
|
|
739
|
+
' Please use the hasAtomDebyeTemp() method instead.')
|
|
740
|
+
return self.hasAtomDebyeTemp()
|
|
741
|
+
|
|
742
|
+
def getAtomInfo(self):
|
|
743
|
+
"""Get list of AtomInfo objects, one for each atom. Returns empty list if unavailable."""
|
|
744
|
+
if self.__atominfo is None:
|
|
745
|
+
self.__initAtomInfo()
|
|
746
|
+
return self.__atominfo[2]
|
|
747
|
+
atominfos = property(getAtomInfo)
|
|
748
|
+
|
|
749
|
+
def __initAtomInfo(self):
|
|
750
|
+
assert self.__atominfo is None
|
|
751
|
+
natoms = _rawfct['ncrystal_info_natominfo'](self._rawobj)
|
|
752
|
+
hasmsd = bool(_rawfct['ncrystal_info_hasatommsd'](self._rawobj))
|
|
753
|
+
hasperelemdt=False
|
|
754
|
+
ll=[]
|
|
755
|
+
self_weakref = _weakref.ref(self)
|
|
756
|
+
for iatom in range(natoms):
|
|
757
|
+
atomidx,n,dt,msd = _rawfct['ncrystal_info_getatominfo'](self._rawobj,iatom)
|
|
758
|
+
if dt:
|
|
759
|
+
hasperelemdt=True
|
|
760
|
+
assert hasmsd == (msd>0.0)
|
|
761
|
+
pos=[]
|
|
762
|
+
for ipos in range(n):
|
|
763
|
+
pos.append( _rawfct['ncrystal_info_getatompos'](self._rawobj,iatom,ipos) )
|
|
764
|
+
ll.append( Info.AtomInfo( self_weakref,atomidx, n,
|
|
765
|
+
( dt if ( dt and dt>0.0) else None),
|
|
766
|
+
(msd if (msd and msd>0.0) else None),
|
|
767
|
+
pos) )
|
|
768
|
+
self.__atominfo = ( natoms>0, hasmsd, ll, hasperelemdt )
|
|
769
|
+
|
|
770
|
+
def hasHKLInfo(self):
|
|
771
|
+
"""Whether or not material has lists of HKL-plane info available"""
|
|
772
|
+
return bool(_rawfct['ncrystal_info_nhkl'](self._rawobj)>-1)
|
|
773
|
+
|
|
774
|
+
def nHKL(self):
|
|
775
|
+
"""Number of HKL planes available (grouped into families with similar
|
|
776
|
+
d-spacing and f-squared)"""
|
|
777
|
+
return int(_rawfct['ncrystal_info_nhkl'](self._rawobj))
|
|
778
|
+
|
|
779
|
+
def hklDLower(self):
|
|
780
|
+
"""Lower d-spacing cutoff (angstrom)."""
|
|
781
|
+
return float(_rawfct['ncrystal_info_hkl_dlower'](self._rawobj))
|
|
782
|
+
|
|
783
|
+
def hklDUpper(self):
|
|
784
|
+
"""Upper d-spacing cutoff (angstrom)."""
|
|
785
|
+
return float(_rawfct['ncrystal_info_hkl_dupper'](self._rawobj))
|
|
786
|
+
|
|
787
|
+
def hklList(self,all_indices=False):
|
|
788
|
+
"""Iterator over HKL info, yielding tuples in the format
|
|
789
|
+
(h,k,l,multiplicity,dspacing,fsquared). Running with all_indices=True to
|
|
790
|
+
get the full list of hkl points in each group - in that case, h, k, and
|
|
791
|
+
l will be numpy arrays of length multiplicity/2 (including just one of
|
|
792
|
+
(h,k,l) and (-h,-k,-l) in the list).
|
|
793
|
+
"""
|
|
794
|
+
nc_assert(self.hasHKLInfo())
|
|
795
|
+
return _rawfct['iter_hkllist']( self._rawobj,
|
|
796
|
+
all_indices = all_indices )
|
|
797
|
+
|
|
798
|
+
def hklObjects( self ):
|
|
799
|
+
"""Iterator like .hklList, but with each entry returned as a single
|
|
800
|
+
object, with the information accessible as (hopefully) more userfriendly
|
|
801
|
+
friendly properties.
|
|
802
|
+
|
|
803
|
+
Example usage:
|
|
804
|
+
|
|
805
|
+
for e in info._hklObjects:
|
|
806
|
+
#help( e );break #<- uncomment for usage info
|
|
807
|
+
print( e )#<- a quick look
|
|
808
|
+
print( e.hkl_label, e.mult, e.d, e.f2 )
|
|
809
|
+
print( e.h, e.k, e.l )#all Miller indices as arrays.
|
|
810
|
+
|
|
811
|
+
"""
|
|
812
|
+
from ._hklobjects import _iter_hklobjects
|
|
813
|
+
for o in _iter_hklobjects(self):
|
|
814
|
+
yield o
|
|
815
|
+
|
|
816
|
+
def getBraggThreshold(self):
|
|
817
|
+
"""Get Bragg threshold in Aa (returns None if non-crystalline). This
|
|
818
|
+
method is meant as a fast way to access the Bragg threshold without
|
|
819
|
+
necessarily triggering a full initialisation of all HKL planes.
|
|
820
|
+
"""
|
|
821
|
+
bt = float(_rawfct['ncrystal_info_braggthreshold'](self._rawobj))
|
|
822
|
+
return bt if bt > 0.0 else None
|
|
823
|
+
braggthreshold = property(getBraggThreshold)
|
|
824
|
+
|
|
825
|
+
def hklInfoType(self):
|
|
826
|
+
"""What kind of information about plane normals and Miller indices are
|
|
827
|
+
available in the hklList(). It is guaranteed to be the same for all
|
|
828
|
+
HKLInfo entries, and will return "Minimal" when hklList() is present but
|
|
829
|
+
empty. Like getBraggThreshold(), calling this method will not
|
|
830
|
+
necessarily trigger a full initialisation of the hklList()."""
|
|
831
|
+
return HKLInfoType(int(_rawfct['ncrystal_info_hklinfotype'](self._rawobj)))
|
|
832
|
+
|
|
833
|
+
def hklIsSymEqvGroup(self):
|
|
834
|
+
"""Returns True if .hklInfoType() equals HKLInfoType.SymEqvGroup."""
|
|
835
|
+
return self.hklInfoType() == HKLInfoType.SymEqvGroup
|
|
836
|
+
|
|
837
|
+
def dspacingFromHKL(self, h, k, l): # noqa E741
|
|
838
|
+
"""Convenience method, calculating the d-spacing of a given Miller
|
|
839
|
+
index. Calling this incurs the overhead of creating a reciprocal lattice
|
|
840
|
+
matrix from the structure info."""
|
|
841
|
+
return float(_rawfct['ncrystal_info_dspacing_from_hkl'](self._rawobj,h,k,l))
|
|
842
|
+
|
|
843
|
+
class DynamicInfo:
|
|
844
|
+
"""Class representing dynamic information (related to inelastic scattering)
|
|
845
|
+
about a given atom"""
|
|
846
|
+
|
|
847
|
+
def __init__(self,theinfoobj_wr,fr,atomidx,tt):
|
|
848
|
+
"""internal usage only"""
|
|
849
|
+
self._info_wr,self.__atomdata = theinfoobj_wr, None
|
|
850
|
+
self.__fraction, self._atomidx, self.__tt = fr,atomidx,tt
|
|
851
|
+
self.__correspAtomInfo_wp = None
|
|
852
|
+
|
|
853
|
+
@property
|
|
854
|
+
def _key( self ):
|
|
855
|
+
i = self._info_wr()
|
|
856
|
+
if not i:
|
|
857
|
+
raise NCException('Dynamic info objects can not be used after the associated Info object is'
|
|
858
|
+
' deleted (the solution is normally to keep the Info object around'
|
|
859
|
+
' explicitly while you work on the dynamic info objects)')
|
|
860
|
+
return i, (i._rawobj,self._atomidx)
|
|
861
|
+
|
|
862
|
+
def correspondingAtomInfo(self):
|
|
863
|
+
"""Get corresponding AtomInfo object from the same Info object. Returns None if Info object does not have AtomInfo available"""
|
|
864
|
+
if self.__correspAtomInfo_wp is not None:
|
|
865
|
+
if self.__correspAtomInfo_wp is False:
|
|
866
|
+
return None
|
|
867
|
+
ai = self.__correspAtomInfo_wp()
|
|
868
|
+
nc_assert(ai is not None,"DynamicInfo.correspondingAtomInfo can not be used after associated Info object is deleted")
|
|
869
|
+
return ai
|
|
870
|
+
_info = self._info_wr()
|
|
871
|
+
nc_assert(_info is not None,"DynamicInfo.correspondingAtomInfo can not be used after associated Info object is deleted")
|
|
872
|
+
if not _info.hasAtomInfo():
|
|
873
|
+
self.__correspAtomInfo_wp = False
|
|
874
|
+
return None
|
|
875
|
+
for ai in _info.atominfos:
|
|
876
|
+
if ai._atomidx == self._atomidx:
|
|
877
|
+
self.__correspAtomInfo_wp = _weakref.ref(ai)
|
|
878
|
+
return ai
|
|
879
|
+
nc_assert(False,"DynamicInfo.correspondingAtomInfo: inconsistent internal state (bug?)")
|
|
880
|
+
atominfo = property(correspondingAtomInfo)
|
|
881
|
+
|
|
882
|
+
@property
|
|
883
|
+
def atomIndex(self):
|
|
884
|
+
"""Index of atom on this material"""
|
|
885
|
+
return self._atomidx
|
|
886
|
+
|
|
887
|
+
@property
|
|
888
|
+
def fraction(self):
|
|
889
|
+
"""Atom fraction in material (all fractions must add up to unity)"""
|
|
890
|
+
return self.__fraction
|
|
891
|
+
|
|
892
|
+
@property
|
|
893
|
+
def temperature(self):
|
|
894
|
+
"""Material temperature (same value as on associated Info object)"""
|
|
895
|
+
return self.__tt
|
|
896
|
+
|
|
897
|
+
@property
|
|
898
|
+
def atomData(self):
|
|
899
|
+
"""Return AtomData object with details about composition and relevant physics constants"""
|
|
900
|
+
if self.__atomdata is None:
|
|
901
|
+
_info = self._info_wr()
|
|
902
|
+
nc_assert(_info is not None,"DynamicInfo.atomData can not be used after associated Info object is deleted")
|
|
903
|
+
self.__atomdata = _info._provideAtomData(self._atomidx)
|
|
904
|
+
assert self.__atomdata.isTopLevel()
|
|
905
|
+
return self.__atomdata
|
|
906
|
+
|
|
907
|
+
def _np(self):
|
|
908
|
+
_ensure_numpy()
|
|
909
|
+
return _np
|
|
910
|
+
|
|
911
|
+
def _copy_cptr_2_nparray(self,cptr,n):
|
|
912
|
+
np = self._np()
|
|
913
|
+
return np.copy(np.ctypeslib.as_array(cptr, shape=(n,)))
|
|
914
|
+
|
|
915
|
+
def __str__(self):
|
|
916
|
+
n=self.__class__.__name__
|
|
917
|
+
if n.startswith('DI_'):
|
|
918
|
+
n=n[3:]
|
|
919
|
+
s=', %s'%self._extradescr() if hasattr(self,'_extradescr') else ''
|
|
920
|
+
return ('DynamicInfo(%s, fraction=%.4g%%, type=%s%s)'%(self.atomData.displayLabel(),
|
|
921
|
+
self.__fraction*100.0,
|
|
922
|
+
n,s))
|
|
923
|
+
def _plotlabel( self ):
|
|
924
|
+
return self.atomData.displayLabel() or self.atomData.description(False)
|
|
925
|
+
|
|
926
|
+
class DI_Sterile(DynamicInfo):
|
|
927
|
+
"""Class indicating atoms for which inelastic neutron scattering is absent
|
|
928
|
+
or disabled."""
|
|
929
|
+
pass
|
|
930
|
+
|
|
931
|
+
class DI_FreeGas(DynamicInfo):
|
|
932
|
+
"""Class indicating atoms for which inelastic neutron scattering should be
|
|
933
|
+
modelled as scattering on a free gas."""
|
|
934
|
+
pass
|
|
935
|
+
|
|
936
|
+
class DI_ScatKnl(DynamicInfo):
|
|
937
|
+
"""Base class indicating atoms for which inelastic neutron scattering will
|
|
938
|
+
be, directly or indirectly, described by a scattering kernel,
|
|
939
|
+
S(alpha,beta). This is an abstract class, and derived classes provide
|
|
940
|
+
actual access to the kernels.
|
|
941
|
+
"""
|
|
942
|
+
|
|
943
|
+
def __init__(self,theinfoobj_wr,fr,atomidx,tt):
|
|
944
|
+
"""internal usage only"""
|
|
945
|
+
super(Info.DI_ScatKnl, self).__init__(theinfoobj_wr,fr,atomidx,tt)
|
|
946
|
+
self.__lastknl,self.__lastvdoslux = None,None
|
|
947
|
+
|
|
948
|
+
def _loadKernel( self, vdoslux = 3 ):
|
|
949
|
+
import numbers
|
|
950
|
+
assert isinstance(vdoslux,numbers.Integral) and 0<=vdoslux<=5
|
|
951
|
+
vdoslux=int(vdoslux)
|
|
952
|
+
if self.__lastvdoslux != vdoslux:
|
|
953
|
+
_keepalive, key = self._key
|
|
954
|
+
sugEmax,ne,na,nb,eptr,aptr,bptr,sabptr = _rawfct['ncrystal_dyninfo_extract_scatknl'](key,vdoslux)
|
|
955
|
+
self.__lastvdoslux = vdoslux
|
|
956
|
+
res={}
|
|
957
|
+
assert ne>=0
|
|
958
|
+
res['suggestedEmax'] = float(sugEmax)
|
|
959
|
+
res['egrid'] = self._copy_cptr_2_nparray(eptr,ne) if ne > 0 else self._np().zeros(0)
|
|
960
|
+
assert na>1 and nb>1
|
|
961
|
+
res['alpha'] = self._copy_cptr_2_nparray(aptr,na)
|
|
962
|
+
res['beta'] = self._copy_cptr_2_nparray(bptr,nb)
|
|
963
|
+
res['sab'] = self._copy_cptr_2_nparray(sabptr,na*nb)
|
|
964
|
+
res['temperature'] = self.temperature
|
|
965
|
+
self.__lastknl = res
|
|
966
|
+
assert self.__lastknl is not None
|
|
967
|
+
return self.__lastknl
|
|
968
|
+
|
|
969
|
+
class DI_ScatKnlDirect(DI_ScatKnl):
|
|
970
|
+
"""Pre-calculated scattering kernel which at most needs a (hidden) conversion to
|
|
971
|
+
S(alpha,beta) format before it is available."""
|
|
972
|
+
|
|
973
|
+
def __init__(self,theinfoobj_wr,fr,atomidx,tt):
|
|
974
|
+
"""internal usage only"""
|
|
975
|
+
super(Info.DI_ScatKnlDirect, self).__init__(theinfoobj_wr,fr,atomidx,tt)
|
|
976
|
+
|
|
977
|
+
def loadKernel( self ):
|
|
978
|
+
"""Prepares and returns the scattering kernel in S(alpha,beta) format.
|
|
979
|
+
|
|
980
|
+
Note that the sab array is ordered so that
|
|
981
|
+
S(alpha[i],beta[j])=sab[j*len(alpha)+i].
|
|
982
|
+
"""
|
|
983
|
+
return self._loadKernel(vdoslux=3)#vdoslux value not actually used
|
|
984
|
+
|
|
985
|
+
def plot_knl( self, **kwargs ):
|
|
986
|
+
"""Plot the scattering kernel using the NCrystal.plot.plot_knl
|
|
987
|
+
function. Any kwargs are simply passed along.
|
|
988
|
+
"""
|
|
989
|
+
from .plot import plot_knl
|
|
990
|
+
plot_knl( self.loadKernel(), **kwargs )
|
|
991
|
+
|
|
992
|
+
class DI_VDOS(DI_ScatKnl):
|
|
993
|
+
"""Solid state material with a phonon spectrum in the form of a Vibrational
|
|
994
|
+
Density Of State (VDOS) parameterisation. This can be expanded into a
|
|
995
|
+
full scattering kernel. How luxurious this expansion will be is
|
|
996
|
+
controlled by an optional vdoslux parameter in the loadKernel call (must
|
|
997
|
+
be integer from 0 to 5)
|
|
998
|
+
"""
|
|
999
|
+
def __init__(self,theinfoobj_wr,fr,atomidx,tt):
|
|
1000
|
+
"""internal usage only"""
|
|
1001
|
+
super(Info.DI_VDOS, self).__init__(theinfoobj_wr,fr,atomidx,tt)
|
|
1002
|
+
self.__vdosdata = None
|
|
1003
|
+
self.__vdosegrid_expanded = None
|
|
1004
|
+
self.__vdosorig = None
|
|
1005
|
+
|
|
1006
|
+
def _extradescr(self):
|
|
1007
|
+
return 'npts=%i'%len(self.vdosOrigDensity())
|
|
1008
|
+
|
|
1009
|
+
def vdosData(self):
|
|
1010
|
+
"""Access the VDOS as ([egrid_min,egrid_max],vdos_density)"""
|
|
1011
|
+
if self.__vdosdata is None:
|
|
1012
|
+
_keepalive, key = self._key
|
|
1013
|
+
emin,emax,nd,dptr = _rawfct['ncrystal_dyninfo_extract_vdos'](key)
|
|
1014
|
+
vdos_egrid = (emin,emax)
|
|
1015
|
+
vdos_density = self._copy_cptr_2_nparray(dptr,nd)
|
|
1016
|
+
self.__vdosdata = (vdos_egrid,vdos_density)
|
|
1017
|
+
return self.__vdosdata
|
|
1018
|
+
|
|
1019
|
+
def __loadVDOSOrig(self):
|
|
1020
|
+
if self.__vdosorig is None:
|
|
1021
|
+
_keepalive, key = self._key
|
|
1022
|
+
neg,egptr,nds,dsptr = _rawfct['ncrystal_dyninfo_extract_vdos_input'](key)
|
|
1023
|
+
self.__vdosorig = ( self._copy_cptr_2_nparray(egptr,neg),
|
|
1024
|
+
self._copy_cptr_2_nparray(dsptr,nds) )
|
|
1025
|
+
return self.__vdosorig
|
|
1026
|
+
|
|
1027
|
+
def vdosOrigEgrid(self):
|
|
1028
|
+
"""Access the original un-regularised VDOS energy grid"""
|
|
1029
|
+
return self.__loadVDOSOrig()[0]
|
|
1030
|
+
|
|
1031
|
+
def vdosOrigDensity(self):
|
|
1032
|
+
"""Access the original un-regularised VDOS energy grid"""
|
|
1033
|
+
return self.__loadVDOSOrig()[1]
|
|
1034
|
+
|
|
1035
|
+
@property
|
|
1036
|
+
def vdos_egrid(self):
|
|
1037
|
+
"""Access the VDOS energy grid as [egrid_min,egrid_max]"""
|
|
1038
|
+
return self.vdosData()[0]
|
|
1039
|
+
|
|
1040
|
+
@property
|
|
1041
|
+
def vdos_egrid_expanded(self):
|
|
1042
|
+
"""Access the egrid expanded into all actual egrid points"""
|
|
1043
|
+
if self.__vdosegrid_expanded is None:
|
|
1044
|
+
_ = self.vdosData()
|
|
1045
|
+
self.__vdosegrid_expanded = _np_linspace(_[0][0],_[0][1],len(_[1]))
|
|
1046
|
+
return self.__vdosegrid_expanded
|
|
1047
|
+
|
|
1048
|
+
@property
|
|
1049
|
+
def vdos_density(self):
|
|
1050
|
+
"""Access the VDOS density array"""
|
|
1051
|
+
return self.vdosData()[1]
|
|
1052
|
+
|
|
1053
|
+
def loadKernel( self, vdoslux = 3 ):
|
|
1054
|
+
"""Converts VDOS to S(alpha,beta) kernel with a luxury level given
|
|
1055
|
+
by the vdoslux parameter.
|
|
1056
|
+
|
|
1057
|
+
Note that the sab array is ordered so that
|
|
1058
|
+
S(alpha[i],beta[j])=sab[j*len(alpha)+i].
|
|
1059
|
+
"""
|
|
1060
|
+
return self._loadKernel(vdoslux=vdoslux)
|
|
1061
|
+
|
|
1062
|
+
def analyseVDOS(self):
|
|
1063
|
+
"""Same as running the global analyseVDOS function on the contained VDOS."""
|
|
1064
|
+
from .vdos import analyseVDOS
|
|
1065
|
+
return analyseVDOS(*self.vdos_egrid,self.vdos_density,
|
|
1066
|
+
self.temperature,self.atomData.averageMassAMU())
|
|
1067
|
+
|
|
1068
|
+
plot_knl = _impl.divdos_methods._plot_knl()
|
|
1069
|
+
plot_vdos = _impl.divdos_methods._plot_vdos()
|
|
1070
|
+
plot_Gn = _impl.divdos_methods._plot_Gn()
|
|
1071
|
+
extract_Gn = _impl.divdos_methods._extract_Gn()
|
|
1072
|
+
extract_custom_knl = _impl.divdos_methods._extract_custom_knl()
|
|
1073
|
+
|
|
1074
|
+
class DI_VDOSDebye(DI_ScatKnl):
|
|
1075
|
+
"""Similarly to DI_VDOS, but instead of using a phonon VDOS spectrum provided
|
|
1076
|
+
externally, an idealised spectrum is used for lack of better
|
|
1077
|
+
options. This spectrum is based on the Debye Model, in which the
|
|
1078
|
+
spectrum rises quadratically with phonon energy below a cutoff value,
|
|
1079
|
+
kT, where T is the Debye temperature
|
|
1080
|
+
"""
|
|
1081
|
+
|
|
1082
|
+
def __init__(self,theinfoobj_wr,fr,atomidx,tt):
|
|
1083
|
+
"""internal usage only"""
|
|
1084
|
+
super(Info.DI_VDOSDebye, self).__init__(theinfoobj_wr,fr,atomidx,tt)
|
|
1085
|
+
self.__vdosdata = None
|
|
1086
|
+
self.__debyetemp = None
|
|
1087
|
+
self.__vdosegrid_expanded = None
|
|
1088
|
+
|
|
1089
|
+
def vdosData(self):
|
|
1090
|
+
"""Access the idealised VDOS as ([egrid_min,egrid_max],vdos_density)"""
|
|
1091
|
+
if self.__vdosdata is None:
|
|
1092
|
+
from .vdos import createVDOSDebye
|
|
1093
|
+
self.__vdosdata = createVDOSDebye(self.debyeTemperature())
|
|
1094
|
+
return self.__vdosdata
|
|
1095
|
+
|
|
1096
|
+
def debyeTemperature(self):
|
|
1097
|
+
"""The Debye temperature of the atom"""
|
|
1098
|
+
if self.__debyetemp is None:
|
|
1099
|
+
_keepalive, key = self._key
|
|
1100
|
+
self.__debyetemp = _rawfct['ncrystal_dyninfo_extract_vdosdebye'](key)
|
|
1101
|
+
return self.__debyetemp
|
|
1102
|
+
|
|
1103
|
+
def _extradescr(self):
|
|
1104
|
+
return 'TDebye=%gK'%self.debyeTemperature()
|
|
1105
|
+
|
|
1106
|
+
@property
|
|
1107
|
+
def vdos_egrid(self):
|
|
1108
|
+
"""Access the VDOS energy grid as [egrid_min,egrid_max]"""
|
|
1109
|
+
return self.vdosData()[0]
|
|
1110
|
+
|
|
1111
|
+
@property
|
|
1112
|
+
def vdos_egrid_expanded(self):
|
|
1113
|
+
"""Access the egrid expanded into all actual egrid points"""
|
|
1114
|
+
if self.__vdosegrid_expanded is None:
|
|
1115
|
+
_ = self.vdosData()
|
|
1116
|
+
self.__vdosegrid_expanded = _np_linspace(_[0][0],_[0][1],len(_[1]))
|
|
1117
|
+
return self.__vdosegrid_expanded
|
|
1118
|
+
|
|
1119
|
+
@property
|
|
1120
|
+
def vdos_density(self):
|
|
1121
|
+
"""Access the VDOS density array"""
|
|
1122
|
+
return self.vdosData()[1]
|
|
1123
|
+
|
|
1124
|
+
def loadKernel( self, vdoslux = 3 ):
|
|
1125
|
+
"""Converts VDOS to S(alpha,beta) kernel with a luxury level given by the
|
|
1126
|
+
vdoslux parameter, which is similar to the vdoslux parameter used
|
|
1127
|
+
in DI_VDOS. Notice that the vdoslux parameter specified here on
|
|
1128
|
+
DI_VDOSDebye will be reduced internally by 3 (but not less than
|
|
1129
|
+
0), since the Debye model is anyway only a crude approximation
|
|
1130
|
+
and it accordingly does not need the same level of precise
|
|
1131
|
+
treatment as a full externally specified VDOS.
|
|
1132
|
+
|
|
1133
|
+
Note that the sab array is ordered so that
|
|
1134
|
+
S(alpha[i],beta[j])=sab[j*len(alpha)+i].
|
|
1135
|
+
"""
|
|
1136
|
+
return self._loadKernel(vdoslux=vdoslux)
|
|
1137
|
+
|
|
1138
|
+
def analyseVDOS(self):
|
|
1139
|
+
"""Same as running the global analyseVDOS function on the contained
|
|
1140
|
+
VDOS. Note that numbers returned here will be fully based on the
|
|
1141
|
+
actual tabulated VDOS curve, and values such as Debye temperature
|
|
1142
|
+
can therefore deviate slightly from the original value returned
|
|
1143
|
+
by .debyeTemperature().
|
|
1144
|
+
"""
|
|
1145
|
+
from .vdos import analyseVDOS
|
|
1146
|
+
return analyseVDOS(*self.vdos_egrid,self.vdos_density,
|
|
1147
|
+
self.temperature,self.atomData.averageMassAMU())
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
plot_knl = _impl.divdos_methods._plot_knl()
|
|
1151
|
+
plot_vdos = _impl.divdos_methods._plot_vdos()
|
|
1152
|
+
plot_Gn = _impl.divdos_methods._plot_Gn()
|
|
1153
|
+
extract_Gn = _impl.divdos_methods._extract_Gn()
|
|
1154
|
+
extract_custom_knl = _impl.divdos_methods._extract_custom_knl()
|
|
1155
|
+
|
|
1156
|
+
def hasDynamicInfo(self):
|
|
1157
|
+
"""Whether or not dynamic information for each atom is present"""
|
|
1158
|
+
return int(_rawfct['ncrystal_info_ndyninfo'](self._rawobj))>0 if self.__dyninfo is None else bool(self.__dyninfo)
|
|
1159
|
+
|
|
1160
|
+
def getDynamicInfoList(self):
|
|
1161
|
+
"""Get list of DynamicInfo objects (if available). One for each atom."""
|
|
1162
|
+
if self.__dyninfo is None:
|
|
1163
|
+
ll = []
|
|
1164
|
+
self_weakref = _weakref.ref(self)
|
|
1165
|
+
for idx in range(int(_rawfct['ncrystal_info_ndyninfo'](self._rawobj))):
|
|
1166
|
+
fr,tt,atomidx,ditype = _rawfct['ncrystal_dyninfo_base']((self._rawobj,idx))
|
|
1167
|
+
args=(self_weakref,fr,atomidx,tt)
|
|
1168
|
+
if ditype==0:
|
|
1169
|
+
di = Info.DI_Sterile(*args)
|
|
1170
|
+
elif ditype==1:
|
|
1171
|
+
di = Info.DI_FreeGas(*args)
|
|
1172
|
+
elif ditype==2:
|
|
1173
|
+
di = Info.DI_ScatKnlDirect(*args)
|
|
1174
|
+
elif ditype==3:
|
|
1175
|
+
di = Info.DI_VDOS(*args)
|
|
1176
|
+
elif ditype==4:
|
|
1177
|
+
di = Info.DI_VDOSDebye(*args)
|
|
1178
|
+
else:
|
|
1179
|
+
raise NCLogicError('Unknown DynInfo type id (%i)'%ditype.value)
|
|
1180
|
+
ll.append( di )
|
|
1181
|
+
self.__dyninfo = ll
|
|
1182
|
+
return self.__dyninfo
|
|
1183
|
+
dyninfos = property(getDynamicInfoList)
|
|
1184
|
+
|
|
1185
|
+
def findDynInfo( self, display_label ):
|
|
1186
|
+
"""Look in the dyninfos list for an entry with the given
|
|
1187
|
+
display-label. Returns it if found, otherwise returns None."""
|
|
1188
|
+
for di in self.dyninfos:
|
|
1189
|
+
if di.atomData.displayLabel() == display_label:
|
|
1190
|
+
return di
|
|
1191
|
+
|
|
1192
|
+
def findAtomInfo( self, display_label ):
|
|
1193
|
+
"""Look in the atominfos list for an entry with the given
|
|
1194
|
+
display-label. Returns it if found, otherwise returns None."""
|
|
1195
|
+
for ai in self.atominfos:
|
|
1196
|
+
if ai.atomData.displayLabel() == display_label:
|
|
1197
|
+
return ai
|
|
1198
|
+
|
|
1199
|
+
def getAllCustomSections(self):
|
|
1200
|
+
"""Custom information for which the core NCrystal code does not have any
|
|
1201
|
+
specific treatment. This is usually intended for usage by developers adding new
|
|
1202
|
+
experimental physics models."""
|
|
1203
|
+
|
|
1204
|
+
if self.__custom is None:
|
|
1205
|
+
self.__custom = _rawfct['ncrystal_info_getcustomsections'](self._rawobj)
|
|
1206
|
+
return self.__custom
|
|
1207
|
+
customsections = property(getAllCustomSections)
|
|
1208
|
+
|
|
1209
|
+
class Process(RCBase):
|
|
1210
|
+
"""Base class for calculations of processes in materials.
|
|
1211
|
+
|
|
1212
|
+
Note that kinetic energies are in electronvolt and direction vectors are
|
|
1213
|
+
tuples of 3 numbers.
|
|
1214
|
+
|
|
1215
|
+
"""
|
|
1216
|
+
def getCalcName(self):
|
|
1217
|
+
"""Obsolete alias for getName"""
|
|
1218
|
+
from ._common import warn
|
|
1219
|
+
warn('The .getCalcName() method is deprecated.'
|
|
1220
|
+
' Please use the .getName() method or the .name property instead.')
|
|
1221
|
+
return self.getName()
|
|
1222
|
+
|
|
1223
|
+
def getName(self):
|
|
1224
|
+
"""Process name"""
|
|
1225
|
+
return _cstr2str(_rawfct['ncrystal_name'](self._rawobj))
|
|
1226
|
+
name = property(getName)
|
|
1227
|
+
|
|
1228
|
+
def getUniqueID(self):
|
|
1229
|
+
"""UID of underlying ProcImpl::Process object."""
|
|
1230
|
+
return _rawfct['procuid'](self._rawobj)
|
|
1231
|
+
uid = property(getUniqueID)
|
|
1232
|
+
|
|
1233
|
+
def domain(self):
|
|
1234
|
+
"""Domain where process has non-vanishing cross section.
|
|
1235
|
+
|
|
1236
|
+
Returns the domain as (ekin_low,ekin_high). Outside this range of
|
|
1237
|
+
neutron kinetic energy, the process can be assumed to have vanishing
|
|
1238
|
+
cross sections. Thus, processes present at all energies will return
|
|
1239
|
+
(0.0,infinity).
|
|
1240
|
+
|
|
1241
|
+
"""
|
|
1242
|
+
return _rawfct['ncrystal_domain'](self._rawobj)
|
|
1243
|
+
|
|
1244
|
+
def isNull(self):
|
|
1245
|
+
"""Domain might indicate that this is a null-process, vanishing everywhere."""
|
|
1246
|
+
elow,ehigh = self.domain()
|
|
1247
|
+
#checking for inf like the following to avoid depending on numpy or math
|
|
1248
|
+
#modules just for this:
|
|
1249
|
+
return ( elow >= ehigh or ( elow>1e99 and elow==float('inf') ) )
|
|
1250
|
+
|
|
1251
|
+
def isNonOriented(self):
|
|
1252
|
+
"""opposite of isOriented()"""
|
|
1253
|
+
return bool(_rawfct['ncrystal_isnonoriented'](self._rawobj))
|
|
1254
|
+
def isOriented(self):
|
|
1255
|
+
"""Check if process is oriented and results depend on the incident direction of the neutron"""
|
|
1256
|
+
return not self.isNonOriented()
|
|
1257
|
+
def crossSection( self, ekin, direction ):
|
|
1258
|
+
"""Access cross sections."""
|
|
1259
|
+
return _rawfct['ncrystal_crosssection'](self._rawobj,ekin, direction)
|
|
1260
|
+
def crossSectionIsotropic( self, ekin, repeat = None ):
|
|
1261
|
+
"""Access cross sections (should not be called for oriented processes).
|
|
1262
|
+
|
|
1263
|
+
For efficiency it is possible to provide the ekin parameter as a numpy
|
|
1264
|
+
array of numbers and get a corresponding array of cross sections
|
|
1265
|
+
back. Likewise, the repeat parameter can be set to a positive number,
|
|
1266
|
+
causing the ekin value(s) to be reused that many times and a numpy array
|
|
1267
|
+
with results returned.
|
|
1268
|
+
|
|
1269
|
+
"""
|
|
1270
|
+
return _rawfct['ncrystal_crosssection_nonoriented'](self._rawobj,ekin,repeat)
|
|
1271
|
+
|
|
1272
|
+
def crossSectionNonOriented( self, ekin, repeat = None ):
|
|
1273
|
+
"""Deprecated method. Please use the crossSectionIsotropic method
|
|
1274
|
+
instead."""
|
|
1275
|
+
from ._common import warn
|
|
1276
|
+
warn('The .crossSectionNonOriented method is deprecated.'
|
|
1277
|
+
' Please use .crossSectionIsotropic or .xsect methods instead')
|
|
1278
|
+
return self.crossSectionIsotropic( ekin, repeat )
|
|
1279
|
+
|
|
1280
|
+
def xsect(self,ekin=None,direction=None,wl=None,repeat=None):
|
|
1281
|
+
"""Convenience function which redirects calls to either crossSectionIsotropic
|
|
1282
|
+
or crossSection depending on whether or not a direction is given. It can
|
|
1283
|
+
also accept wavelengths instead of kinetic energies via the wl
|
|
1284
|
+
parameter. The repeat parameter is currently only supported when
|
|
1285
|
+
direction is not provided.
|
|
1286
|
+
"""
|
|
1287
|
+
ekin = Process._parseekin( ekin, wl )
|
|
1288
|
+
if direction is None:
|
|
1289
|
+
return self.crossSectionIsotropic( ekin, repeat )
|
|
1290
|
+
else:
|
|
1291
|
+
if repeat is None:
|
|
1292
|
+
return self.crossSection( ekin, direction )
|
|
1293
|
+
else:
|
|
1294
|
+
raise NCBadInput('The repeat parameter is not currently supported when the direction parameter is also provided.')
|
|
1295
|
+
|
|
1296
|
+
@staticmethod
|
|
1297
|
+
def _parseekin(ekin,wl):
|
|
1298
|
+
if wl is None:
|
|
1299
|
+
if ekin is None:
|
|
1300
|
+
raise NCBadInput('Please provide either one of the "ekin" or "wl" parameters.')
|
|
1301
|
+
return ekin
|
|
1302
|
+
else:
|
|
1303
|
+
if ekin is not None:
|
|
1304
|
+
raise NCBadInput('Do not provide both "ekin" and "wl" parameters')
|
|
1305
|
+
return _nc_constants.wl2ekin(wl)
|
|
1306
|
+
|
|
1307
|
+
def getSummary(self,short = False ):
|
|
1308
|
+
"""By default access a high-level summary of the process in the form of
|
|
1309
|
+
a dictionary holding various information which is also available on
|
|
1310
|
+
the underlying C++ process object. If instead short==True, what is
|
|
1311
|
+
instead returned is simply a short process label along with a
|
|
1312
|
+
(recursive) list of sub-components and their scales (if
|
|
1313
|
+
appropriate). Finally, if short=='printable', the returned object
|
|
1314
|
+
will instead be a list of strings suitable for a quick printout (each
|
|
1315
|
+
string is one line of printout).
|
|
1316
|
+
|
|
1317
|
+
"""
|
|
1318
|
+
#Not caching, method is likely to be called sparringly.
|
|
1319
|
+
|
|
1320
|
+
#short printable:
|
|
1321
|
+
if short == 'printable':
|
|
1322
|
+
toplbl,comps=self.getSummary(short=True)
|
|
1323
|
+
ll=[ toplbl]
|
|
1324
|
+
def add_lines( comps, indentlvl = 1 ):
|
|
1325
|
+
ncomps = len(comps)
|
|
1326
|
+
prefix = ' '*indentlvl
|
|
1327
|
+
for i,(scale,(lbl,subcomps)) in enumerate(comps):
|
|
1328
|
+
smb = r'\--' if i+1==ncomps else '|--'
|
|
1329
|
+
scale_str = '' if scale==1.0 else f'{scale:g} * '
|
|
1330
|
+
ll.append(f'{prefix}{smb} {scale_str}{lbl}')
|
|
1331
|
+
if subcomps:
|
|
1332
|
+
add_lines( subcomps, indentlvl + 1 )
|
|
1333
|
+
if comps:
|
|
1334
|
+
add_lines( comps )
|
|
1335
|
+
return ll
|
|
1336
|
+
|
|
1337
|
+
d=_rawfct['nc_dbg_proc'](self._rawobj)
|
|
1338
|
+
#full:
|
|
1339
|
+
if not short:
|
|
1340
|
+
return d
|
|
1341
|
+
#short:
|
|
1342
|
+
def fmt_lbl(proc):
|
|
1343
|
+
name = proc['name']
|
|
1344
|
+
summarystr = proc['specific'].get('summarystr','')
|
|
1345
|
+
return f'{name}({summarystr})' if summarystr else name
|
|
1346
|
+
def extract_subcomponents(proc):
|
|
1347
|
+
subprocs = proc.get('specific',{}).get('components',[])
|
|
1348
|
+
return (fmt_lbl(proc),list( (scl, extract_subcomponents(sp)) for scl,sp in subprocs ))
|
|
1349
|
+
return extract_subcomponents(d)
|
|
1350
|
+
|
|
1351
|
+
def dump(self,prefix=''):
|
|
1352
|
+
"""
|
|
1353
|
+
Prints a quick high level summary of the process. What is printed is in
|
|
1354
|
+
fact the lines resulting from a call to
|
|
1355
|
+
self.getSummary(short='printable'), with an optional prefix prepended to
|
|
1356
|
+
each line.
|
|
1357
|
+
"""
|
|
1358
|
+
from ._common import print
|
|
1359
|
+
_flush()
|
|
1360
|
+
print(self.dump_str(prefix=prefix),end='')
|
|
1361
|
+
_flush()
|
|
1362
|
+
|
|
1363
|
+
def dump_str(self, prefix=''):
|
|
1364
|
+
"""
|
|
1365
|
+
The string-returning sibling of .dump(..). Returns a quick high level
|
|
1366
|
+
summary of the process as a multi-line string. What is printed is in
|
|
1367
|
+
fact the lines resulting from a call to
|
|
1368
|
+
self.getSummary(short='printable'), with an optional prefix prepended to
|
|
1369
|
+
each line.
|
|
1370
|
+
"""
|
|
1371
|
+
return prefix+f'\n{prefix}'.join(self.getSummary(short='printable'))+'\n'
|
|
1372
|
+
|
|
1373
|
+
def plot(self, *args, **kwargs ):
|
|
1374
|
+
"""Convenience method for plotting cross sections. This is the same as
|
|
1375
|
+
NCrystal.plot.plot_xsect(material=self,*args,**kwargs), so refer to that
|
|
1376
|
+
function for information about allowed arguments."""
|
|
1377
|
+
from .plot import plot_xsect
|
|
1378
|
+
return plot_xsect( self, *args, **kwargs )
|
|
1379
|
+
|
|
1380
|
+
class Absorption(Process):
|
|
1381
|
+
"""Base class for calculations of absorption in materials"""
|
|
1382
|
+
|
|
1383
|
+
def __init__(self, cfgstr):
|
|
1384
|
+
"""create Absorption object based on cfg-string (same as using createAbsorption(cfgstr))"""
|
|
1385
|
+
if isinstance(cfgstr,tuple) and len(cfgstr)==2 and cfgstr[0]=='_rawobj_':
|
|
1386
|
+
#Cloning:
|
|
1387
|
+
rawobj_abs = cfgstr[1]
|
|
1388
|
+
else:
|
|
1389
|
+
rawobj_abs = _rawfct['ncrystal_create_absorption'](_str2cstr(cfgstr))
|
|
1390
|
+
self._rawobj_abs = rawobj_abs
|
|
1391
|
+
rawobj_proc = _rawfct['ncrystal_cast_abs2proc'](rawobj_abs)
|
|
1392
|
+
super(Absorption, self).__init__(rawobj_proc)
|
|
1393
|
+
|
|
1394
|
+
def clone(self):
|
|
1395
|
+
"""Clone object. The clone will be using the same physics models and sharing any
|
|
1396
|
+
read-only data with the original, but will be using its own private copy of any
|
|
1397
|
+
mutable caches. All in all, this means that the objects are safe to use
|
|
1398
|
+
concurrently in multi-threaded programming, as long as each thread gets
|
|
1399
|
+
its own clone. Return value is the new Absorption object.
|
|
1400
|
+
"""
|
|
1401
|
+
newrawobj = _rawfct['ncrystal_clone_absorption'](self._rawobj_abs)
|
|
1402
|
+
return Absorption( ('_rawobj_',newrawobj) )
|
|
1403
|
+
|
|
1404
|
+
class Scatter(Process):
|
|
1405
|
+
|
|
1406
|
+
"""Base class for calculations of scattering in materials.
|
|
1407
|
+
|
|
1408
|
+
Note that kinetic energies are in electronvolt and direction vectors are
|
|
1409
|
+
tuples of 3 numbers.
|
|
1410
|
+
|
|
1411
|
+
"""
|
|
1412
|
+
|
|
1413
|
+
def __init__(self, cfgstr):
|
|
1414
|
+
"""create Scatter object based on cfg-string (same as using createScatter(cfgstr))"""
|
|
1415
|
+
if isinstance(cfgstr,tuple) and len(cfgstr)==2 and cfgstr[0]=='_rawobj_':
|
|
1416
|
+
#Already got an ncrystal_scatter_t object:
|
|
1417
|
+
self._rawobj_scat = cfgstr[1]
|
|
1418
|
+
else:
|
|
1419
|
+
self._rawobj_scat = _rawfct['ncrystal_create_scatter'](_str2cstr(cfgstr))
|
|
1420
|
+
rawobj_proc = _rawfct['ncrystal_cast_scat2proc'](self._rawobj_scat)
|
|
1421
|
+
super(Scatter, self).__init__(rawobj_proc)
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def clone(self,rng_stream_index=None,for_current_thread=False):
|
|
1425
|
+
"""Clone object. The clone will be using the same physics models and sharing any
|
|
1426
|
+
read-only data with the original, but will be using its own private copy
|
|
1427
|
+
of any mutable caches and will get an independent RNG stream. All in
|
|
1428
|
+
all, this means that the objects are safe to use concurrently in
|
|
1429
|
+
multi-threaded programming, as long as each thread gets its own
|
|
1430
|
+
clone. Return value is the new Scatter object.
|
|
1431
|
+
|
|
1432
|
+
If greater control over RNG streams are needed, it is optionally allowed
|
|
1433
|
+
to either set rng_stream_index to a non-negative integral value, or set
|
|
1434
|
+
for_current_thread=True.
|
|
1435
|
+
|
|
1436
|
+
If rng_stream_index is set, the resulting object will use a specific
|
|
1437
|
+
rngstream index. All objects with the same indeed will share the same
|
|
1438
|
+
RNG state, so a sensible strategy is to use the same index for all
|
|
1439
|
+
scatter objects which are to be used in the same thread:
|
|
1440
|
+
|
|
1441
|
+
If setting for_current_thread=True, the resulting object will use a
|
|
1442
|
+
specific rngstream which has been set aside for the current thread. Thus
|
|
1443
|
+
this function can be called from a given work-thread, in order to get
|
|
1444
|
+
thread-safe scatter handle, with all objects cloned within the same
|
|
1445
|
+
thread sharing RNG state.
|
|
1446
|
+
|
|
1447
|
+
"""
|
|
1448
|
+
if rng_stream_index is not None:
|
|
1449
|
+
if for_current_thread:
|
|
1450
|
+
raise NCBadInput('Scatter.clone(..): do not set both rng_stream_index and for_current_thread parameters')
|
|
1451
|
+
import numbers
|
|
1452
|
+
if not isinstance(rng_stream_index, numbers.Integral) or not 0 <= rng_stream_index <= 4294967295:
|
|
1453
|
+
raise NCBadInput('Scatter.clone(..): rng_stream_index must be integral and in range [0,4294967295]')
|
|
1454
|
+
newrawobj = _rawfct['ncrystal_clone_scatter_rngbyidx'](self._rawobj_scat,int(rng_stream_index))
|
|
1455
|
+
elif for_current_thread:
|
|
1456
|
+
newrawobj = _rawfct['ncrystal_clone_scatter_rngforcurrentthread'](self._rawobj_scat)
|
|
1457
|
+
else:
|
|
1458
|
+
newrawobj = _rawfct['ncrystal_clone_scatter'](self._rawobj_scat)
|
|
1459
|
+
return Scatter( ('_rawobj_',newrawobj) )
|
|
1460
|
+
|
|
1461
|
+
def sampleScatter( self, ekin, direction, repeat = None ):
|
|
1462
|
+
"""Randomly generate scatterings.
|
|
1463
|
+
|
|
1464
|
+
Assuming a scattering took place, generate final state of neutron based
|
|
1465
|
+
on current kinetic energy and direction. Returns
|
|
1466
|
+
tuple(ekin_final,direction_final) where direct_final is itself a tuple
|
|
1467
|
+
(ux,uy,uz). The repeat parameter can be set to a positive number,
|
|
1468
|
+
causing the scattering to be sampled that many times and numpy arrays
|
|
1469
|
+
with results returned.
|
|
1470
|
+
|
|
1471
|
+
"""
|
|
1472
|
+
return _rawfct['ncrystal_samplesct'](self._rawobj_scat,ekin,direction,repeat)
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def sampleScatterIsotropic( self, ekin, repeat = None ):
|
|
1476
|
+
"""Randomly generate scatterings (should not be called for oriented processes).
|
|
1477
|
+
|
|
1478
|
+
Assuming a scattering took place, generate final state of
|
|
1479
|
+
neutron. Returns tuple(ekin_final,mu) where mu is the cosine of the
|
|
1480
|
+
scattering angle. For efficiency it is possible to provide the ekin
|
|
1481
|
+
parameter as a numpy array of numbers and get corresponding arrays of
|
|
1482
|
+
energies and mu back. Likewise, the repeat parameter can be
|
|
1483
|
+
set to a positive number, causing the ekin value(s) to be reused that
|
|
1484
|
+
many times and numpy arrays with results returned.
|
|
1485
|
+
|
|
1486
|
+
"""
|
|
1487
|
+
return _rawfct['ncrystal_samplesct_iso'](self._rawobj_scat,ekin,repeat)
|
|
1488
|
+
|
|
1489
|
+
def generateScattering( self, ekin, direction, repeat = None ):
|
|
1490
|
+
"""WARNING: Deprecated method. Please use the sampleScatter method instead.
|
|
1491
|
+
|
|
1492
|
+
Randomly generate scatterings.
|
|
1493
|
+
|
|
1494
|
+
Assuming a scattering took place, generate energy transfer (delta_ekin)
|
|
1495
|
+
and new neutron direction based on current kinetic energy and direction
|
|
1496
|
+
and return tuple(new_direction,delta_ekin). The repeat parameter can be
|
|
1497
|
+
set to a positive number, causing the scattering to be sampled that many
|
|
1498
|
+
times and numpy arrays with results returned.
|
|
1499
|
+
|
|
1500
|
+
"""
|
|
1501
|
+
from ._common import warn
|
|
1502
|
+
warn('The .generateScattering method is deprecated.'
|
|
1503
|
+
' Please use .sampleScatter or .scatter instead')
|
|
1504
|
+
return _rawfct['ncrystal_genscatter'](self._rawobj_scat,ekin,direction,repeat)
|
|
1505
|
+
|
|
1506
|
+
def generateScatteringNonOriented( self, ekin, repeat = None ):
|
|
1507
|
+
"""WARNING: Deprecated method. Please use the sampleScatterIsotropic method instead.
|
|
1508
|
+
|
|
1509
|
+
Randomly generate scatterings (should not be called for oriented processes).
|
|
1510
|
+
|
|
1511
|
+
Assuming a scattering took place, generate energy transfer (delta_ekin)
|
|
1512
|
+
and scatter angle in radians and return tuple(scatter_angle,delta_ekin)
|
|
1513
|
+
(this method should not be invoked on oriented processes). For
|
|
1514
|
+
efficiency it is possible to provide the ekin parameter as a numpy array
|
|
1515
|
+
of numbers and get corresponding arrays of angles and energy transfers
|
|
1516
|
+
back. Likewise, the repeat parameter can be set to a positive number,
|
|
1517
|
+
causing the ekin value(s) to be reused that many times and numpy arrays
|
|
1518
|
+
with results returned.
|
|
1519
|
+
|
|
1520
|
+
"""
|
|
1521
|
+
from ._common import warn
|
|
1522
|
+
warn('The .generateScatteringNonOriented method is deprecated.'
|
|
1523
|
+
' Please use .sampleScatterIsotropic or .scatter instead')
|
|
1524
|
+
return _rawfct['ncrystal_genscatter_nonoriented'](self._rawobj_scat,ekin,repeat)
|
|
1525
|
+
|
|
1526
|
+
def scatter(self,ekin=None,direction=None,wl=None,repeat=None):
|
|
1527
|
+
"""Convenience function which redirects calls to either
|
|
1528
|
+
sampleScatterIsotropic or sampleScatter depending on whether
|
|
1529
|
+
or not a direction is given. It can also accept wavelengths instead of
|
|
1530
|
+
kinetic energies via the wl parameter.
|
|
1531
|
+
"""
|
|
1532
|
+
ekin = Process._parseekin( ekin, wl )
|
|
1533
|
+
return ( self.sampleScatterIsotropic( ekin, repeat )
|
|
1534
|
+
if direction is None
|
|
1535
|
+
else self.sampleScatter( ekin, direction, repeat ) )
|
|
1536
|
+
|
|
1537
|
+
def genscat(self,ekin=None,direction=None,wl=None,repeat=None):
|
|
1538
|
+
"""WARNING: Deprecated method. Please use the "scatter" method instead.
|
|
1539
|
+
|
|
1540
|
+
Convenience function which redirects calls to either
|
|
1541
|
+
generateScatteringNonOriented or generateScattering depending on whether
|
|
1542
|
+
or not a direction is given. It can also accept wavelengths instead of
|
|
1543
|
+
kinetic energies via the wl parameter.
|
|
1544
|
+
"""
|
|
1545
|
+
from ._common import warn
|
|
1546
|
+
warn('The .genscat method is deprecated.'
|
|
1547
|
+
' Please use .scatter instead')
|
|
1548
|
+
ekin = Process._parseekin( ekin, wl )
|
|
1549
|
+
return ( self.generateScatteringNonOriented( ekin, repeat )
|
|
1550
|
+
if direction is None
|
|
1551
|
+
else self.generateScattering( ekin, direction, repeat ) )
|
|
1552
|
+
|
|
1553
|
+
def rngSupportsStateManipulation(self):
|
|
1554
|
+
"""Query whether associated RNG stream supports state manipulation"""
|
|
1555
|
+
return bool(_rawfct['ncrystal_rngsupportsstatemanip_ofscatter'](self._rawobj_scat))
|
|
1556
|
+
|
|
1557
|
+
def getRNGState(self):
|
|
1558
|
+
"""Get current RNG state (as printable hex-string with RNG type info
|
|
1559
|
+
embedded). This function returns None if RNG stream does not support
|
|
1560
|
+
state manipulation
|
|
1561
|
+
"""
|
|
1562
|
+
return _rawfct['nc_getrngstate_scat'](self._rawobj_scat)
|
|
1563
|
+
|
|
1564
|
+
def setRNGState(self,state):
|
|
1565
|
+
"""Set current RNG state.
|
|
1566
|
+
|
|
1567
|
+
Note that setting the rng state will affect all objects sharing the
|
|
1568
|
+
RNG stream with the given scatter object (and those subsequently cloned
|
|
1569
|
+
from any of those).
|
|
1570
|
+
|
|
1571
|
+
Note that if the provided state originates in (the current version
|
|
1572
|
+
of) NCrystal's builtin RNG algorithm, it can always be used here,
|
|
1573
|
+
even if the current RNG uses a different algorithm (it will simply be
|
|
1574
|
+
replaced). Otherwise, a mismatch of RNG stream algorithms will result
|
|
1575
|
+
in an error.
|
|
1576
|
+
"""
|
|
1577
|
+
_rawfct['ncrystal_setrngstate_ofscatter']( self._rawobj_scat,
|
|
1578
|
+
_str2cstr(state) )
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def createInfo(cfgstr):
|
|
1582
|
+
"""Construct Info object based on provided configuration (using available factories)"""
|
|
1583
|
+
return Info(cfgstr)
|
|
1584
|
+
|
|
1585
|
+
def createScatter(cfgstr):
|
|
1586
|
+
"""Construct Scatter object based on provided configuration (using available factories)"""
|
|
1587
|
+
return Scatter(cfgstr)
|
|
1588
|
+
|
|
1589
|
+
def createScatterIndependentRNG(cfgstr,seed = 0):
|
|
1590
|
+
"""Construct Scatter object based on provided configuration (using available
|
|
1591
|
+
factories) and with its own independent RNG stream (using the builtin RNG
|
|
1592
|
+
generator and the provided seed)"""
|
|
1593
|
+
rawobj = _rawfct['ncrystal_create_scatter_builtinrng'](_str2cstr(cfgstr),seed)
|
|
1594
|
+
return Scatter(('_rawobj_',rawobj))
|
|
1595
|
+
|
|
1596
|
+
def createAbsorption(cfgstr):
|
|
1597
|
+
"""Construct Absorption object based on provided configuration (using available factories)"""
|
|
1598
|
+
return Absorption(cfgstr)
|
|
1599
|
+
|
|
1600
|
+
def createLoadedMaterial(cfgstr):
|
|
1601
|
+
"""Create and return LoadedMaterial object. A "loaded material" is simply a
|
|
1602
|
+
convenience object which combines loaded Info, Scatter, and Absorption
|
|
1603
|
+
objects of the same material.
|
|
1604
|
+
|
|
1605
|
+
This function does the same as calling the LoadedMaterial constructor
|
|
1606
|
+
directly, and exists purely for consistency.
|
|
1607
|
+
"""
|
|
1608
|
+
return LoadedMaterial(cfgstr)
|
|
1609
|
+
|
|
1610
|
+
load = createLoadedMaterial#convenience alias
|
|
1611
|
+
|
|
1612
|
+
class LoadedMaterial:
|
|
1613
|
+
"""This convenience class combines loaded Info, Scatter, and Absorption
|
|
1614
|
+
objects of the same material."""
|
|
1615
|
+
|
|
1616
|
+
@staticmethod
|
|
1617
|
+
def fromExistingObjects(info=None,scatter=None,absorption=None):
|
|
1618
|
+
return LoadedMaterial( ('__fromexistingobjects__',dict(info=info,scatter=scatter,absorption=absorption)) )
|
|
1619
|
+
|
|
1620
|
+
def __init__(self,cfgstr):
|
|
1621
|
+
"""Instantiate from a cfg-string which will be passed to the createInfo,
|
|
1622
|
+
createScatter, and createAbsorption functions.
|
|
1623
|
+
|
|
1624
|
+
As a special case, one can also pass a TextData object to this function,
|
|
1625
|
+
and it will be loaded as by calling the directLoad(..)
|
|
1626
|
+
function. Likewise, python strings beginning with 'NCMAT' and containing
|
|
1627
|
+
at least one newline, will also be assumed to be raw NCMAT data and
|
|
1628
|
+
loaded accordingly. For more control, however, the .directLoad function
|
|
1629
|
+
is recommended.
|
|
1630
|
+
"""
|
|
1631
|
+
if isinstance(cfgstr,tuple) and len(cfgstr)==2 and cfgstr[0] == '__fromexistingobjects__':
|
|
1632
|
+
self.__i,self.__s,self.__a = cfgstr[1]['info'],cfgstr[1]['scatter'],cfgstr[1]['absorption']
|
|
1633
|
+
assert self.__i is None or isinstance(self.__i,Info)
|
|
1634
|
+
assert self.__s is None or isinstance(self.__s,Scatter)
|
|
1635
|
+
assert self.__a is None or isinstance(self.__a,Absorption)
|
|
1636
|
+
elif ( isinstance(cfgstr,TextData)
|
|
1637
|
+
or ( isinstance(cfgstr,str) and cfgstr.startswith('NCMAT') and '\n' in cfgstr ) ):
|
|
1638
|
+
m = directLoad( cfgstr, dtype = cfgstr.dataType if isinstance(cfgstr,TextData) else 'ncmat' )
|
|
1639
|
+
self.__i = m.info
|
|
1640
|
+
self.__s = m.scatter
|
|
1641
|
+
self.__a = m.absorption
|
|
1642
|
+
else:
|
|
1643
|
+
self.__i = createInfo(cfgstr)
|
|
1644
|
+
self.__s = createScatter(cfgstr)
|
|
1645
|
+
self.__a = createAbsorption(cfgstr)
|
|
1646
|
+
|
|
1647
|
+
@property
|
|
1648
|
+
def info(self):
|
|
1649
|
+
"""Info object (None if not present)."""
|
|
1650
|
+
return self.__i
|
|
1651
|
+
|
|
1652
|
+
@property
|
|
1653
|
+
def scatter(self):
|
|
1654
|
+
"""Scatter object (None if not present)."""
|
|
1655
|
+
return self.__s
|
|
1656
|
+
|
|
1657
|
+
@property
|
|
1658
|
+
def absorption(self):
|
|
1659
|
+
"""Absorption object (None if not present)."""
|
|
1660
|
+
return self.__a
|
|
1661
|
+
|
|
1662
|
+
def plot(self, *args, **kwargs ):
|
|
1663
|
+
"""Convenience method for plotting cross sections. This is the same as
|
|
1664
|
+
NCrystal.plot.plot_xsect(material=self,*args,**kwargs), so refer to that
|
|
1665
|
+
function for information about allowed arguments."""
|
|
1666
|
+
from .plot import plot_xsect
|
|
1667
|
+
return plot_xsect( self, *args, **kwargs )
|
|
1668
|
+
|
|
1669
|
+
def dump(self, verbose=0 ):
|
|
1670
|
+
"""
|
|
1671
|
+
Convenience method for print information about contained objects. Use
|
|
1672
|
+
verbose argument to set verbosity level to 0 (minimal), 1 (middle), 2
|
|
1673
|
+
(most verbose).
|
|
1674
|
+
"""
|
|
1675
|
+
from ._common import print
|
|
1676
|
+
_flush()
|
|
1677
|
+
print(self.dump_str(verbose=verbose),end='')
|
|
1678
|
+
_flush()
|
|
1679
|
+
|
|
1680
|
+
def dump_str(self, verbose=0 ):
|
|
1681
|
+
"""
|
|
1682
|
+
The string-returning sibling of .dump(..). Returns information about
|
|
1683
|
+
contained objects as a multi-line string. Use verbose argument to set
|
|
1684
|
+
verbosity level to 0 (minimal), 1 (middle), 2 (most verbose).
|
|
1685
|
+
"""
|
|
1686
|
+
res=''
|
|
1687
|
+
has_any=False
|
|
1688
|
+
for name,descr in [ ('info','Material info'),
|
|
1689
|
+
('scatter','Scattering process (objects tree)'),
|
|
1690
|
+
('absorption','Absorption process (object tree)') ]:
|
|
1691
|
+
o = getattr(self,name)
|
|
1692
|
+
if o:
|
|
1693
|
+
has_any=True
|
|
1694
|
+
res += '\n>>> '+descr+':\n'
|
|
1695
|
+
res += '\n'
|
|
1696
|
+
res += o.dump_str(**(dict(verbose=verbose) if name=='info' else {}))
|
|
1697
|
+
if not has_any:
|
|
1698
|
+
res += '<empty>'
|
|
1699
|
+
res += '\n'
|
|
1700
|
+
return res
|
|
1701
|
+
|
|
1702
|
+
def xsect( self, *args, **kwargs ):
|
|
1703
|
+
"""Convenience function which adds up the cross sections from any loaded
|
|
1704
|
+
absorption and scatter processes. Refer to the Process.xsect method for
|
|
1705
|
+
arguments."""
|
|
1706
|
+
procs = [p for p in (self.scatter,self.absorption) if p is not None]
|
|
1707
|
+
if not procs:
|
|
1708
|
+
raise NCCalcError('.xsect(..) can only be called on'
|
|
1709
|
+
' LoadedMaterial which contains processes.')
|
|
1710
|
+
assert len(procs) in (1,2)
|
|
1711
|
+
xs = procs[0].xsect(*args,**kwargs)
|
|
1712
|
+
if len(procs)==2:
|
|
1713
|
+
xs += procs[1].xsect(*args,**kwargs)
|
|
1714
|
+
return xs
|
|
1715
|
+
|
|
1716
|
+
def macroscopic_xsect( self, *args, **kwargs ):
|
|
1717
|
+
"""Convenience function which calculates cross sections from any loaded
|
|
1718
|
+
absorption and scatter processes, and converts to macroscopic cross
|
|
1719
|
+
sections via the numerical density of the loaded Info object. Returned
|
|
1720
|
+
unit is 1/cm.
|
|
1721
|
+
"""
|
|
1722
|
+
if not self.info or not (self.scatter or self.absorption):
|
|
1723
|
+
raise NCCalcError('.macroscopic_xsect(..) can only be called on'
|
|
1724
|
+
' LoadedMaterial which contains both processes'
|
|
1725
|
+
' and material Info.')
|
|
1726
|
+
return self.info.factor_macroscopic_xs * self.xsect( *args,**kwargs )
|
|
1727
|
+
|
|
1728
|
+
def __str__(self):
|
|
1729
|
+
def fmt( x ):
|
|
1730
|
+
return str(x) if x else 'n/a'
|
|
1731
|
+
return 'LoadedMaterial(Info=%s, Scatter=%s, Absorption=%s)'%( fmt(self.__i),
|
|
1732
|
+
fmt(self.__s),
|
|
1733
|
+
fmt(self.__a) )
|
|
1734
|
+
def __repr__(self):
|
|
1735
|
+
return str(self)
|
|
1736
|
+
|
|
1737
|
+
def directLoad( data, cfg_params='', *, dtype='',
|
|
1738
|
+
doInfo = True, doScatter = True, doAbsorption = True ):
|
|
1739
|
+
"""Convenience function which creates Info, Scatter, and Absorption objects
|
|
1740
|
+
directly from a text data source (like a data string or file path) rather
|
|
1741
|
+
requiring an on-disk or in-memory file. Such usage obviously precludes
|
|
1742
|
+
proper caching behind the scenes, and is intended for scenarios where the
|
|
1743
|
+
same data should not be used repeatedly.
|
|
1744
|
+
"""
|
|
1745
|
+
if not dtype and hasattr(data,'dataSourceName') and hasattr(data,'rawData') and hasattr(data,'dataType'):
|
|
1746
|
+
#TextData might carry the dtype:
|
|
1747
|
+
dtype = data.dataType
|
|
1748
|
+
|
|
1749
|
+
from .misc import AnyTextData
|
|
1750
|
+
any_data = AnyTextData( data )
|
|
1751
|
+
content = any_data.content
|
|
1752
|
+
|
|
1753
|
+
if not dtype:
|
|
1754
|
+
if content.startswith('NCMAT'):
|
|
1755
|
+
dtype = 'ncmat'
|
|
1756
|
+
else:
|
|
1757
|
+
#Try to give a nice error for a common user-error:
|
|
1758
|
+
if content[0:1024].lstrip().startswith('NCMAT'):
|
|
1759
|
+
raise NCBadInput('NCMAT data must have "NCMAT" as the first 5 characters (must not be preceded by whitespace)')
|
|
1760
|
+
|
|
1761
|
+
if not dtype and any_data.name and '.' in any_data.name:
|
|
1762
|
+
p = any_data.name.split('.')
|
|
1763
|
+
if len(p)>=2 and p[-1] and p[-1].isalpha() and p[-1] not in ('gz','tgz','bz2','zip','tar'):
|
|
1764
|
+
dtype = dtype
|
|
1765
|
+
|
|
1766
|
+
rawi,raws,rawa = _rawfct['multicreate_direct'](content,dtype,cfg_params,doInfo,doScatter,doAbsorption)
|
|
1767
|
+
info = Info( ('_rawobj_',rawi) ) if rawi else None
|
|
1768
|
+
scatter = Scatter( ('_rawobj_',raws) ) if raws else None
|
|
1769
|
+
absorption = Absorption( ('_rawobj_',rawa) ) if rawa else None
|
|
1770
|
+
return LoadedMaterial.fromExistingObjects(info,scatter,absorption)
|
|
1771
|
+
|
|
1772
|
+
MultiCreated = LoadedMaterial#backwards compat alias
|
|
1773
|
+
directMultiCreate = directLoad#backwards compat alias
|
|
1774
|
+
|
|
1775
|
+
class TextData:
|
|
1776
|
+
"""Text data accessible line by line, with associated meta-data. This always
|
|
1777
|
+
include a UID (useful for comparison and downstream caching purposes) and
|
|
1778
|
+
the data type (e.g. "ncmat"). Optionally available is the last known
|
|
1779
|
+
on-disk path to a file with the same content, which might be useful in
|
|
1780
|
+
case the data needs to be passed to 3rd party software which can only
|
|
1781
|
+
work with physical files.
|
|
1782
|
+
|
|
1783
|
+
Text data objects are easily line-iterable, easily providing lines
|
|
1784
|
+
(without newline characters): for( auto& line : mytextdata ) {...}. Of
|
|
1785
|
+
course, the raw underlying data buffer can also be accessed if needed.
|
|
1786
|
+
|
|
1787
|
+
The raw data must be ASCII or UTF-8 text, with line endings \\n=CR=0x0A
|
|
1788
|
+
(Unix) or \\r\\n=LF+CR=0x0D0A (Windows/DOS). Other encodings might work
|
|
1789
|
+
only if 0x00, 0x0A, 0x0D bytes do not occur in them outside of line
|
|
1790
|
+
endings.
|
|
1791
|
+
|
|
1792
|
+
Notice that ancient pre-OSX Mac line-endings \\r=LF=0x0D are not
|
|
1793
|
+
supported, and iterators will actually throw an error upon encountering
|
|
1794
|
+
them. This is done on purpose, since files with \\r on unix might hide
|
|
1795
|
+
content when inspected in a terminal can be either confusing, a potential
|
|
1796
|
+
security issue, or both.
|
|
1797
|
+
"""
|
|
1798
|
+
|
|
1799
|
+
def __init__(self,name):
|
|
1800
|
+
"""create TextData object based on string (same as using createTextData(name))"""
|
|
1801
|
+
ll=_rawfct['nc_gettextdata'](name)
|
|
1802
|
+
assert len(ll)==5
|
|
1803
|
+
self.__rd = ll[0]
|
|
1804
|
+
self.__uid = int(ll[1])
|
|
1805
|
+
self.__dsn = ll[2]
|
|
1806
|
+
self.__datatype= ll[3]
|
|
1807
|
+
import pathlib
|
|
1808
|
+
self.__rp = pathlib.Path(ll[4]) if ll[4] else None
|
|
1809
|
+
|
|
1810
|
+
@property
|
|
1811
|
+
def uid(self):
|
|
1812
|
+
"""Unique identifier. Objects only have identical UID if all contents and
|
|
1813
|
+
metadata are identical."""
|
|
1814
|
+
return self.__uid
|
|
1815
|
+
|
|
1816
|
+
@property
|
|
1817
|
+
def dataType(self):
|
|
1818
|
+
"""Data type ("ncmat", "lau", ...)."""
|
|
1819
|
+
return self.__datatype
|
|
1820
|
+
|
|
1821
|
+
@property
|
|
1822
|
+
def dataSourceName(self):
|
|
1823
|
+
"""Data source name. This might for instance be a filename."""
|
|
1824
|
+
return self.__dsn
|
|
1825
|
+
|
|
1826
|
+
@property
|
|
1827
|
+
def rawData(self):
|
|
1828
|
+
"""Raw access to underlying data."""
|
|
1829
|
+
return self.__rd
|
|
1830
|
+
|
|
1831
|
+
@property
|
|
1832
|
+
def lastKnownOnDiskLocation(self):
|
|
1833
|
+
"""Last known on-disk location (returns None if unavailable). Note that there
|
|
1834
|
+
is no guarantee against the file having been removed or modified since the
|
|
1835
|
+
TextData object was created.
|
|
1836
|
+
"""
|
|
1837
|
+
return self.__rp
|
|
1838
|
+
|
|
1839
|
+
def load( self ):
|
|
1840
|
+
"""Convenience method which loads the TextData with the directLoad(..)
|
|
1841
|
+
function and returns the resulting LoadedMaterial object.
|
|
1842
|
+
"""
|
|
1843
|
+
return LoadedMaterial(self)
|
|
1844
|
+
|
|
1845
|
+
def dump( self ):
|
|
1846
|
+
"""Convenience method which prints the contents.
|
|
1847
|
+
"""
|
|
1848
|
+
from ._common import print
|
|
1849
|
+
_flush()
|
|
1850
|
+
print( self.rawData )
|
|
1851
|
+
_flush()
|
|
1852
|
+
|
|
1853
|
+
def dump_str( self ):
|
|
1854
|
+
"""Alias which simply returns .rawData (this method exists exclusively
|
|
1855
|
+
for API consistency).
|
|
1856
|
+
"""
|
|
1857
|
+
return self.rawData
|
|
1858
|
+
|
|
1859
|
+
def __str__(self):
|
|
1860
|
+
return 'TextData(%s, uid=%i, %i chars)'%(self.__dsn,self.__uid,len(self.__rd))
|
|
1861
|
+
|
|
1862
|
+
def __repr__(self):
|
|
1863
|
+
return self.__str__()
|
|
1864
|
+
|
|
1865
|
+
def __iter__(self):
|
|
1866
|
+
"""Line-iteration, yielding lines without terminating newline characters"""
|
|
1867
|
+
from io import StringIO
|
|
1868
|
+
def chomp(x):
|
|
1869
|
+
return x[:-2] if x.endswith('\r\n') else (x[:-1] if x.endswith('\n') else x)
|
|
1870
|
+
for e in StringIO(self.__rd):
|
|
1871
|
+
yield chomp(e)
|
|
1872
|
+
|
|
1873
|
+
def createTextData(name):
|
|
1874
|
+
"""creates TextData objects based on requested name"""
|
|
1875
|
+
return TextData(name)
|
|
1876
|
+
|
|
1877
|
+
def setDefaultRandomGenerator(rg):
|
|
1878
|
+
"""Set the default random generator.
|
|
1879
|
+
|
|
1880
|
+
Note that this can only change the random generator for those processes not
|
|
1881
|
+
already created.
|
|
1882
|
+
|
|
1883
|
+
To ensure Python does not clean up the passed function object prematurely,
|
|
1884
|
+
the NCrystal python code will keep a reference to the passed function
|
|
1885
|
+
eternally (or rather, until the Python process shuts down).
|
|
1886
|
+
|
|
1887
|
+
"""
|
|
1888
|
+
_rawfct['ncrystal_setrandgen'](rg)
|
|
1889
|
+
|
|
1890
|
+
def clearCaches():
|
|
1891
|
+
"""Clear various caches"""
|
|
1892
|
+
_rawfct['ncrystal_clear_caches']()
|
|
1893
|
+
|
|
1894
|
+
def _flush():
|
|
1895
|
+
import sys
|
|
1896
|
+
sys.stdout.flush()
|
|
1897
|
+
sys.stderr.flush()
|
|
1898
|
+
|
|
1899
|
+
def enableFactoryThreads( nthreads = 'auto' ):
|
|
1900
|
+
"""Enable threading during object initialisation phase. Supply 'auto' or a
|
|
1901
|
+
value >= 9999 to simply use a number of threads appropriate for the system.
|
|
1902
|
+
|
|
1903
|
+
The requested value is the TOTAL number of threads utilised INCLUDING the
|
|
1904
|
+
main user thread. Thus, a value of 0 or 1 number will disable this thread
|
|
1905
|
+
pool, while for instance calling enableFactoryThreads(8) will result in 7
|
|
1906
|
+
secondary worker threads being allocated.
|
|
1907
|
+
|
|
1908
|
+
"""
|
|
1909
|
+
nt = 9999 if nthreads=='auto' else min(9999,max(1,int(nthreads)))
|
|
1910
|
+
_rawfct['ncrystal_enable_factory_threadpool'](nt)
|