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/_mmc.py ADDED
@@ -0,0 +1,757 @@
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
+ # For backwards compatibility we keep __all__ here in this internal file. In
23
+ # NCrystal 4.0.0 we actually renamed the public API file from _mmc.py to mmc.py.
24
+
25
+ __all__ = ['runsim_diffraction_pattern',
26
+ 'quick_diffraction_pattern',
27
+ 'quick_diffraction_pattern_autoparams']
28
+
29
+ def runsim_diffraction_pattern( cfgstr, *,
30
+ geomcfg,
31
+ srccfg,
32
+ nthreads = 'auto',
33
+ tally_detail_lvl = 2 ):
34
+ """Run embedded "mini-MonteCarlo" to produce diffraction pattern, including
35
+ both effects of multiple scattering and absorption physics. This for now
36
+ results in exit angle (angle of emitted neutrons wrt. the Z-axis)
37
+ histograms, with a perfect 4pi detector.
38
+
39
+ This is highly experimental, and not yet fully documented.
40
+
41
+ Example geomcfg: 'sphere;r=0.1'. This describes a 0.1m=10cm radius sphere
42
+ centered at (0,0,0).
43
+
44
+ Example srccfg: 'constant;ekin=0.025;n=1e6;z=-0.1'. This starts 1e6 0.025eV
45
+ neutrons at (0,0,-10cm) with a direction (0,0,1).
46
+
47
+ tally_detail_lvl can be reduced to 1 or 0, if only the exit_angle histogram
48
+ is needed. tally_detail_lvl=2 provides more details, including histograms
49
+ for specific components (e.g. separating the contributions from single and
50
+ multiple scattering, and inelastic/elastic scatterings.
51
+
52
+ nthreads can be 'auto' or a specific integral value of threads to use.
53
+
54
+ """
55
+ assert tally_detail_lvl in (0,1,2)
56
+ from ._numpy import _np, _ensure_numpy, _np_linspace
57
+ _ensure_numpy()
58
+
59
+ nthreads = 9999 if nthreads=='auto' else min(9999,max(1,int(nthreads)))
60
+
61
+ setup_info = dict( nthreads = nthreads,
62
+ tally_detail_lvl = tally_detail_lvl,
63
+ material_cfgstr = cfgstr,
64
+ geomcfg = geomcfg,
65
+ srccfg = srccfg )
66
+
67
+ from ._chooks import _get_raw_cfcts
68
+ _rawfct = _get_raw_cfcts()
69
+ cfct = _rawfct['runmmcsim_stdengine']
70
+
71
+ ct,errsq,res_json = cfct( nthreads = nthreads,
72
+ tally_detail_lvl = int(tally_detail_lvl),
73
+ mat_cfgstr = str(cfgstr),
74
+ mmc_geomcfg = str(geomcfg),
75
+ mmc_srccfg = str(srccfg) )
76
+
77
+ class Hist1D:
78
+
79
+ def __init__( self, data ):
80
+ if data=='_no_init_':
81
+ return
82
+ self.__stats = data.get('stats')
83
+ self.__title = data['title']
84
+ hb = data['bindata']
85
+ self.__xmin = hb['xmin']
86
+ self.__xmax = hb['xmax']
87
+ self.__nbins = hb['nbins']
88
+ self.__y = _np.asarray(hb['content'],dtype=float)
89
+ esq = hb.get('errorsq')
90
+ self.__yerrsq = ( _np.asarray(esq,dtype=float)
91
+ if esq is not None
92
+ else None )
93
+ self.__yerr = None
94
+ assert self.__nbins == len(self.__y)
95
+ assert self.__yerrsq is None or self.__nbins == len(self.__yerrsq)
96
+
97
+ def clone( self, rebin_factor = 1 ):
98
+ c = Hist1D('_no_init_')
99
+ c.__stats = self.__stats
100
+ c.__title = self.__title
101
+ c.__xmin = self.__xmin
102
+ c.__xmax = self.__xmax
103
+ c.__nbins = self.__nbins
104
+ c.__y = self.__y.copy()
105
+ c.__yerrsq = self.__yerrsq.copy() if self.__yerrsq is not None else None
106
+ c.__yerr = self.__yerr.copy() if self.__yerr is not None else None
107
+ if rebin_factor > 1:
108
+ c.rebin( rebin_factor )
109
+ return c
110
+
111
+ def integrate( self, xlow, xhigh, tolerance = 1e-5 ):
112
+ """
113
+ Returns integrated contents of the histogram over the area
114
+ [xlow,xhigh] along with the error of that value in a tuple
115
+ (content,error).
116
+
117
+ This is done translating xlow and xhigh to exact bin edges and then
118
+ calling integrate_bins. If that is not possible within the
119
+ tolerance, an exception is raised.
120
+
121
+ """
122
+ if not ( xhigh >= xlow ):
123
+ from .exceptions import NCBadInput
124
+ raise NCBadInput('Invalid integration range requested.')
125
+
126
+ bw = self.binwidth
127
+ def _findedge(x):
128
+ if x <= self.__xmin:
129
+ return 0
130
+ if x >= self.__xmax:
131
+ return self.nbins
132
+ r = ( x - self.__xmin ) / bw
133
+ ir = int(r+0.5)
134
+ if abs(r-ir) > tolerance:
135
+ from .exceptions import NCBadInput
136
+ raise NCBadInput(f'Value {x} does not correspond exactly'
137
+ ' to a bin edge within the tolerance.')
138
+ return ir
139
+ e_low = _findedge(xlow)
140
+ e_high = _findedge(xhigh)
141
+ if e_low == e_high:
142
+ return ( 0.0, 0.0 )
143
+ assert e_low >= 0
144
+ assert e_low < e_high < self.nbins
145
+ return self.integrate_bins( e_low, e_high - 1 )
146
+
147
+ def integrate_bins( self, bin_low = None, bin_up = None ):
148
+ """
149
+ Returns integrated contents of the bins [bin_low,bin_up[ along with
150
+ the error of that value in a tuple (content,error).
151
+
152
+ If bin_low is None the integration will start at the first bin and
153
+ include the underflow bin.
154
+
155
+ If bin_up is None the integration will end at the last bin and
156
+ include the overflow bin.
157
+ """
158
+
159
+ add_overflow, add_underflow = False, False
160
+ if bin_low is None:
161
+ add_underflow = True
162
+ bin_low = 0
163
+ underflow_c = self.stats.get('underflow')
164
+ underflow_e2 = self.stats.get('underflow_errorsq')
165
+ if bool(underflow_c is None) != bool(underflow_e2 is None):
166
+ from .exceptions import NCBadInput
167
+ raise NCBadInput('Inconsistent underflow info')
168
+ if underflow_c is None:
169
+ add_underflow = False
170
+
171
+ if bin_up is None:
172
+ add_overflow = True
173
+ bin_up = self.__nbins
174
+ overflow_c = self.stats.get('overflow')
175
+ overflow_e2 = self.stats.get('overflow_errorsq')
176
+ if bool(overflow_c is None) != bool(overflow_e2 is None):
177
+ from .exceptions import NCBadInput
178
+ raise NCBadInput('Inconsistent overflow info')
179
+ if overflow_c is None:
180
+ add_overflow = False
181
+
182
+ bin_low, bin_up = int(bin_low), int(bin_up)
183
+ if bin_up < bin_low or bin_low<0 or bin_up > self.__nbins:
184
+ from .exceptions import NCBadInput
185
+ raise NCBadInput('Invalid bin range requested')
186
+ content_integral = self.__y[bin_low:bin_up].sum()
187
+ if add_underflow:
188
+ content_integral += underflow_c
189
+ if add_overflow:
190
+ content_integral += overflow_c
191
+ if self.__yerrsq is None:
192
+ #unweighted, just base erros on contents:
193
+ return ( content_integral, _np.sqrt(content_integral) )
194
+ errorsq_integral = self.__yerrsq[bin_low:bin_up].sum()
195
+ if add_underflow:
196
+ errorsq_integral += underflow_e2
197
+ if add_overflow:
198
+ errorsq_integral += overflow_e2
199
+ return ( content_integral, _np.sqrt(errorsq_integral) )
200
+
201
+ def add_contents( self, other_hist ):
202
+ o = other_hist
203
+ assert self.__xmin == o.__xmin
204
+ assert self.__xmax == o.__xmax
205
+ assert self.__nbins == o.__nbins
206
+ self.__stats = {}
207
+ self.__title = '<edited>'
208
+ self.__y += o.__y
209
+ self.__yerr = None
210
+ if self.__yerrsq is None:
211
+ if o.__yerrsq is None:
212
+ pass#done
213
+ else:
214
+ self.__yerrsq = self.__y + o.__yerrsq
215
+ else:
216
+ if o.__yerrsq is None:
217
+ self.__yerrsq += o.__y
218
+ else:
219
+ self.__yerrsq += o.__yerrsq
220
+
221
+ def rebin( self, rebin_factor ):
222
+ assert self.__nbins % rebin_factor == 0
223
+ def _dorebin(x):
224
+ return _np.sum( x.reshape( len(x)//rebin_factor, rebin_factor ),
225
+ axis=1)
226
+ self.__y = _dorebin(self.__y)
227
+ self.__yerr = None
228
+ if self.__yerrsq is not None:
229
+ self.__yerrsq = _dorebin(self.__yerrsq)
230
+ self.__nbins = self.__nbins // rebin_factor
231
+ assert self.__yerrsq is None or len(self.__yerrsq)==len(self.__y)
232
+ assert self.__nbins==len(self.__y)
233
+
234
+ @property
235
+ def stats( self ):
236
+ return self.__stats or {}
237
+
238
+ @property
239
+ def errors( self ):
240
+ if self.__yerr is None:
241
+ self.__yerr = _np.sqrt( self.errors_squared )
242
+ return self.__yerr
243
+
244
+ @property
245
+ def errors_squared( self ):
246
+ return ( self.__yerrsq
247
+ if self.__yerrsq is not None
248
+ else self.__y )
249
+
250
+ @property
251
+ def content( self ):
252
+ return self.__y
253
+
254
+ @property
255
+ def title( self ):
256
+ return self.__title
257
+
258
+ @property
259
+ def xmin( self ):
260
+ return self.__xmin
261
+
262
+ @property
263
+ def xmax( self ):
264
+ return self.__xmax
265
+
266
+ @property
267
+ def binwidth( self ):
268
+ return (self.__xmax-self.__xmin)/self.__nbins
269
+
270
+ @property
271
+ def nbins( self ):
272
+ return self.__nbins
273
+
274
+ @property
275
+ def bincenters( self ):
276
+ halfbw = 0.5*self.binwidth
277
+ return _np_linspace(self.__xmin+halfbw,
278
+ self.__xmax-halfbw,
279
+ self.__nbins)
280
+
281
+ @property
282
+ def binedges( self ):
283
+ return _np_linspace(self.__xmin,
284
+ self.__xmax,
285
+ self.__nbins+1)
286
+
287
+
288
+ def _hist_curve( self, error_offset = 0.0 ):
289
+ be = self.binedges
290
+ y = self.content
291
+ if error_offset:
292
+ y += self.errors * error_offset
293
+ cx = _np.empty(self.__nbins*2)
294
+ cy = _np.empty(self.__nbins*2)
295
+ i = 0
296
+ for ibin in range(self.__nbins):
297
+ cx[i] = be[ibin]
298
+ cy[i] = y[ibin]
299
+ i+=1
300
+ cx[i] = be[ibin+1]
301
+ cy[i] = y[ibin]
302
+ i+=1
303
+ return cx,cy
304
+
305
+ def errorbar_args( self, style = True, **kwargs ):
306
+ d = {'x':self.bincenters,
307
+ 'y':self.content,'xerr':0.5*self.binwidth,
308
+ 'yerr':self.errors }
309
+ if style:
310
+ d.update({'fmt':'.',#dont connect with line
311
+ 'mec':'black','mfc':'black',
312
+ #'ms':4,'mew':1,
313
+ 'ecolor':'black','elinewidth':1.0 })
314
+ d.update(kwargs)
315
+ return d
316
+
317
+ def bar_args( self, style = True, **kwargs ):
318
+ d = {'x' : self.binedges[:-1],
319
+ 'height': self.content,
320
+ 'width': self.binwidth,
321
+ 'align':'edge'
322
+ }
323
+ d.update(kwargs)
324
+ return d
325
+
326
+ def plot_hist( self, plt=None, axis=None, style=True, label=None,
327
+ show_errors=True, do_show = True, set_xlim = True ):
328
+ if not plt and not axis:
329
+ from .plot import _import_matplotlib_plt
330
+ plt = _import_matplotlib_plt()
331
+ if not axis:
332
+ axis = plt.gca()
333
+ axis.bar(**self.bar_args(label=label))
334
+ if show_errors:
335
+ ( error_markers,
336
+ ecaplines,
337
+ ebarlinecols ) = axis.errorbar(**self.errorbar_args())
338
+
339
+ xmin,xmax,binwidth = self.xmin, self.xmax, self.binwidth
340
+ if set_xlim:
341
+ axis.set_xlim(xmin-1e-6*binwidth,xmax+1e-6*binwidth)
342
+ if do_show and plt:
343
+ plt.show()
344
+
345
+ class Results:
346
+
347
+ def __init__( self,
348
+ main_hist_content,
349
+ main_hist_errsq,
350
+ json_details,
351
+ cfgstr,
352
+ setup_info ):
353
+
354
+ self.__setup_info = setup_info
355
+
356
+ mainhist_dict = dict( title = 'MAIN',
357
+ bindata = dict( content = main_hist_content,
358
+ errorsq = main_hist_errsq,
359
+ nbins = len(main_hist_content),
360
+ xmin = 0.0,
361
+ xmax = 180.0 ),
362
+ stats = None )
363
+
364
+ datadict = {}
365
+ if json_details is not None:
366
+ import json
367
+ datadict = json.loads(json_details)
368
+ mainhist_dict['stats'] = datadict.get('main_stats')
369
+
370
+ hists = [ Hist1D( mainhist_dict ) ]
371
+ hists += [ Hist1D( h )
372
+ for h in (datadict.get('breakdown_hists',[])) ]
373
+ self.__hists = hists
374
+ self.__cfgstr = cfgstr
375
+
376
+ @property
377
+ def histograms( self ):
378
+ return self.__hists
379
+
380
+ @property
381
+ def histogram_main( self ):
382
+ return self.__hists[0]
383
+
384
+ @property
385
+ def histogram_breakdown( self ):
386
+ return dict( (h.title,h) for h in self.__hists[1:] )
387
+
388
+ def histogram_sum( self, *, select=None, exclude=None ):
389
+ if isinstance(exclude,str):
390
+ exclude=[exclude]
391
+ if isinstance(select,str):
392
+ select=[select]
393
+ hl = self.__hists[1:]
394
+ if not exclude and not select:
395
+ return self.histogram_main
396
+ if select:
397
+ hl = [ h for h in hl if h.title in select ]
398
+ if exclude:
399
+ hl = [ h for h in hl if h.title not in exclude ]
400
+ if len(hl) <= 1:
401
+ return hl[0] if hl else None
402
+ h = hl[0].clone()
403
+ for o in hl[1:]:
404
+ h.add_contents( o )
405
+ return h
406
+
407
+ @property
408
+ def cfgstr( self ):
409
+ return self.__cfgstr
410
+
411
+ @property
412
+ def setup_info( self ):
413
+ return self.__setup_info
414
+
415
+ def plot_main( self, **kwargs ):
416
+ self.plot(hist=0,**kwargs)
417
+
418
+ def plot_breakdown( self, **kwargs ):
419
+ self.plot(hist='breakdown',**kwargs)
420
+
421
+ def plot( self, *,
422
+ do_show = True,
423
+ do_newfig = True,
424
+ do_grid = False,
425
+ logy = False,
426
+ rebin_factor = 1,
427
+ hist = None,
428
+ title = None,
429
+ plt = None):
430
+
431
+ breakdown_mode = hist=='breakdown'
432
+
433
+ if breakdown_mode:
434
+ hist = None
435
+ else:
436
+ if hist is None:
437
+ hist = self.__hists[0]
438
+ assert isinstance(hist,Hist1D)
439
+ elif not isinstance(hist,Hist1D):
440
+ #must be index:
441
+ hist = self.__hists[hist]
442
+ assert isinstance(hist,Hist1D)
443
+
444
+ from .plot import _import_matplotlib_plt, _plt_final
445
+ plt = plt or _import_matplotlib_plt()
446
+ if do_newfig:
447
+ plt.figure()
448
+
449
+ do_legend = False
450
+ tot_integral_with_absorption = None#unknown
451
+
452
+ if breakdown_mode:
453
+
454
+ colors = {
455
+ 'SINGLESCAT_ELAS' : 'blue',
456
+ 'SINGLESCAT_INELAS' : 'orangered',
457
+ 'MULTISCAT_PUREELAS' : 'cornflowerblue',
458
+ 'MULTISCAT_OTHER' : 'orange',
459
+ 'NOSCAT' : 'green',
460
+ }
461
+
462
+ do_legend = True
463
+ hists = [h for h in self.histograms if h.title!='MAIN']
464
+ hist_main = [h for h in self.histograms if h.title=='MAIN'][0]
465
+ if not hists:
466
+ from .exceptions import NCCalcError
467
+ raise NCCalcError('Can not produce breakdown plots'
468
+ ' without more detailed tally.')
469
+ _hists = [h for h in hists if h.stats.get('integral',1.0) > 0.0]
470
+ if _hists:
471
+ hists=_hists
472
+ #Sort histograms for (possibly) more meaningful plot order:
473
+ hists.sort( key = lambda h: ['MULTISCAT_OTHER',
474
+ 'SINGLESCAT_INELAS',
475
+ 'MULTISCAT_PUREELAS',
476
+ 'SINGLESCAT_ELAS',
477
+ 'NOSCAT'].index(h.title))
478
+
479
+ integrals = [ h.stats.get('integral',-1.0) for h in hists ]
480
+ tot_integral = ( sum(integrals)
481
+ if all( i>=0.0 for i in integrals )
482
+ else None )
483
+ if 'nstat' in self.setup_info:#TODO: we should always have this!
484
+ #A bit hackish way to get the initial flux... works only
485
+ #with the current unweighted generators and quick mode:
486
+ tot_integral_with_absorption = self.setup_info['nstat']
487
+
488
+ if rebin_factor != 1:
489
+ _ = []
490
+ for h in hists:
491
+ h = h.clone( rebin_factor = rebin_factor )
492
+ _.append( h )
493
+ hists = _
494
+ hist_main = hist_main.clone( rebin_factor = rebin_factor)
495
+
496
+ h=hists[0].clone()
497
+ curve = h._hist_curve()
498
+ axis = plt.gca()
499
+ ymax_non_NOSCAT = [0.0]
500
+ def _fractionval_fmt(x):
501
+ return f'({x*100.0:.3g}%)'
502
+ def _addplot( title, curve, refcurve = None, fraction = None):
503
+ maxval = 0.0
504
+ if refcurve is None:
505
+ y2 = _np.zeros(len(curve[1]))
506
+ else:
507
+ y2=refcurve[1]
508
+ maxval = y2.max()
509
+ if title!='NOSCAT':
510
+ ymax_non_NOSCAT[0] = max(ymax_non_NOSCAT[0],
511
+ maxval,
512
+ curve[1].max())
513
+ lbl = title if title!='NOSCAT' else 'Transmitted'
514
+ if fraction is not None:
515
+ lbl = f'{lbl} {_fractionval_fmt(fraction)}'
516
+ axis.fill_between(*curve,y2,
517
+ label=lbl,
518
+ edgecolor="none",
519
+ facecolor=colors[title])
520
+
521
+
522
+
523
+ def calcfrac(h):
524
+ ti = tot_integral
525
+ if tot_integral_with_absorption is not None:
526
+ ti = tot_integral_with_absorption
527
+ if ti is not None and ti > 0.0:
528
+ return h.stats['integral'] / ti
529
+
530
+ _addplot( h.title, curve, fraction = calcfrac(h) )
531
+
532
+ for i in range(1,len(hists)):
533
+ hi = hists[i]
534
+ h.add_contents(hi)
535
+ newcurve = h._hist_curve()
536
+ assert _np.array_equal(curve[0],newcurve[0])
537
+ _addplot(hi.title, newcurve, curve,
538
+ fraction = calcfrac(hi))
539
+ curve = newcurve
540
+
541
+ plt.gca().errorbar(**hist_main.errorbar_args())
542
+ if not logy:
543
+ plt.ylim(0.0,ymax_non_NOSCAT[0]*1.3 or None)
544
+ plt.xlim(0.0,180.0)
545
+ else:
546
+ if rebin_factor != 1:
547
+ hist = hist.clone( rebin_factor = rebin_factor )
548
+ hist.plot_hist(plt=plt,do_show = False)
549
+ if not title:
550
+ plt.title(hist.title or '<untitled histogram>')
551
+ if not logy:
552
+ plt.ylim(0.0)
553
+
554
+ plt.xticks(_np_linspace(0.0,180.0,180//30+1))
555
+ plt.xticks(_np_linspace(0.0,180.0,180//15+1),minor=True)
556
+ plt.xlabel('Exit Angle (degrees)')
557
+ plt.ylabel('Intensity (arbitrary units)')
558
+ if title:
559
+ plt.title(title)
560
+
561
+ suptitle_fs = 'medium'
562
+ if len(self.cfgstr)>40:
563
+ suptitle_fs = 'small'
564
+ if len(self.cfgstr)>80:
565
+ suptitle_fs = 'x-small'
566
+ if len(self.cfgstr)>101:
567
+ suptitle_fs = 'xx-small'
568
+ plt.suptitle(self.cfgstr,fontsize=suptitle_fs)
569
+
570
+ absfracstr = 'unknown fraction'
571
+ if tot_integral_with_absorption and tot_integral:
572
+ _absfrac = 1.0 - tot_integral/tot_integral_with_absorption
573
+ absfracstr = _fractionval_fmt(_absfrac)
574
+ plt.plot([], [], ' ', label="Absorbed %s"%absfracstr)
575
+
576
+ legargs = {}
577
+
578
+ _plt_final(do_grid = do_grid,
579
+ do_legend = do_legend,
580
+ do_show = do_show,
581
+ logx = False,
582
+ logy = logy,
583
+ plt = plt,
584
+ extra_legend_kwargs = legargs )
585
+
586
+ return Results( main_hist_content = ct,
587
+ main_hist_errsq = errsq,
588
+ json_details = res_json,
589
+ cfgstr = cfgstr,
590
+ setup_info = setup_info)
591
+
592
+ _length_units = {'km':1000.0,
593
+ 'm':1.0,
594
+ 'meter':1.0,
595
+ 'cm':0.01,
596
+ 'mm':0.001,
597
+ 'mfp': None,#special
598
+ 'nm':1e-9,
599
+ 'aa':1e-10,
600
+ 'Aa':1e-10,
601
+ 'AA':1e-10,
602
+ 'angstrom':1e-10}
603
+
604
+ _energy_units = {'eV':1.0,
605
+ 'keV':1e3,
606
+ 'MeV':1e6,
607
+ 'GeV':1e9,
608
+ 'meV':0.001,
609
+ 'neV':1e-9,
610
+ 'aa':None,
611
+ 'Aa':None,
612
+ 'AA':None,
613
+ 'angstrom':None}
614
+
615
+ def _tofloat(s):
616
+ try:
617
+ return float(s)
618
+ except ValueError:
619
+ return None
620
+
621
+ def _parse_unit(valstr,unitmap):
622
+ v = _tofloat(valstr)
623
+ if v is not None:
624
+ return v, None, None
625
+ valstr=valstr.strip()
626
+ for unit,unitvalue in sorted(unitmap.items(),key=lambda x : (-len(x),x)):
627
+ if valstr.endswith(unit):
628
+ v = _tofloat(valstr[:-len(unit)])
629
+ if v is not None:
630
+ return v, unit, unitvalue
631
+ return None,None,None
632
+
633
+ def _parse_energy( valstr ):
634
+ v,u,uv = _parse_unit( valstr, _energy_units )
635
+ if v is not None and u is None and uv is None:
636
+ from .exceptions import NCBadInput
637
+ raise NCBadInput('Invalid energy specification (missing unit'
638
+ f' like Aa or eV): "{valstr}"')
639
+ if v is None:
640
+ from .exceptions import NCBadInput
641
+ raise NCBadInput(f'Invalid energy specification: "{valstr}"')
642
+ if u is not None and u.lower() in ('aa','angstrom'):
643
+ from .constants import wl2ekin
644
+ return wl2ekin(v)
645
+ v *= uv
646
+ return v
647
+
648
+ def _parse_length( valstr, mfp = None ):
649
+ v,u,uv = _parse_unit( valstr, _length_units )
650
+ if v is not None and u is None and uv is None:
651
+ from .exceptions import NCBadInput
652
+ _ex0="mfp" if mfp is not None else "mm"
653
+ raise NCBadInput('Invalid length specification (missing unit like '
654
+ f'{_ex0} or cm): "{valstr}"')
655
+ if v is None:
656
+ from .exceptions import NCBadInput
657
+ raise NCBadInput(f'Invalid length specification: "{valstr}"')
658
+ if u=='mfp':
659
+ if mfp is None:
660
+ raise ValueError('Invalid length specification ("mfp" '
661
+ f'not supported for this parameter): "{valstr}"')
662
+ v *= mfp
663
+ else:
664
+ v *= uv
665
+ return v
666
+
667
+ def _macroxs_if_isotropic( mat, **xsect_kwargs ):
668
+ #macroxs_scatter in units of [1/m]
669
+ return ( None if mat.scatter.isOriented()
670
+ else ( mat.info.factor_macroscopic_xs
671
+ * mat.scatter.xsect( **xsect_kwargs )
672
+ / _parse_length('1cm') ) )
673
+
674
+ def quick_diffraction_pattern_autoparams( cfgstr ):
675
+ from .core import load as ncload
676
+ mat = ncload( cfgstr )
677
+
678
+ neutron_wl = 1.8 # todo: depend on e.g. Bragg threshold?
679
+
680
+ unit_mm = _parse_length('1mm')
681
+ unit_cm = _parse_length('1cm')
682
+ unit_m = _parse_length('1m')
683
+ assert unit_m == 1.0
684
+ macroxs_scatter = _macroxs_if_isotropic( mat, wl=neutron_wl )
685
+ if not macroxs_scatter:
686
+ material_thickness = '1cm'
687
+ else:
688
+ mfp_scatter = 1.0 / macroxs_scatter
689
+ def _round2digits(x):
690
+ return int(x*100+0.5) * 0.01
691
+ if mfp_scatter <= unit_cm:
692
+ material_thickness = f'{_round2digits(mfp_scatter/unit_mm):g}mm'
693
+ elif mfp_scatter <= unit_m:
694
+ material_thickness = f'{_round2digits(mfp_scatter/unit_cm):g}cm'
695
+ else:
696
+ material_thickness = f'{_round2digits(mfp_scatter/unit_m):g}m'
697
+ return dict( neutron_energy_str = f'{neutron_wl}Aa',
698
+ material_thickness_str = material_thickness )
699
+
700
+ def quick_diffraction_pattern( cfgstr, *,
701
+ neutron_energy,
702
+ material_thickness,
703
+ nstat = 'auto',
704
+ nthreads = 'auto' ):
705
+
706
+ ### TODO: change c-api and use:
707
+ ### from .misc import MaterialSource
708
+ ### matsrc = MaterialSource(material)
709
+ ### if matsrc.is_preloaded():
710
+ ### #from .exceptions import NCBadInput
711
+ ### raise NCBadInput( 'Diffraction patterns can not be produced for'
712
+ ### ' preloaded materials (for instance simply passing'
713
+ ### ' a cfgstring will work).' )
714
+ ###
715
+
716
+ neutron_energy_eV = _parse_energy( neutron_energy )
717
+
718
+ from .core import load as ncload
719
+ mat = ncload( cfgstr )
720
+ #unit_cm = _parse_length('1cm')
721
+ unit_m = _parse_length('1m')
722
+
723
+ macroxs_scatter = _macroxs_if_isotropic( mat, ekin=neutron_energy_eV )
724
+ mfp_scatter = 1/macroxs_scatter if macroxs_scatter else float('inf')
725
+
726
+ sphere_diameter = _parse_length(material_thickness, mfp=mfp_scatter)
727
+
728
+ def simfct( n, cfgstr ):
729
+ import time
730
+ t0 = time.time()
731
+ r_meter = 0.5*sphere_diameter / unit_m
732
+ res = runsim_diffraction_pattern( cfgstr,
733
+ geomcfg = f'sphere;r={r_meter}',
734
+ srccfg = (f'constant;ekin={neutron_energy_eV}'
735
+ f';n={n};z={-r_meter*(1-1e-13)}'),
736
+ tally_detail_lvl = 2,
737
+ nthreads = nthreads
738
+ )
739
+ t1 = time.time()
740
+ return t1-t0, res
741
+
742
+ if nstat is None or nstat=='auto':
743
+ #simfct(1,cfgstr)#build mat cache
744
+ for nstat in [1e4,1e5,1e6,1e7]:
745
+ t,res = simfct(nstat,cfgstr)
746
+ #Usually, end within a second in total, but in worst cases, up to
747
+ #10seconds:
748
+ if t>0.1 and nstat >= 1e6:
749
+ break
750
+ if t>1.0:
751
+ break
752
+ else:
753
+ t,res=simfct(nstat,cfgstr)
754
+
755
+ res.setup_info['nstat'] = nstat
756
+
757
+ return res