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
@@ -0,0 +1,503 @@
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
+ from ._cliimpl import ( create_ArgumentParser,
23
+ cli_entry_point,
24
+ print,
25
+ warn )
26
+
27
+ from . import constants as nc_constants
28
+ from . import core as nccore
29
+
30
+ import warnings
31
+ import pathlib
32
+
33
+ __pynecache=[None]
34
+ def import_pyne():
35
+ global __pynecache
36
+ if __pynecache[0] is not None:
37
+ return __pynecache[0]
38
+
39
+ try:
40
+ with warnings.catch_warnings():
41
+ warnings.simplefilter("ignore")#silence annoying pyne.endf QAWarning's.
42
+ import pyne.endf
43
+ except ImportError:
44
+ raise RuntimeError('Could not "import pyne.endf". You probably need to install PyNE (see http://pyne.io/)')
45
+
46
+ #Monkey-patch pyne.endf reader, which was ignoring optional sections with teff
47
+ #curves of non-principal atoms (see
48
+ #https://github.com/pyne/pyne/issues/1209). The fix was accepted upstream, so
49
+ #eventually we can remove this monkey-patching once we think all users will have
50
+ #recent enough PyNE installations:
51
+ _orig_rti = pyne.endf.Evaluation._read_thermal_inelastic
52
+ def _fix_rti(_self):
53
+ _orig_rti(_self)
54
+ inel=_self.thermal_inelastic
55
+ B=inel['B']
56
+ for ii in range(len(B)//6-1):
57
+ if B[6*(ii+1)]==0.0:
58
+ if 'teff_%i'%(ii+1) in inel:
59
+ #If upstream already includes fix, do nothing.
60
+ continue
61
+ _, teff = _self._get_tab1_record()
62
+ inel['teff_%i'%(ii+1)] = teff#Store as teff_1, teff_2, etc. for now.
63
+ pyne.endf.Evaluation._read_thermal_inelastic=_fix_rti
64
+ __pynecache[0] = pyne
65
+ return pyne
66
+
67
+ def elementZToName(z):
68
+ d = nccore.atomDB(z,throwOnErrors=False)
69
+ return d.elementName() if d else None
70
+
71
+ def guessElementFromMass(mass_amu):
72
+ elem_data=[ (abs(d.averageMassAMU()-mass_amu),d) for d in [nccore.atomDB(z,throwOnErrors=False) for z in range(1,120)] if d ]
73
+ elem_data.sort()
74
+ print (f'Guessing mass={mass_amu:.8}u is element {elem_data[0][1].elementName()}')
75
+ return elem_data[0][1].elementName()
76
+
77
+ def guessElementName(ZA,mass_amu):
78
+ Z=ZA//1000
79
+ if ZA==155:
80
+ return 'Y'#YH2, whatever it is
81
+ if ZA==140:
82
+ return 'H'#benzene... C6H6. Guessing H is dominant. But tsl-benzene.endf is actually a compound SAB...
83
+ if ZA==1002:
84
+ return 'D'
85
+ if ZA in (133,134):
86
+ return 'H'#solid or liquid methane (CH4 happens to have mass 12+1*4=16 close to oxygen, so can't guess from mass)
87
+ if ZA in (112,113):
88
+ return 'D'#ortho-D or para-D
89
+ guess_by_mass = guessElementFromMass(mass_amu)
90
+ guess_by_z = elementZToName(Z) if (Z>0 and Z<120) else None
91
+ if guess_by_z:
92
+ if guess_by_z!=guess_by_mass:
93
+ raise RuntimeError(f"Mass and Z guess gives different element name (Z={Z}=>'{guess_by_z}', mass={mass_amu}amu=>'{guess_by_mass}')")
94
+ return guess_by_z
95
+ if Z==0:
96
+ return guess_by_mass
97
+ raise ValueError(f'Could not determine element name from ZA={ZA}, mass={mass_amu}amu')
98
+
99
+ #Custom bare-bones reader, so we can extract the data header as-is in the file:
100
+ def parse_endf6(src,name=None,selectfct=None):
101
+ """Parse TSL data from ENDF-6 file according to ENDF manual. Select function can
102
+ used to determine which mat,mf,mt lines to read. E.g. "selectfct=lambda
103
+ mat,mf,mt:mf==7". Remember to select (mf,mt)=(1,451) for the description at
104
+ the start of each material.
105
+
106
+ """
107
+ if isinstance(src,str) or isinstance(src,bytes) or hasattr(src,'__fspath__'):
108
+ p=pathlib.Path(src).expanduser()
109
+ with p.open('rt') as srcstream:
110
+ return parse_endf6(srcstream,name=p.name)
111
+ #according to the manual section 0.6, each line is broken down as follows:
112
+ #Columns 1-66 content, 67-70 MAT number, 71-72 MF, 73-75 MT, 76-80 [optional] NS, 81+ undefined
113
+ #Content can either be one 66 char long text field, or there are 6 fields of 11 chars.
114
+
115
+ errprefix = '' if not name else f'in {name}'
116
+ def require(check,errmsg):
117
+ if not check:
118
+ raise ValueError(f'{errprefix}: {errmsg}')
119
+
120
+ def parse_endf6_line(line):
121
+ require(len(line)>=75,"line too short")
122
+ content=line[0:66]
123
+ mat=int(line[66:70])
124
+ mf=int(line[70:72])
125
+ mt=int(line[72:75])
126
+ return (content,mat,mf,mt)
127
+
128
+ content,mat,mf,mt = parse_endf6_line(next(src))
129
+ require((mat,mf,mt)==(1,0,0),'Not an ENDF-6 file?')
130
+ global_header=content
131
+ d={}#material -> material info
132
+ for line in src:
133
+ #print(l)
134
+ content,mat,mf,mt = parse_endf6_line(line)
135
+ if mat<1:
136
+ continue#Ignore dummy lines (section dividers, etc.)
137
+ if selectfct and not selectfct(mat,mf,mt):
138
+ continue
139
+ if mat not in d:
140
+ d[mat]=dict()
141
+ dd=d[mat]
142
+ if mf not in dd:
143
+ dd[mf]={}
144
+ ddd=dd[mf]
145
+ if mt not in ddd:
146
+ ddd[mt]=[content]
147
+ else:
148
+ ddd[mt]+=[content]
149
+ return dict(global_header=global_header,name=name),d
150
+
151
+ def extract_and_format_endf_file_header(filename):
152
+ custom_parse = parse_endf6(filename,selectfct=lambda mat,mf,mt : (mf,mt)==(1,451))
153
+ mats=custom_parse[1]
154
+ assert len(mats)==1,("File contains more than one material number which"
155
+ " is not supported by the present script (please"
156
+ " contact NCrystal developers if you really"
157
+ " need this).")
158
+ material_number,material_info = next(iter(mats.items()))
159
+ return dict( filename = custom_parse[0]['name'],
160
+ material_number = material_number,
161
+ global_header = custom_parse[0]['global_header'],
162
+ infosection=material_info[1][451] )
163
+
164
+ def parse_endf_file(filename):
165
+ print(f"Attempting to load ENDF file {filename}...")
166
+ result={}
167
+ warnings_=[]
168
+ pyne = import_pyne()
169
+ data=pyne.endf.Evaluation(filename)
170
+ data.read()
171
+ result['input_file']=pathlib.Path(filename)
172
+ result['pynedata']=data
173
+ print('Performing a few sanity checks...')
174
+ if not hasattr(data,'thermal_inelastic') or 'B' not in data.thermal_inelastic or 'scattering_law' not in data.thermal_inelastic:
175
+ raise RuntimeError('File does not appear to contain any inelastic neutron scattering kernel.')
176
+
177
+ ti=data.thermal_inelastic
178
+
179
+ if ti['ln(S)']:
180
+ raise RuntimeError('The ln(S) flag is enabled. For the reasons given in the ENDF manual just before'
181
+ 'section 7.4.1, it is not safe to simple convert these to S-values. We need ncmat to support a logsab_scaled option first.'
182
+ ' Another (easier?) alternative would be to use mpmath module here and calculate unscaled S-values and produce an ncmat file with S=...')
183
+
184
+ assert ti['temperature_used'] in ('actual','0.0253 eV')
185
+ if ti['temperature_used'].strip()=='actual':
186
+ alphabetagrid_T0=None
187
+ else:
188
+ alphabetagrid_T0 = float(ti['temperature_used'].split('eV',1)[0].strip())/nc_constants.constant_boltzmann#Must stretch betagrid values relative to this
189
+
190
+ #print(data.target)
191
+ element_name=guessElementName(data.target['ZA'],data.target['mass'])
192
+ zsymam=data.target['zsymam'].strip()
193
+ if zsymam not in ['s-ch4','l-ch4','ortho-d','para-d','BENZINE'] and 'graphit' not in zsymam.lower():
194
+ assert element_name in zsymam,f"{element_name} not in {zsymam}"
195
+ result['element_name_principal']=element_name
196
+
197
+ lenB = len(ti['B'])
198
+ B = [None] + list(ti['B'])#B-array, with fortran indexing (for easier reference with ENDF manual):
199
+ A0 = float(B[3]) #Needed to unscale
200
+ if B[1]==0.0:
201
+ raise RuntimeError('Material has no principal scatterers - thus no S(alpha,beta)')
202
+ suggested_emax = B[4]#In principle... but in practice not sure if this holds! Also, data.info['energy_max'] gives a different number (5.0)?!?
203
+ count_principal = B[6]
204
+ num_non_principal = ti['num_non_principal']
205
+
206
+ non_principal_data = []
207
+ for i in range(num_non_principal):
208
+ offset = 6*(i+1)
209
+ assert lenB >= 6*(i+2)
210
+ a1=B[offset+1]#0.0: SCT (short collision time approx), 1.0: free-gas, 2.0: diffusive motion.
211
+ if a1!=1.0:
212
+ warnings_+=['WARNING: A non-free-gas model is proposed for the first non-principal atom']
213
+ warnings_+=[' in the original file. This is currently not handled by NCrystal.']
214
+ print(warnings_[-2])
215
+ print(warnings_[-1])
216
+ effective_mass = B[offset+3]#mass in units of neutron mass
217
+ count = B[offset+6]#count per molecule or unit cell
218
+ theElementName = guessElementFromMass(effective_mass*nc_constants.const_neutron_mass_amu)
219
+ non_principal_data+=[[theElementName,count,effective_mass]]
220
+
221
+ count_total = count_principal + sum(count_ for elemname,count_,effmass in non_principal_data)
222
+ def format_fraction(c,ctot):
223
+ return f'{c:.14g}/{ctot:.14g}' if c!=ctot else '1'
224
+ for e in non_principal_data:
225
+ elementName,thecount,effective_mass=e
226
+ e += [format_fraction(thecount,count_total)]
227
+
228
+ header = extract_and_format_endf_file_header(filename)
229
+ result['header']=header
230
+ result['warnings']=warnings_
231
+ result['non_principal_data']=non_principal_data
232
+ result['count_total']=count_total
233
+ result['count_principal']=count_principal
234
+ result['fraction_str_principal']=format_fraction(count_principal,count_total)
235
+ result['suggested_emax']=suggested_emax
236
+
237
+ ndatablock = len(ti['teff'].x)
238
+ result_datablocks=[]
239
+ result['result_datablocks'] = result_datablocks
240
+ for idatablock in range(ndatablock):
241
+ result_datablock={}
242
+ result_datablocks += [result_datablock]
243
+ temperature=ti['teff'].x[idatablock]
244
+ temperature_effective=ti['teff'].y[idatablock]
245
+ result_datablock['T']=temperature
246
+ result_datablock['Teff']=temperature_effective
247
+ sabt=ti['scattering_law'][:,:,idatablock] #indexing t,a,b
248
+ sab=sabt.T
249
+ result_datablock['sab']=sab
250
+ #We must .copy() alpha/beta grids due to '*=' operators below:
251
+ alphagrid = ti['alpha'].copy()*A0#undo funny ENDF alpha definition which divides by A0
252
+ betagrid = ti['beta'].copy()
253
+ if alphabetagrid_T0 is not None:
254
+ #Undo funny ENDF scaling of grids at each temperature
255
+ betagrid *= (alphabetagrid_T0/temperature)
256
+ alphagrid *= (alphabetagrid_T0/temperature)
257
+ result_datablock['alphagrid']=alphagrid.copy()
258
+ result_datablock['betagrid']=betagrid.copy()
259
+
260
+ assert ti['symmetric'] == bool(betagrid[0]>=0.0),"inconsistencies detected"
261
+
262
+ return result
263
+
264
+ def format_endf_block_as_ncmatdyninfo_for_principal_element(parsed_endf_data,temperature,fraction_str=None):
265
+ elem_name = parsed_endf_data["element_name_principal"]
266
+
267
+ #Find block by temperature:
268
+ temperature_exact,block_idx = list(sorted((abs(temperature-_["T"]),_["T"],idx) for idx,_ in enumerate(parsed_endf_data['result_datablocks'])))[0][1:]
269
+ assert abs(temperature_exact-temperature)<1e-6
270
+ temperature=temperature_exact
271
+
272
+ res = '@DYNINFO\n'
273
+ res += f" # Scattering kernel for {elem_name} @ {temperature}K extracted from {parsed_endf_data['input_file'].name}\n"
274
+ for w in parsed_endf_data['warnings']:
275
+ res += f'\n # ----> {w}'
276
+ res += ' #\n'
277
+ res += ' # For reference the description embedded in input ENDF file (section MF1,MT451) is repeated here:\n'
278
+ res += ' #\n'
279
+ header=parsed_endf_data['header']
280
+ for line in [header['global_header']]+header['infosection']:
281
+ res+=f' # --->{(" "+line).rstrip()}\n'
282
+
283
+ datablock = parsed_endf_data['result_datablocks'][block_idx]
284
+ res+=f' element {elem_name}\n'
285
+ if fraction_str is None:
286
+ fraction_str = parsed_endf_data["fraction_str_principal"]
287
+ else:
288
+ assert parsed_endf_data["fraction_str_principal"]=='1'
289
+ res+=f' fraction {fraction_str}\n'
290
+ res +=' type scatknl\n'
291
+ res+=f' temperature {temperature:.10} #NB: ENDF file specified "effective temperature" as {datablock["Teff"]:.10}K\n'
292
+ #NB: Not suggesting an emax value, since it seems to be unreliable! Same for B[4] from above...
293
+ #res+=f" egrid {parsed_endf_data['pynedata'].info['energy_max']}#Value (in eV) as specified in source ENDF file.\n"#argh... e.g. D2O@300K shows this can't be trusted!!!
294
+ res += nccore.formatVectorForNCMAT('alphagrid',datablock['alphagrid'])
295
+ res += nccore.formatVectorForNCMAT('betagrid',datablock['betagrid'])
296
+ res += nccore.formatVectorForNCMAT('sab_scaled',datablock['sab'])
297
+ return res
298
+
299
+ def parseArgs( progname, arglist, return_parser=False ):
300
+ descr="""
301
+ Converts neutron scattering kernels ("thermal scattering laws, tsl") found in
302
+ ENDF files to NCMAT format (specifically @DYNINFO sections of NCMAT files).
303
+
304
+ Note that this script exists for the benefit of experts only. Most users are
305
+ recommended to simply download and use one of the pre-converted files shipped
306
+ with NCrystal or found at:
307
+
308
+ https://github.com/mctools/ncrystal-extra/tree/master/data
309
+
310
+ Only information for NCMAT @DYNINFO sections are extracted from ENDF files, so
311
+ it is recommended to provide the initial parts of the target NCMAT file in a
312
+ separate file specified via the --filehdr parameter. That file should include
313
+ both initial comments for the file as well as either @DENSITY or
314
+ @CELL/@SPACEGROUP/@ATOMPOSITIONS/@DEBYETEMPERATUE sections, depending on whether
315
+ or not the material is crystalline. Note that comments describing the origin of
316
+ the @DYNINFO sections extracted from the ENDF files will be inserted in the
317
+ relevant @DYNINFO sections. The --filehdr should include the string
318
+ '<<STDNOTICE>>' on a separate line, which will be expanded to a notice about
319
+ availability of files for other temperatures.
320
+
321
+ If the --filehdr argument is not provided, a dummy header will be
322
+ automatically generated, with a fake @DENSITY section, using the density of
323
+ water (1g/cm3). This allows the file to be technically valid, but will of
324
+ course result in incorrect physics in case the file is used for simulations.
325
+
326
+ As each ENDF file contains a list of temperatures for which scattering kernels
327
+ are available, one NCMAT file will be generated for each such temperature. The
328
+ resulting filename will be <basename>_T<temperature>K.ncmat, where <basename>
329
+ will default to the input filename unless --outbn is provided.
330
+
331
+ The script internally relies on the Python Nuclear Engineering Toolkit
332
+ (http://pyne.io/) for ENDF parsing, so this must be installed on the system.
333
+
334
+ Input ENDF files can for instance be downloaded from:
335
+
336
+ https://www.nndc.bnl.gov/endf-b8.0/download.html
337
+
338
+ (download and open zip-file from the "Thermal Neutron Scattering Sublibrary").
339
+
340
+ """
341
+
342
+ import textwrap
343
+ from argparse import RawTextHelpFormatter
344
+
345
+ def wrap(t):
346
+ return textwrap.fill(t,width=60)
347
+
348
+ parser = create_ArgumentParser( prog = progname,
349
+ description=descr,
350
+ formatter_class=RawTextHelpFormatter)
351
+ parser.add_argument('ENDFFILE',type=str,
352
+ help="Primary ENDF file with tsl data.")
353
+ parser.add_argument('--secondary',metavar='ENDFFILE2:fraction',
354
+ type=str,
355
+ help=wrap("ENDF file for secondary element, "
356
+ "along with the fraction of that element"))
357
+ parser.add_argument("--filehdr",type=str,
358
+ help=wrap("File containing initial part"
359
+ " of generated files."))
360
+ parser.add_argument("--outbn",type=str,
361
+ help=wrap("Basename of generated ncmat files."))
362
+ parser.add_argument("--ignoretemp",type=float,nargs='+',metavar='T',
363
+ help=wrap("If some temperature blocks in input should"
364
+ " be ignored, provide the temperature values"
365
+ " here (kelvin)."))
366
+
367
+ def to_path(parser,fn):
368
+ _ = pathlib.Path(fn)
369
+ if not _.exists():
370
+ parser.error(f'File not found: {_}')
371
+ return _
372
+
373
+
374
+ if return_parser:
375
+ return parser
376
+
377
+ args = parser.parse_args(arglist)
378
+
379
+ args.ENDFFILE = to_path(parser,args.ENDFFILE)
380
+ args.fraction1='1'
381
+ args.fraction2=None
382
+ if not args.secondary:
383
+ args.ENDFFILE2=None
384
+ else:
385
+ if not args.secondary.count(':')==1:
386
+ parser.error('Argument to --secondary must contain exactly one semicolon (:)')
387
+ sn,fr2 = args.secondary.split(':',1)
388
+ args.secondary = True
389
+ if fr2.count('/')==1:
390
+ a,b = [int(e) for e in fr2.split('/')]
391
+ if b<=0 or not ( 0 < a < b ):
392
+ parser.error('Invalid fraction specified in --secondary')
393
+ args.fraction1=f'{b-a}/{b}'
394
+ args.fraction2=f'{a}/{b}'
395
+ else:
396
+ args.fraction2=fr2
397
+ if not ( 0.0 < float(fr2) < 1.0):
398
+ parser.error('Invalid fraction specified in --secondary')
399
+ args.fraction1=f'{1.0-float(fr2):10}'
400
+ args.ENDFFILE2 = to_path(parser,sn)
401
+ if args.filehdr:
402
+ _filehdrtxt=to_path(parser,args.filehdr).read_text()
403
+ else:
404
+ _filehdrtxt=f"""
405
+ NCMAT v2
406
+ #
407
+ # Material with scattering kernel extracted from ENDF file.
408
+ #
409
+ <<STDNOTICE>>
410
+ #
411
+ @DENSITY
412
+ 1 g_per_cm3 #FIX{""}ME. Dummy number!!! (update or add unit cell sections and then remove @DENSITY section)
413
+ """
414
+ args.filehdr = [line.rstrip() for line in _filehdrtxt.strip().splitlines()]
415
+
416
+ if not any('<<STDNOTICE>>' in line for line in args.filehdr):
417
+ parser.error('ERROR: File specified with --filehdr should contain the string "<<STDNOTICE>>" on a separate line (just before the first data section)')
418
+
419
+ if not args.outbn:
420
+ if args.ENDFFILE2:
421
+ parser.error('--outbn is required when providing two input files')
422
+ else:
423
+ args.outbn=args.ENDFFILE.name
424
+ if args.outbn.endswith('.endf'):
425
+ args.outbn=args.outbn[0:-5]
426
+ assert args.outbn
427
+
428
+ return args
429
+
430
+ def create_argparser_for_sphinx( progname ):
431
+ return parseArgs(progname,[],return_parser=True)
432
+
433
+ @cli_entry_point
434
+ def main( progname, arglist ):
435
+ args = parseArgs( progname, arglist )
436
+
437
+ p1=parse_endf_file(args.ENDFFILE)
438
+ p2=parse_endf_file(args.ENDFFILE2) if args.ENDFFILE2 else None
439
+
440
+ temperatures1 = [ _["T"] for _ in p1['result_datablocks'] ]
441
+ if p2:
442
+ temperatures2 = [ _["T"] for _ in p2['result_datablocks'] ]
443
+ temperatures_combined = [ t1 for t1 in temperatures1 if min(abs(t1-t2) for t2 in temperatures2)<1e-5 ]
444
+ if len(temperatures_combined)<len(temperatures1):
445
+ warn(f"{len(temperatures1)-len(temperatures_combined)} temperature blocks in {args.ENDFFILE} could not be used due to missing data in other file!")
446
+ if len(temperatures_combined)<len(temperatures2):
447
+ warn(f"{len(temperatures2)-len(temperatures_combined)} temperature blocks in {args.ENDFFILE} could not be used due to missing data in other file!")
448
+ else:
449
+ temperatures_combined = temperatures1
450
+
451
+ temperatures_combined=sorted(list(set(temperatures_combined)))
452
+ for t in (args.ignoretemp or []):
453
+ removed=False
454
+ for _ in temperatures_combined:
455
+ if abs(t-_)<1e-6:
456
+ temperatures_combined.remove(_)
457
+ print (f"Ignoring T={_}K as requested")
458
+ removed=True
459
+ break
460
+ if not removed:
461
+ raise RuntimeError(f'Asked to ignore T={t}K, but no such temperature was found in the input')
462
+
463
+
464
+ if p2 and (p1['non_principal_data'] or p2['non_principal_data']):
465
+ raise RuntimeError("When providing two ENDF files as input both must files must be without non-principal"
466
+ " elements! If you know how to handle this, You can try to convert separately and combine the"
467
+ " resulting .ncmat files manually.")
468
+
469
+ for t in temperatures_combined:
470
+ fn=pathlib.Path(f'{args.outbn}_T{t}K.ncmat')
471
+ with fn.open('wt') as fh:
472
+ print(f' -> Writing {fn}')
473
+ fh.write('NCMAT v2\n')
474
+ stdnotice = f'#\n# Notice: This NCMAT file is valid at T={t}K only.'
475
+ if len(temperatures_combined)>1:
476
+ stdnotice+=' Other files alternatively provide\n'
477
+ stdnotice+='# the same material at temperatures:\n#\n'
478
+ nperline=7
479
+ t_to_write=list(_ for _ in temperatures_combined if _!=t)
480
+ for i in range(0,len(t_to_write),nperline):
481
+ stdnotice += ('# '+' '.join((f'{_}K' for _ in t_to_write[i:i+nperline]))+'\n')
482
+ for line in args.filehdr:
483
+ if line.startswith('NCMAT '):
484
+ continue
485
+ if stdnotice and '<<STDNOTICE>>' in line:
486
+ line=line.replace('<<STDNOTICE>>',stdnotice)
487
+ stdnotice=''
488
+ fh.write(line if line.endswith('\n') else f'{line}\n')
489
+ if stdnotice:
490
+ fh.write(stdnotice)
491
+ for elementName,count_,effmass,fraction_str in p1['non_principal_data']:
492
+ assert not p2
493
+ fh.write('@DYNINFO\n')
494
+ fh.write(f' element {elementName}\n')
495
+ fh.write(f' fraction {fraction_str}\n')
496
+ fh.write(' type freegas\n')
497
+ fh.write(format_endf_block_as_ncmatdyninfo_for_principal_element(p1,t,
498
+ args.fraction1 if p2 else None))
499
+ if p2:
500
+ fh.write(format_endf_block_as_ncmatdyninfo_for_principal_element(p2,t,args.fraction2))
501
+ print(' -> Testing that NCrystal can load this file')
502
+ nccore.createScatter(f'{fn};dcutoff=0.8')
503
+ print("All done.")
@@ -0,0 +1,144 @@
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
+ from ._cliimpl import ( create_ArgumentParser,
23
+ cli_entry_point,
24
+ print )
25
+
26
+ def _parseArgs( default_debye_temp, progname, arglist, return_parser=False ):
27
+ from argparse import RawTextHelpFormatter
28
+
29
+ fdt = default_debye_temp
30
+ #NOTE: Keep the description below synchronised with doc-string of the
31
+ #hfg2ncmat python API function:
32
+ descr=f"""
33
+
34
+ Script which can be used to generate NCMAT files for hydrogen-rich amorphous
35
+ materials, in which the hydrogen atoms are bound to certain standard functional
36
+ groups (e.g. carbohydrates, polyimides, polymers, ...). Based on the material's
37
+ density, (empirical) chemical formula, and the specification of hydrogen
38
+ bindings in terms of standard functional groups, an NCMAT file is generated. In
39
+ this NCMAT file, non-hydrogen atoms are treated with a simplistic model
40
+ (idealised Debye model of phonon vibrations, assuming a Debye temperature of
41
+ {fdt}K for all atoms unless --debyetemp is specified), and the hydrogen atoms
42
+ are treated with a proper phonon density of state (VDOS) curve, which is
43
+ constructed based on the provided binding specifications. This is done using an
44
+ idea (and VDOS curves from) the following publication:
45
+
46
+ "Thermal neutron cross sections of amino acids from average contributions
47
+ of functional groups", G. Romanelli, et. al., J. Phys.: Condens. Matter,
48
+ (2021). doi:10.1088/1361-648X/abfc13
49
+
50
+ Example of valid spec strings for the --spec parameter. Note only the hydrogen
51
+ bindings are important here:
52
+
53
+ "1xCHali+2xCH2+8xCHaro+2xCH3+1xOH" (20 hydrogen atoms)
54
+ "1xNH+3xSH+2xCH2" (8 hydrogen atoms).
55
+
56
+ List of valid bindings which can be used in --spec:
57
+
58
+ CHali (1 hydrogen atom, aliphatic binding)
59
+ CHaro (1 hydrogen atom, aromatic binding, e.g. on phenyl group).
60
+ CH2 (2 hydrogen atoms)
61
+ CH3 (3 hydrogen atoms)
62
+ NH (1 hydrogen atom)
63
+ NH2 (2 hydrogen atoms)
64
+ NH3 (3 hydrogen atoms)
65
+ OH (1 hydrogen atom)
66
+ SH (1 hydrogen atom)
67
+
68
+ Note that as only hydrogen atoms will have a realistic VDOS curve, and as the
69
+ incoherent approximation is employed, the realism of the resulting material
70
+ modelling will be higher for materials with more hydrogen atoms. As a metric of
71
+ this, the script prints out how much of the total scattering cross section (sum
72
+ of sigma_coh+sigma_incoh for all atoms), is due to the sigma_incoh contribution
73
+ from hydrogen atoms.
74
+
75
+ """
76
+ parser = create_ArgumentParser(prog = progname,
77
+ description=descr,
78
+ formatter_class=RawTextHelpFormatter)
79
+ parser.add_argument("--output",'-o',default='autogen.ncmat',type=str,
80
+ help=("Output file name (defaults to autogen.ncmat)."
81
+ " Can be stdout."))
82
+ parser.add_argument('--force',action='store_true',
83
+ help=("Will overwrite existing file "
84
+ "if it already exists."))
85
+ parser.add_argument("--spec",'-s',metavar='SPEC',type=str,required=True,
86
+ help="Hydrogen binding specification (see above).")
87
+ parser.add_argument("--formula",'-f',metavar='FORMULA',
88
+ type=str,
89
+ required=True,
90
+ help=("Chemical formula such as C20H18O3 (only"
91
+ " the relative ratios matter)."))
92
+ parser.add_argument("--density",'-d',metavar='DENSITY',
93
+ type=float,
94
+ required=True,
95
+ help="Material density in g/cm3.")
96
+ parser.add_argument("--debyetemp",metavar='VALUE',
97
+ type=float,
98
+ default=default_debye_temp,
99
+ help=("Debye temperature (kelvin) of non-hydrogen"
100
+ f" atoms (default is {default_debye_temp})."))
101
+ parser.add_argument("--title",type=str,required=True,
102
+ help=('Title string of material (will be placed as'
103
+ ' comment near top of output file). Use \\n '
104
+ 'for line-breaks.'))
105
+ parser.add_argument('--notrim',action='store_true',
106
+ help="No trimming of resulting VDOS curve.")
107
+ if return_parser:
108
+ return parser
109
+ args=parser.parse_args(arglist)
110
+ return args
111
+
112
+ def create_argparser_for_sphinx( progname ):
113
+ from .hfg2ncmat import _default_debye_temp
114
+ return _parseArgs(_default_debye_temp(),progname,[],return_parser=True)
115
+
116
+ @cli_entry_point
117
+ def main( progname, arglist ):
118
+ import pathlib
119
+ from .hfg2ncmat import _default_debye_temp, hfg2ncmat
120
+ from ._common import write_text as nc_write_text
121
+ args = _parseArgs( _default_debye_temp(), progname, arglist )
122
+ do_stdout = args.output=='stdout'
123
+ try:
124
+ ncmat = hfg2ncmat( spec = args.spec,
125
+ formula = args.formula,
126
+ density = args.density,
127
+ title = args.title,
128
+ debyetemp = args.debyetemp,
129
+ verbose = not do_stdout,
130
+ notrim = args.notrim )
131
+ except RuntimeError as e:
132
+ raise SystemExit('Error: %s'%str(e))
133
+ if do_stdout:
134
+ print(ncmat)
135
+ return
136
+ outfile = pathlib.Path(args.output)
137
+ if outfile.exists() and not args.force:
138
+ raise SystemExit('Error: output file already exists'
139
+ ' (run with --force to overwrite)')
140
+ if not outfile.parent.is_dir():
141
+ raise SystemExit('Error: output directory does not exist:'
142
+ f' { outfile.parent }')
143
+ nc_write_text(outfile,ncmat)
144
+ print(f"Wrote: {outfile}")