diff --git a/src/DataLib/ALARAJOY.cpp b/src/DataLib/ALARAJOY.cpp index 5112e47d..872723ea 100644 --- a/src/DataLib/ALARAJOY.cpp +++ b/src/DataLib/ALARAJOY.cpp @@ -35,7 +35,8 @@ void ALARAJOYLib::loadDSVData() // Read header containing energy group number in first entry std::string groupName; - inTrans >> nGroups >> groupName; + std::string processingCode; + inTrans >> nGroups >> groupName >> processingCode; // Extract Parent KZA until EOF at pKZA == -1 while ((inTrans >> row.parentKZA) && row.parentKZA != -1) diff --git a/tools/ALARAJOYWrapper/README.md b/tools/ALARAJOYWrapper/README.md index ca2c080c..bf265057 100644 --- a/tools/ALARAJOYWrapper/README.md +++ b/tools/ALARAJOYWrapper/README.md @@ -38,6 +38,10 @@ Many decay data libraries are distributed in repositories containing individual * [TENDL](https://tendl.imperial.ac.uk/) - Usable with any TENDL release - For FENDL3.2x processing, [TENDL 2017](https://tendl.imperial.ac.uk/tendl_2017/tendl2017.html) is the standard version. + * [PREPRO-Processed TENDL Data (FISPACT-II)](https://git.oecd-nea.org/fispact/nuclear_data) + - Nuclear data repository for use specifically with FISPACT-II. + - TENDL 2017/14 availble. + - Convertable to ALARAJOY-readable DSV file by calling `preprocess_fendl3` with `-n` flag. - Decay Data * EAF @@ -55,7 +59,7 @@ ALARAJOYWrapper is designed to produce a space-delimited DSV containing cross-da Running ALARAJOYWrapper can be done with one Python command: ``` -python preprocess_fendl3.py -f /path/to/fendl3_data_dir/ -d /path/to/decay_library/ decay_library-type -g group_name -a -t -r -p +python preprocess_fendl3.py -f /path/to/fendl3_data_dir/ -d /path/to/decay_library/ decay_library-type -g group_name -a -t -r -p -n ``` To read in detail about each of these arguments, call this command: ``` diff --git a/tools/ALARAJOYWrapper/preprocess_fendl3.py b/tools/ALARAJOYWrapper/preprocess_fendl3.py index f71fe285..131f2151 100644 --- a/tools/ALARAJOYWrapper/preprocess_fendl3.py +++ b/tools/ALARAJOYWrapper/preprocess_fendl3.py @@ -70,6 +70,15 @@ def make_argparser(): sections from the original TENDL file. ''') ) + parser.add_argument( + '--nea_prepro', '-n', action='store_true', + help=(''' + Option to convert PREPRO-formatted CCFE-709 groupwise data into + an ALARAJOY-formatted DSV. If true, must also provide the path to + the directory containing PREPRO .asc GENDF files for the `-f` + argument. + ''') + ) return parser def configure_logging(redirect_warnings=False): @@ -226,7 +235,7 @@ def process_pendf( def process_gendf( njoy_groupr_input, material_id, MTs, mt_dict, temperature, pKZA, - isomer_dict, all_rxns, all_nucs, group_name, tendl_dir, + isomer_dict, all_rxns, all_nucs, group_name, tendl_dir, gendf_parser, ign=17, ngn='', egn='' ): """ @@ -299,7 +308,8 @@ def process_gendf( if gendf_path: # Extract MT values again from GENDF file as there may be some # difference from the original MT values in the ENDF/PENDF files - non_zero_xs, gendf_MTs, nGroups = tp.extract_gendf_data(gendf_path) + gendf_dict, nGroups, _ = gendf_parser.parse(gendf_path) + gendf_MTs = gendf_dict[3]['MTs'] # Conditionally save group bounds from NJOY/GROUPR output if not Path(group_name.split()[-1]).is_file(): @@ -313,12 +323,13 @@ def process_gendf( f'GENDF file missing MTs {diffs} present in the ' \ f'original TENDL file for {element}-{A}.' ) + if gendf_MTs: all_rxns = tp.iterate_MTs( - gendf_MTs, mt_dict, non_zero_xs, pKZA, - all_rxns, all_nucs, isomer_dict, nGroups + gendf_dict, mt_dict, pKZA, all_rxns, all_nucs, nGroups, + isomer_dict=isomer_dict ) - print(f'Finished processing {element}-{A}') + notify_nuc_completion(element, A) else: warnings.warn( @@ -336,6 +347,78 @@ def process_gendf( return all_rxns, nGroups +def notify_nuc_completion(element, A): + """ + Print to output stream to notify the completion of all processing for a + single nuclide. + + Arguments: + element (str): Chemical symbol of the nuclide whose data was + processed. + A (str): Mass number for the provided nuclide, including an "m" or "n" + for the first two isomeric states, if applicable. + + Returns: + None + """ + + print(f'Finished processing {element}-{A}') + +def collect_all_prepro_TENDL_data( + prepro_tendl_dir, mt_dict, all_rxns, all_nucs + ): + """ + As an alternate pathway to the standard NJOY-based processing procedure, + if the path to a directory containing PREPRO-processed groupwise TENDL + files is provided as the `-f` argument and the `-n` flag is included + when running `main()`, then the reaction data from these files are + parsed and stored directly to `all_rxns`. Nuclear data of this format + is usable by FISPACT-II and is disbtributed through the NEA GitLab + (https://git.oecd-nea.org/fispact/nuclear_data/), with options for + both TENDL-2017 and TENDL-2014. + + Arguments: + prepro_tendl_dir (pathlib._local.PosixPath): Path to the directory + containing PREPRO-processed TENDL data files. + mt_dict (dict): Dictionary formatted data structure for mt_table.csv. + all_rxns (collections.defaultdict): Hierarchical dictionary keyed by + parent nuclides to store all reaction data, with structured as: + {parent: + {daughter: + {MT: + { + 'emitted': (str of emitted particles) + 'xsections': (array of groupwise XS) + } + } + } + } + all_nucs (dict): Dictionary keyed by all nuclide KZAs in the decay + library, with values of their half-lives (-1 for stable nuclides). + + Returns: + all_rxns (collections.defaultdict): Updated dictionary for all + reactions and sub-pathways for all nuclides with TENDL files + present in `prepro_tendl_dir`. + nGroups (int): Number of energy groups into which the groupwise cross + sections were calculated. + """ + + # File format from FISPACT NEA GitLab: {element}{A}g.asc + # ("g" for "groupwise") + for gendf_path in sorted(prepro_tendl_dir.glob(f'*g.asc')): + gendf_dict, nGroups, pKZA = tp.GENDFParser( + MFs=[3,10], prepro=True + ).parse(gendf_path) + + all_rxns |= tp.iterate_MTs( + gendf_dict, mt_dict, pKZA, all_rxns, all_nucs, nGroups, + prepro=True + ) + notify_nuc_completion(*tp.interpret_KZA(pKZA)) + + return all_rxns, nGroups + def subtract_gas_from_totals(all_rxns): """ For any reaction that produces a gas daughter, subtract the individual @@ -429,7 +512,8 @@ def rxn_to_str(parent, daughter, MT, rxn): return dsv_row + ' '.join(str(xs) for xs in rxn['xsections']) def store_results( - dsv_path, all_rxns, nGroups, tendl_dir, group_name, plotting + dsv_path, all_rxns, nGroups, tendl_dir, + group_name, processing_code, plotting ): """ Save groupwise-converted cross-section data to a space-delimited DSV file @@ -471,6 +555,9 @@ def store_results( group_name (str): Name of the group structure according to which GROUPR converted continuous energy cross-sections to groupwise cross-sections. + processing_code (str): Name of the nuclear data processing code used. + NJOY for standard ALARAJOY workflow, PREPRO if `-n` flag is + applied for FISPACT-II formatted TENDL data. plotting (bool): Boolean to set whether to produce cross-section plots. @@ -479,7 +566,7 @@ def store_results( """ with open(dsv_path, 'w') as dsv: - dsv.write(f'{nGroups} {group_name}\n') + dsv.write(f'{nGroups} {group_name} {processing_code}\n') for parent in sorted(all_rxns): element, A = tp.interpret_KZA(parent) for daughter in all_rxns[parent]: @@ -577,48 +664,63 @@ def main(): all_nucs = rxd.find_nucs_from_decay_lib(decay_path) all_rxns = defaultdict(lambda: defaultdict(dict)) - unresr_err_cases = [] - for file_properties in tp.search_for_files(search_dir): - element, A, pKZA, endf_path = tuple(file_properties.values()) - TAPE20.write_bytes(endf_path.read_bytes()) - endf_file_dict, material_id = tp.parse_endf_file_level_data(TAPE20) - MTs = set(endf_file_dict) + if args.nea_prepro: + group_name = 'CCFE-709' + processing_code = 'PREPRO' + all_rxns, nGroups = collect_all_prepro_TENDL_data( + search_dir, mt_dict, all_rxns, all_nucs + ) - if len((MTs - rxd.SPEC_MTS) - endf6_MTs) > 0: - invalid_MTs = sorted((MTs - rxd.SPEC_MTS) - endf6_MTs) - warnings.warn( - f'Invalid MTs in provided TENDL file for ' \ - f'{element}{A}: {invalid_MTs}' + else: + processing_code = 'NJOY' + unresr_err_cases = [] + gendf_parser = tp.GENDFParser() + for file_properties in tp.search_for_files(search_dir): + element, A, pKZA, endf_path = tuple(file_properties.values()) + TAPE20.write_bytes(endf_path.read_bytes()) + endf_file_dict, material_id = tp.parse_endf_file_level_data( + TAPE20 + ) + MTs = set(endf_file_dict) + + if len((MTs - rxd.SPEC_MTS) - endf6_MTs) > 0: + invalid_MTs = sorted((MTs - rxd.SPEC_MTS) - endf6_MTs) + warnings.warn( + f'Invalid MTs in provided TENDL file for ' \ + f'{element}{A}: {invalid_MTs}' + ) + MTs = MTs.intersection(endf6_MTs) + + MTs, isomer_dict, njoy_prep_error, unresr_err_cases = ( + process_pendf( + material_id, MTs, pKZA, mt_dict, temperature, + TAPE20, search_dir, unresr_err_cases + ) ) - MTs = MTs.intersection(endf6_MTs) - MTs, isomer_dict, njoy_prep_error, unresr_err_cases = process_pendf( - material_id, MTs, pKZA, mt_dict, temperature, - TAPE20, search_dir, unresr_err_cases - ) + if not njoy_prep_error: + all_rxns, nGroups = process_gendf( + njt.groupr_input, material_id, MTs, mt_dict, + temperature, pKZA, isomer_dict, all_rxns, + all_nucs, group_name, search_dir, gendf_parser, + ign=ign, ngn=ngn, egn=egn + ) - if not njoy_prep_error: - all_rxns, nGroups = process_gendf( - njt.groupr_input, material_id, MTs, mt_dict, temperature, - pKZA, isomer_dict, all_rxns, all_nucs, group_name, search_dir, - ign=ign, ngn=ngn, egn=egn - ) + else: + warnings.warn( + f'''PENDF preparation failed for {element}-{A}. + NJOY error message: {njoy_prep_error}''' + ) - else: + njt.cleanup_njoy_files(element, A) + + if unresr_err_cases: warnings.warn( - f'''PENDF preparation failed for {element}-{A}. - NJOY error message: {njoy_prep_error}''' + f'A total of {len(unresr_err_cases)} TENDL files required ' \ + 'an increase in UNRESR fractional error tolerance for the ' \ + f'following nuclides: {unresr_err_cases}' ) - njt.cleanup_njoy_files(element, A) - - if unresr_err_cases: - warnings.warn( - f'A total of {len(unresr_err_cases)} TENDL files required ' \ - 'an increase in UNRESR fractional error tolerance for the ' \ - f'following nuclides: {unresr_err_cases}' - ) - # Handle gas total production cross-sections, per user specifications gas_filtered = subtract_gas_from_totals(all_rxns) @@ -627,8 +729,8 @@ def main(): dsv_path = dir / 'cumulative_gendf_data.dsv' store_results( - dsv_path, gas_filtered, nGroups, - search_dir, group_name, args.xs_plotting + dsv_path, gas_filtered, nGroups, search_dir, + group_name, processing_code, args.xs_plotting ) print( f'Neutron activation cross-sections converted to {nGroups} groups ' \ diff --git a/tools/ALARAJOYWrapper/tendl_processing.py b/tools/ALARAJOYWrapper/tendl_processing.py index 3bc28067..6bdcf3cf 100644 --- a/tools/ALARAJOYWrapper/tendl_processing.py +++ b/tools/ALARAJOYWrapper/tendl_processing.py @@ -3,7 +3,7 @@ from pathlib import Path from reaction_data import GAS_DF from njoy_tools import elements -from collections import defaultdict +from collections import defaultdict, abc import warnings import numpy as np @@ -219,113 +219,15 @@ def determine_all_excitations(endf_path, MTs): return isomer_dict -def _gendf_parse_control(line): - """ - Extract the integer values of the MF (file) and MT (reaction) numbers from - a given line of an ENDF-formatted file. By ENDF formatting - conventions, these values are always in a fixed position, but may - contain additional whitespace. - - Arguments: - line (str): Text of a whole line of an ENDF-formatted file. - - Returns: - MF (int or None): ENDF file number. None if the file is incorrectly - formatted. - MT (int): Reaction number. None if the file is incorrectly formatted. - """ - - if len(line) < 75: - return None, None - - try: - return ( - int(line[70:72]), # MF - int(line[72:75]) # MT - ) - except ValueError: - return None, None - -def extract_gendf_data(gendf_path): - """ - Parse a GENDF-formatted (post-GROUPR processing) file for lines containing - cross-section data for each reaction type, while compiling a full set - of all MT numbers corresponding to that reaction. - - Arguments - gendf_path (pathlib._local.PosixPath): Path to the GENDF file from - which to extract cross-section data. - - Returns: - non_zero_xs (collections.defaultdict): Dictionary keyed by MT number - valued by lists of sub-dictionaries. Each of these are keyed by - the GROUPR group index tag (`IG`) and valued by the associated - groupwise cross-section value for that energy group. - MTs (set of ints): All reaction types with cross-section data in the - provided GENDF. - nGroups (int): Number of energy groups into which the groupwise cross - sections were calculated. - """ - - MTs = set() - non_zero_xs = defaultdict(list) - - current_MT = None - current_section = {} - current_IG = None - line_count = 0 - nGroups = None - - with open(gendf_path, 'r') as f: - for line in f: - mf, mt = _gendf_parse_control(line) - - # Extract number of groups, found in second line of file - # description section - if mf == 1 and mt == 451 and int(line.rstrip('\n')[-3:]) == 2: - nGroups = int(line.split()[2]) - - if mf == 3 and mt != 0: - MTs.add(mt) - if mt != current_MT: - if current_section: - non_zero_xs[current_MT].append(current_section) - - current_section = {} - current_MT = mt - line_count = 0 - - line_count += 1 - - if line_count < 2: - continue - - if line_count % 2 == 0: - current_IG = int(line[62:66]) - - else: - current_section[current_IG] = float( - line.split()[1].replace('+','E+').replace('-','E-') - ) - - else: - if current_section: - non_zero_xs[current_MT].append(current_section) - current_section = {} - - current_MT = None - current_IG = None - line_count = 0 - - if nGroups is None: - raise ValueError( - f'{gendf_path} misformatted. Expecting to find group number in ' \ - 'MF1, MT451.' - ) +def _section_to_group_array(section, nGroups): + sigmas = np.zeros(nGroups) + for IG, sigma in section.items(): + if IG <= nGroups: + sigmas[IG - 1] = sigma - return non_zero_xs, MTs, nGroups + return sigmas[::-1] -def populate_xs(xs_by_index, M_values, nGroups): +def populate_xs(gendf_dict, MT, nGroups, isomer_dict={}, prepro=False): """ Given a dictionary containing all cross-section data for a given reaction type, identify all excitation pathways and save group-positioned @@ -342,6 +244,16 @@ def populate_xs(xs_by_index, M_values, nGroups): daughter produced from the given reaction type. nGroups (int): Number of energy groups into which the groupwise cross sections were calculated. + isomer_dict (collections.defaultdict, optional): Dictionary keyed by + reaction type (MT), with each MT containing a subdictionary of the + MF from which the isomeric pathways are extracted. The lowest + MT/MF level has a list of all isomeric states of possible daughter + nuclides for which there are cross-section data in the original + TENDL file. + (Defaults to {}) + prepro (bool, optional): Option to handle PREPRO/GROUPIE-processed + groupwise nuclear data. + (Defaults to False) Returns: sigma_dict (dict): Dictionary keyed by excitation levels (M) with @@ -350,22 +262,24 @@ def populate_xs(xs_by_index, M_values, nGroups): Vitamin-J 175 group structure if none is otherwise specified. """ - sigma_dict = {} - excitations = len(M_values) - N = min(excitations, len(xs_by_index)) - - if excitations > 1: - M_values = list(range(excitations)) - - for M, ordered_xs in zip(M_values[:N], xs_by_index[:N]): - sigmas = np.zeros(nGroups) + if MT in gendf_dict[10]['MTs']: + sections = gendf_dict[10]['non_zero_xs'][MT] + keys = list(sections) + M_values = keys + section_iter = [sections[key] for key in keys] - for IG, sigma in ordered_xs.items(): - sigmas[IG - 1] = sigma + else: + section_iter = gendf_dict[3]['non_zero_xs'][MT] + M_values = [0] if prepro else list(isomer_dict[MT].values())[0] + section_iter = section_iter[:len(M_values)] - sigma_dict[M] = sigmas[::-1] + if len(M_values) > 1: + M_values = range(len(M_values)) - return sigma_dict + return { + M: _section_to_group_array(section, nGroups) + for M, section in zip(M_values, section_iter) + } def incrementally_deexcite_isomer(M, dKZA, eaf_nucs): """ @@ -392,7 +306,8 @@ def incrementally_deexcite_isomer(M, dKZA, eaf_nucs): return dKZA + trial_M def iterate_MTs( - MTs, mt_dict, non_zero_xs, pKZA, all_rxns, all_nucs, isomer_dict, nGroups + gendf_dict, mt_dict, pKZA, all_rxns, all_nucs, nGroups, + isomer_dict={}, prepro=False ): """ Iterate through all of the MTs present in a given GENDF file to extract @@ -407,7 +322,7 @@ def iterate_MTs( Arguments: MTs (list of int): List of reaction types present in the GENDF file. - mt_dict (dict): Dictionary formatted data structure for mt_table.csv + mt_dict (dict): Dictionary formatted data structure for mt_table.csv. non_zero_xs (collections.defaultdict): Dictionary keyed by MT number valued by lists of sub-dictionaries. Each of these are keyed by the GROUPR group index tag (`IG`) and valued by the associated @@ -427,20 +342,26 @@ def iterate_MTs( } all_nucs (dict): Dictionary keyed by all nuclide KZAs in the decay library, with values of their half-lives (-1 for stable nuclides). - isomer_dict (collections.defaultdict): Dictionary keyed by reaction - type (MT), with each MT containing a subdictionary of the MF from - which the isomeric pathways are extracted. At the lowest MT/MF - level has a list of all isomeric states of possible daughter - nuclides for which there are cross-section data in the original - TENDL file. nGroups (int): Number of energy groups into which the groupwise cross sections were calculated. + isomer_dict (collections.defaultdict, optional): Dictionary keyed by + reaction type (MT), with each MT containing a subdictionary of the + MF from which the isomeric pathways are extracted. The lowest + MT/MF level has a list of all isomeric states of possible daughter + nuclides for which there are cross-section data in the original + TENDL file. + (Defaults to {}) + prepro (bool, optional): Option to handle PREPRO/GROUPIE-processed + groupwise nuclear data. + (Defaults to False) + Returns: all_rxns (collections.defaultdict): Updated dictionary for all reaction pathways for the given parent and its MTs. """ + MTs = gendf_dict[3]['MTs'] filtered_MTs = MTs - EXCITATION_REACTIONS for MT in MTs: cumulative_MT = REVERSE_EXCITATION_DICT.get(MT) @@ -448,13 +369,18 @@ def iterate_MTs( filtered_MTs.add(MT) for MT in filtered_MTs: - xs_by_index = non_zero_xs[MT] - M_values = list(isomer_dict[MT].values())[0] - rxn = mt_dict[MT] + rxn = mt_dict.get(MT) + if not rxn: + continue + gas = rxn['gas'] - - # Calculate dKZA values for each excitation pathway in MF10 - for M, sigmas in populate_xs(xs_by_index, M_values, nGroups).items(): + + # Calculate dKZA values for each excitation pathway in MF9/10 + sigma_dict = populate_xs( + gendf_dict, MT, nGroups, + isomer_dict=isomer_dict, prepro=prepro + ) + for M, sigmas in sigma_dict.items(): emitted = rxn['emitted'] dKZA = (((pKZA // 10) * 10 + rxn['delKZA']) // 10) * 10 + M if gas: @@ -499,4 +425,353 @@ def iterate_MTs( all_rxns[pKZA][dKZA][special_MT]['xsections'] += sigmas - return all_rxns \ No newline at end of file + return all_rxns + +class GENDFParser: + + def __init__(self, MFs=(3,), prepro=False): + if not isinstance(MFs, abc.Iterable): + MFs = [MFs] + + self.MFs = MFs + self.prepro = prepro + self.gendf_dict = None + self.nGroups = None + self.pKZA = None + self._reset_section_state() + self.current_MF = None + self.current_MT = None + + @staticmethod + def _parse_control(line): + """ + Extract the integer values of the MF (file) and MT (reaction) numbers from + a given line of an ENDF-formatted file. By ENDF formatting + conventions, these values are always in a fixed position, but may + contain additional whitespace. + + Arguments: + line (str): Text of a whole line of an ENDF-formatted file. + + Returns: + MF (int or None): ENDF file number. None if the file is incorrectly + formatted. + MT (int): Reaction number. None if the file is incorrectly formatted. + """ + + if len(line) < 75: + return None, None + + try: + return ( + int(line[70:72]), # MF + int(line[72:75]) # MT + ) + except ValueError: + return None, None + + @staticmethod + def _reformat_endf_float(num_str): + """ + Convert a parsed ENDF numeric string to a standard Python-usable + floating point number. + + Argument: + num_str (str): Numeric string parsed from an ENDF file. Can be in + plain decimal for (0.1), explicit E/D notation (1.0E-1, + 1.0D-1), or a Fortran shortand exponent with implicit 'E' + (1.0-1). + + Returns: + num_float (float or None): Converted numeric value as a floating + point number, if one could be found in `num_str`. Otherwise, + `None`. + """ + + v = num_str.strip() + if not v: + return None + + if 'E' in v or 'e' in v or 'D' in v or 'd' in v: + v = v.replace('D', 'E').replace('d', 'e') + else: + for i in range(1, len(v)): + if v[i] in '+-': + v = v[:i] + 'E' + v[i:] + break + + return float(v) + + @staticmethod + def _parse_tab1_header(line, with_lfs=False): + """ + Extract three integer parameters from an ENDF reaction header needed + to identify the reaction's file section (or subsection for MF9/10 + specific excitation pathways). For further information on these + TAB1 parameters, see the ENDF6 Manual + (https://www.nndc.bnl.gov/endfdocs/ENDF-102-2023.pdf). + + Arguments: + line (str): Text of a whole line of an ENDF-formatted file. + with_lfs (bool, optional): Option to search for the LFS parameter. + Only needed when parsing MF9/10 for reaction subsections for + specific excitation pathways. + (Defaults to False) + + Returns: + LFS (int or None): Excited state of the daughter nuclide resultant + from the given reaction/excitation pathway. `None` if + `with_lfs` is `False`. + NR (int): "Number of energy ranges. A different scheme may be + given for each range" (ENDF6 Manual, Section 9.2). + NP (int): "Total number of energy points used to specify the + data" (ENDF6 Manual, Section 9.2). + """ + + LFS = int(line[33:44]) if with_lfs else None + NR = int(line[44:55]) + NP = int(line[55:66]) + + return LFS, NR, NP + + @classmethod + def _parse_prepro_tab1_xs(cls, line): + """ + For a given line in a PREPRO-formatted TAB1 data table (i.e. after the + header), extract, reformat, and organize all cross sections into a + list. + + Arguments: + line (str): A single TAB1 data line. + + Returns: + line_xs (list of float): List of all cross-section values parsed + from the provided TAB1 data line. + """ + + return [ + v for v in ( + cls._reformat_endf_float(line[i : i + 11]) + for i in range(0, 66, 11) + ) if v is not None + ][1::2] + + def _reset_section_state(self): + """ + Reset all state variables tracked within the section currently being + parsed. + + Arguments: + None + + Returns: + None + """ + + self.current_section = {} + self.current_LFS = None + + # Standard NJOY/GROUPR (IG) state + self.line_count = 0 + self.current_IG = None + + # PREPRO/GROUPIE state (TAB1) state + self.section_line_idx = 0 + self.NR = None + self.NP = None + self.interp_lines_left = 0 + self.points_collected = 0 + self.point_idx = 0 + + def _save_current_section(self): + """ + Store the currently accumulated section, if non-empty. + + Arguments: + None + + Returns: + None + """ + + if not self.current_section: + return + + if self.current_MF == 10: + self.gendf_dict[self.current_MF]['non_zero_xs'][self.current_MT][ + self.current_LFS + ] = self.current_section + + else: + self.gendf_dict[self.current_MF]['non_zero_xs'][ + self.current_MT + ].append(self.current_section) + + def _handle_groupr_line(self, line): + """ + Parse one line of a standard GROUPR IG-tagged MF section to save + either the current IG (group-index) for even line counts or the + associated energy-dependent cross section for odd line counts. + + Arguments: + line (str): Text of a whole line of an ENDF-formatted file. + + Returns: + None + """ + + self.line_count += 1 + if self.line_count < 2: + return + + if self.line_count % 2 == 0: + self.current_IG = int(line[62:66]) + else: + self.current_section[self.current_IG] = self._reformat_endf_float( + line.split()[1] + ) + + def _handle_prepro_line(self, line, MF): + """ + Parse one line of a PREPRO/GROUPIE TAB1 section. Handles both single- + TAB1-record MFs and MF10's multi-subsection (per-LFS) layout + uniformly, by detecting new subsections via NP exhaustion rather + than a fixed line index. + + Arguments: + line (str): Text of a whole line of an ENDF-formatted file. + MF (int): ENDF file number. + + Returns: + None + """ + + self.section_line_idx += 1 + if self.section_line_idx == 1: + return + + if self.NP is None or self.points_collected >= self.NP: + self._save_current_section() + self.current_section = {} + self.current_LFS, self.NR, self.NP = self._parse_tab1_header( + line, with_lfs=(MF == 10) + ) + self.interp_lines_left = int(np.ceil(self.NR / 3)) + self.points_collected = 0 + self.point_idx = 0 + return + + if self.interp_lines_left > 0: + self.interp_lines_left -= 1 + return + + for sigma in self._parse_prepro_tab1_xs(line): + if self.points_collected >= self.NP: + break + + self.point_idx += 1 + self.current_section[self.point_idx] = sigma + self.points_collected += 1 + + def parse(self, gendf_path): + """ + Main externally callable function on a GENDFParser parser object to + parse a GENDF file for all cross-sections for each MF/MT pair, as + specified in the GENDFParser initialization. Capable of parsing + either NJOY/GROUPR or PREPRO/GROUPIE processed GENDF files. + + Arguments: + gendf_path (pathlib._local.PosixPath): Path to the GENDF file from + which to extract activation data. + + Returns: + gendf_dict (dict): Dictionary keyed by MF file number and valued + by a subdictionary with keys `'MTs'` and `'non_zero_xs'`. The + `'MTs'` key is valued by a set of all reaction types with + associated activation cross-sections within that `MF`. The + `'non_zero_xs'` key will be valued by either a + `collections.defaultdict(dict)` structure keyed by each MT + with subkeys for each LFS in the case of MF == 10, or a + `collections.defaultdict(list)` list of dictionaries of + groupwise-energy bound keys with associated cross-sections. + nGroups (int): Number of energy groups into which the groupwise + cross-sections were calculated. + pKZA (int or None): Parent KZA value. Only extracted when + `prepro` is True for the parser initialization. Otherwise + `None`. + """ + + self.gendf_dict = { + MF : { + 'MTs' : set(), + 'non_zero_xs' : ( + defaultdict(dict) if MF == 10 else defaultdict(list) + ) + } + for MF in self.MFs + } + + self.nGroups = None + self.pKZA = None + self._reset_section_state() + self.current_MF = None + self.current_MT = None + + with open(gendf_path, 'r') as f: + lines = f.readlines() + + if self.prepro: + self.pKZA = int( + self._reformat_endf_float(lines[1].split()[0]) * 10 + + self._reformat_endf_float(lines[2].split()[3]) + ) + + for line in lines: + mf, mt = self._parse_control(line) + + # Extract number of groups, found in second line of file + # description sectino + if ( + not self.prepro and mf == 1 and mt == 451 + and int(line.rstrip('\n')[-3:]) == 2 + ): + self.nGroups = int(line.split()[2]) + elif self.prepro: + groupie_tag = 'Unshielded Group Averages Using' + if groupie_tag in line: + self.nGroups = int( + line.split(groupie_tag)[1].split()[0] + ) + + if mf in self.MFs and mt != 0: + self.gendf_dict[mf]['MTs'].add(mt) + + if mt != self.current_MT or mf != self.current_MF: + self._save_current_section() + self._reset_section_state() + self.current_MT = mt + self.current_MF = mf + + if self.prepro: + self._handle_prepro_line(line, mf) + else: + self._handle_groupr_line(line) + + else: + self._save_current_section() + self._reset_section_state() + self.current_MT = None + self.current_MF = None + + self._save_current_section() + + if not self.gendf_dict.get(10): + self.gendf_dict.setdefault(10, {'MTs' : [], 'non_zero_xs' : []}) + + if not self.nGroups: + raise ValueError( + f'{gendf_path} misformatted. Expecting to find group number' \ + ' in MF1, MT451.' + ) + + return self.gendf_dict, self.nGroups, self.pKZA \ No newline at end of file