ncrystal-python 3.9.81__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- NCrystal/__init__.py +85 -0
- NCrystal/__main__.py +98 -0
- NCrystal/_chooks.py +854 -0
- NCrystal/_cli_cif2ncmat.py +269 -0
- NCrystal/_cli_endf2ncmat.py +503 -0
- NCrystal/_cli_hfg2ncmat.py +144 -0
- NCrystal/_cli_mcstasunion.py +74 -0
- NCrystal/_cli_ncmat2cpp.py +31 -0
- NCrystal/_cli_ncmat2hkl.py +180 -0
- NCrystal/_cli_nctool.py +1018 -0
- NCrystal/_cli_vdos2ncmat.py +463 -0
- NCrystal/_cli_verifyatompos.py +257 -0
- NCrystal/_cliimpl.py +307 -0
- NCrystal/_cliwrap_config.py +36 -0
- NCrystal/_common.py +499 -0
- NCrystal/_coreimpl.py +114 -0
- NCrystal/_hfgdata.py +546 -0
- NCrystal/_hklobjects.py +136 -0
- NCrystal/_is_std.py +0 -0
- NCrystal/_locatelib.py +210 -0
- NCrystal/_miscimpl.py +354 -0
- NCrystal/_mmc.py +757 -0
- NCrystal/_msg.py +60 -0
- NCrystal/_ncmat2cpp_impl.py +445 -0
- NCrystal/_ncmatimpl.py +2131 -0
- NCrystal/_numpy.py +76 -0
- NCrystal/_testimpl.py +579 -0
- NCrystal/api.py +56 -0
- NCrystal/atomdata.py +177 -0
- NCrystal/cfgstr.py +77 -0
- NCrystal/cifutils.py +1795 -0
- NCrystal/cli.py +96 -0
- NCrystal/constants.py +134 -0
- NCrystal/core.py +1910 -0
- NCrystal/datasrc.py +226 -0
- NCrystal/exceptions.py +66 -0
- NCrystal/hfg2ncmat.py +270 -0
- NCrystal/mcstasutils.py +438 -0
- NCrystal/misc.py +317 -0
- NCrystal/mmc.py +35 -0
- NCrystal/ncmat.py +778 -0
- NCrystal/ncmat2cpp.py +80 -0
- NCrystal/obsolete.py +67 -0
- NCrystal/plot.py +484 -0
- NCrystal/plugins.py +49 -0
- NCrystal/test.py +76 -0
- NCrystal/vdos.py +1034 -0
- ncrystal_python-3.9.81.dist-info/LICENSE +206 -0
- ncrystal_python-3.9.81.dist-info/METADATA +515 -0
- ncrystal_python-3.9.81.dist-info/RECORD +53 -0
- ncrystal_python-3.9.81.dist-info/WHEEL +5 -0
- ncrystal_python-3.9.81.dist-info/entry_points.txt +10 -0
- ncrystal_python-3.9.81.dist-info/top_level.txt +1 -0
|
@@ -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}")
|