ncrystal-python 3.9.81__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. NCrystal/__init__.py +85 -0
  2. NCrystal/__main__.py +98 -0
  3. NCrystal/_chooks.py +854 -0
  4. NCrystal/_cli_cif2ncmat.py +269 -0
  5. NCrystal/_cli_endf2ncmat.py +503 -0
  6. NCrystal/_cli_hfg2ncmat.py +144 -0
  7. NCrystal/_cli_mcstasunion.py +74 -0
  8. NCrystal/_cli_ncmat2cpp.py +31 -0
  9. NCrystal/_cli_ncmat2hkl.py +180 -0
  10. NCrystal/_cli_nctool.py +1018 -0
  11. NCrystal/_cli_vdos2ncmat.py +463 -0
  12. NCrystal/_cli_verifyatompos.py +257 -0
  13. NCrystal/_cliimpl.py +307 -0
  14. NCrystal/_cliwrap_config.py +36 -0
  15. NCrystal/_common.py +499 -0
  16. NCrystal/_coreimpl.py +114 -0
  17. NCrystal/_hfgdata.py +546 -0
  18. NCrystal/_hklobjects.py +136 -0
  19. NCrystal/_is_std.py +0 -0
  20. NCrystal/_locatelib.py +210 -0
  21. NCrystal/_miscimpl.py +354 -0
  22. NCrystal/_mmc.py +757 -0
  23. NCrystal/_msg.py +60 -0
  24. NCrystal/_ncmat2cpp_impl.py +445 -0
  25. NCrystal/_ncmatimpl.py +2131 -0
  26. NCrystal/_numpy.py +76 -0
  27. NCrystal/_testimpl.py +579 -0
  28. NCrystal/api.py +56 -0
  29. NCrystal/atomdata.py +177 -0
  30. NCrystal/cfgstr.py +77 -0
  31. NCrystal/cifutils.py +1795 -0
  32. NCrystal/cli.py +96 -0
  33. NCrystal/constants.py +134 -0
  34. NCrystal/core.py +1910 -0
  35. NCrystal/datasrc.py +226 -0
  36. NCrystal/exceptions.py +66 -0
  37. NCrystal/hfg2ncmat.py +270 -0
  38. NCrystal/mcstasutils.py +438 -0
  39. NCrystal/misc.py +317 -0
  40. NCrystal/mmc.py +35 -0
  41. NCrystal/ncmat.py +778 -0
  42. NCrystal/ncmat2cpp.py +80 -0
  43. NCrystal/obsolete.py +67 -0
  44. NCrystal/plot.py +484 -0
  45. NCrystal/plugins.py +49 -0
  46. NCrystal/test.py +76 -0
  47. NCrystal/vdos.py +1034 -0
  48. ncrystal_python-3.9.81.dist-info/LICENSE +206 -0
  49. ncrystal_python-3.9.81.dist-info/METADATA +515 -0
  50. ncrystal_python-3.9.81.dist-info/RECORD +53 -0
  51. ncrystal_python-3.9.81.dist-info/WHEEL +5 -0
  52. ncrystal_python-3.9.81.dist-info/entry_points.txt +10 -0
  53. ncrystal_python-3.9.81.dist-info/top_level.txt +1 -0
NCrystal/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)