Loading src/scripts/parse_output.py 0 → 100755 +260 −0 Original line number Diff line number Diff line #!/usr/bin/env python3 # Copyright (C) 2025 INAF - Osservatorio Astronomico di Cagliari # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # A copy of the GNU General Public License is distributed along with # this program in the COPYING file. If not, see: <https://www.gnu.org/licenses/>. ## @package parse_output # \brief Script to extract physical quantities from NPTMcode output files. # # This script is intended to assist users in the inspection of results. It # can be used to extract physical quantities, such as radiation pressure # forces and cross-sections from the standard code output files and save # the data in machine readable CSV files. # # The script execution requires python3. import math from sys import argv ## \brief Main execution code # # `main()` is the function that handles the creation of the script configuration # and the execution of the comparison. It returns an integer value corresponding # to the number of detected error-level inconsistencies. # # \returns errors: `int` Number of runtime errors (0 means successful run). def main(): config = {} errors = 0 try: config = parse_arguments() except ValueError as ex: print(ex) print("\nType \"parse_output.py --help\" to get more detailed help.") errors = 1 if config['help_mode']: config['help_mode'] = True print_help() else: if (config['result_file'] == ''): print("ERROR: no input file name given (missing \"--in FILE\" option).") errors += 1 if (config['output_name'] == ''): print("ERROR: no output file name given (missing \"--out FILE\" option).") errors += 1 if (config['application'] == ''): print("ERROR: no application identifier given (missing \"--app=APP\" option).") errors += 1 if (errors == 0): if (config['format'] == 'LEGACY'): if (config['application'] == 'CLU'): try: errors += parse_legacy_oclu(config) except Error as ex: print(ex) errors += 1 return errors ## \brief Parse the command line arguments. # # The script behaviour can be modified through a set of mandatory and optional # arguments. Mandatory arguments are those required to execute a meaningful # parsing and they are limited to the names of the files that need to be read # and written. The other arguments affect the format and the data fields that # are sought for. # # \returns config: `dict` A dictionary containing the script configuration. def parse_arguments(): config = { 'result_file': '', 'output_name': '', 'application': '', 'format': 'LEGACY', 'selection': '[ALL]', 'help_mode': False } arg_index = 1 skip_arg = False for arg in argv[1:]: if skip_arg: skip_arg = False continue split_arg = arg.split('=') if (arg.startswith("--in")): config['result_file'] = argv[arg_index + 1] arg_index += 1 skip_arg = True if (config['result_file'].endswith('.hd5')): config['format'] = 'HDF5' elif (arg.startswith("--out")): config['output_name'] = argv[arg_index + 1] arg_index += 1 skip_arg = True elif (arg.startswith("--app=")): config['application'] = split_arg[1] elif (arg.startswith("--help")): config['help_mode'] = True else: raise ValueError("Unrecognized argument \'{0:s}\'".format(arg)) arg_index += 1 return config ## \brief Parse a legacy output file of np_cluster. # # \param config: `dict` A dictionary containing the script configuration. # \return errors: `int` The number of encountered errors. def parse_legacy_oclu(config): errors = 0 oclu_name = config['result_file'] root_name = config['output_name'] out22 = None # Cluster average cross-sections out31 = None # Cluster differential cross-sections in state -1 out32 = None # Cluster differential cross-sections in state +1 out91 = None # Cluster radiation pressure forces in state +1 out92 = None # Cluster radiation pressure forces in state -1 try: oclu_file = open(oclu_name, "r") # open the OCLU file for reading file_line = "first line" # a string to parse the OCLU file lines if ('ALL' in config['selection'] or 'ICS' in config['selection']): out22 = open(root_name + "_ics.csv", "w") # open a file for integrated cross sections if ('ALL' in config['selection'] or 'DCS' in config['selection']): out31 = open(root_name + "_dcs1.csv", "w") # open a file for differential cross-sections in state -1 out32 = open(root_name + "_dcs2.csv", "w") # open a file for differential cross-sections in state +1 if ('ALL' in config['selection'] or 'RAP' in config['selection']): out91 = open(root_name + "_dcs2.csv", "w") # open a file for differential cross-sections in state +1 out92 = open(root_name + "_dcs1.csv", "w") # open a file for differential cross-sections in state -1 # Define the quantities that you need to extract alam = 0.0 # Parsing loop until the end of the OCLU file while (file_line != ""): file_line = oclu_file.readline() # read the next OCLU file line if (file_line.startswith(" VK=")): # we found VK, so we calculate lambda # extract VK as a number from a string section, after # replacing FORTRAN's D with E vk = float(file_line[5:20].replace("D", "E")) alam = 2.0 * math.pi / vk if ("CLUSTER (ENSEMBLE AVERAGE, MODE 0)" in file_line): # we are in average section. We start a nested loop to # extract the average values found_averages = False while (not found_averages): file_line = oclu_file.readline() if ("----- SCC ----- ABC ----- EXC ----- ALBEDC --" in file_line): # we know we are in LIN -1 because it is the first one # we also know that the next line contains the values # we are looking for, so we read it file_line = oclu_file.readline() # we now extract the values from string sections scasm = float(file_line[1:15].replace("D", "E")) abssm = float(file_line[17:30].replace("D", "E")) extsm = float(file_line[32:45].replace("D", "E")) # we can write the average values, similarly to fort.22 # note that \n puts a new line at the end of the string output_line = "{0:15.4E}{1:15.4E}{2:15.4E}{3:15.4E}\n".format(alam, scasm, abssm, extsm) if (out22 is not None): out22.write(output_line) found_averages = True # terminate the inner loop # the averages were written. We look for CLUSTER section # using another inner loop found_differentials = False while (not found_differentials): file_line = oclu_file.readline() if (" CLUSTER" in file_line): # we found CLUSTER. We know cross-sections for # polarization state -1 will be after 2 more lines for i in range(3): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() scc31 = float(file_line[1:15].replace("D", "E")) abc31 = float(file_line[17:30].replace("D", "E")) exc31 = float(file_line[32:45].replace("D", "E")) # we can write the differential values, similarly to fort.31 output_line = "{0:15.4E}{1:15.4E}{2:15.4E}{3:15.4E}\n".format(alam, scc31, abc31, exc31) if (out31 is not None): out31.write(output_line) # we know that RAPRS values for polarization state -1 # are after 9 more lines for i in range(9): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() raprs92 = float(file_line[33:46].replace("D", "E")) # we can write the RAPRS values output_line = " ALAM={0:15.7E}, RAPRS={1:15.7E}\n".format(alam, raprs92) if (out92 is not None): out92.write(output_line) # we know the differential values for polarization # state 1 are after 9 more lines for i in range(9): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() scc32 = float(file_line[1:15].replace("D", "E")) abc32 = float(file_line[17:30].replace("D", "E")) exc32 = float(file_line[32:45].replace("D", "E")) # we can write the differential values, similarly to fort.31 output_line = "{0:15.4E}{1:15.4E}{2:15.4E}{3:15.4E}\n".format(alam, scc32, abc32, exc32) if (out32 is not None): out32.write(output_line) # we know that RAPRS values for polarization state 1 # are after 9 more lines for i in range(9): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() raprs91 = float(file_line[33:46].replace("D", "E")) # we can write the RAPRS values output_line = " ALAM={0:15.7E}, RAPRS={1:15.7E}\n".format(alam, raprs91) if (out91 is not None): out91.write(output_line) found_differentials = True # terminate the inner loop # The parsing loop ends here if (out22 is not None): out22.close() if (out31 is not None): out31.close() if (out32 is not None): out32.close() if (out91 is not None): out91.close() if (out92 is not None): out92.close() oclu_file.close() # close the OCLU file except Error as ex: print(ex) errors += 1 return errors ## \brief Print a command-line help summary. def print_help(): print(" ") print("*** PARSE RESULTS ***") print(" ") print("Parse the results of a NPTMcode model calculation.") print(" ") print("Usage: \"./parse_results.py --in INPUT --out OUTPUT [OPTIONS]\" ") print(" ") print("Valid options are: ") print("--in INPUT File containing the results of the model calculation (mandatory).") print("--out OUTPUT Root name for the output CSV data files (mandatory). ") print("--app=[SPH|CLU|INCLU] Application whose output needs to be parsed (mandatory). ") print("--format=[HDF5|LEGACY] Format of the result file to be parsed (autodetected by default).") print("--help Print this help and exit. ") print(" ") # ### PROGRAM EXECUTION ### ## \cond res = main() ## \endcond exit(res) Loading
src/scripts/parse_output.py 0 → 100755 +260 −0 Original line number Diff line number Diff line #!/usr/bin/env python3 # Copyright (C) 2025 INAF - Osservatorio Astronomico di Cagliari # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # A copy of the GNU General Public License is distributed along with # this program in the COPYING file. If not, see: <https://www.gnu.org/licenses/>. ## @package parse_output # \brief Script to extract physical quantities from NPTMcode output files. # # This script is intended to assist users in the inspection of results. It # can be used to extract physical quantities, such as radiation pressure # forces and cross-sections from the standard code output files and save # the data in machine readable CSV files. # # The script execution requires python3. import math from sys import argv ## \brief Main execution code # # `main()` is the function that handles the creation of the script configuration # and the execution of the comparison. It returns an integer value corresponding # to the number of detected error-level inconsistencies. # # \returns errors: `int` Number of runtime errors (0 means successful run). def main(): config = {} errors = 0 try: config = parse_arguments() except ValueError as ex: print(ex) print("\nType \"parse_output.py --help\" to get more detailed help.") errors = 1 if config['help_mode']: config['help_mode'] = True print_help() else: if (config['result_file'] == ''): print("ERROR: no input file name given (missing \"--in FILE\" option).") errors += 1 if (config['output_name'] == ''): print("ERROR: no output file name given (missing \"--out FILE\" option).") errors += 1 if (config['application'] == ''): print("ERROR: no application identifier given (missing \"--app=APP\" option).") errors += 1 if (errors == 0): if (config['format'] == 'LEGACY'): if (config['application'] == 'CLU'): try: errors += parse_legacy_oclu(config) except Error as ex: print(ex) errors += 1 return errors ## \brief Parse the command line arguments. # # The script behaviour can be modified through a set of mandatory and optional # arguments. Mandatory arguments are those required to execute a meaningful # parsing and they are limited to the names of the files that need to be read # and written. The other arguments affect the format and the data fields that # are sought for. # # \returns config: `dict` A dictionary containing the script configuration. def parse_arguments(): config = { 'result_file': '', 'output_name': '', 'application': '', 'format': 'LEGACY', 'selection': '[ALL]', 'help_mode': False } arg_index = 1 skip_arg = False for arg in argv[1:]: if skip_arg: skip_arg = False continue split_arg = arg.split('=') if (arg.startswith("--in")): config['result_file'] = argv[arg_index + 1] arg_index += 1 skip_arg = True if (config['result_file'].endswith('.hd5')): config['format'] = 'HDF5' elif (arg.startswith("--out")): config['output_name'] = argv[arg_index + 1] arg_index += 1 skip_arg = True elif (arg.startswith("--app=")): config['application'] = split_arg[1] elif (arg.startswith("--help")): config['help_mode'] = True else: raise ValueError("Unrecognized argument \'{0:s}\'".format(arg)) arg_index += 1 return config ## \brief Parse a legacy output file of np_cluster. # # \param config: `dict` A dictionary containing the script configuration. # \return errors: `int` The number of encountered errors. def parse_legacy_oclu(config): errors = 0 oclu_name = config['result_file'] root_name = config['output_name'] out22 = None # Cluster average cross-sections out31 = None # Cluster differential cross-sections in state -1 out32 = None # Cluster differential cross-sections in state +1 out91 = None # Cluster radiation pressure forces in state +1 out92 = None # Cluster radiation pressure forces in state -1 try: oclu_file = open(oclu_name, "r") # open the OCLU file for reading file_line = "first line" # a string to parse the OCLU file lines if ('ALL' in config['selection'] or 'ICS' in config['selection']): out22 = open(root_name + "_ics.csv", "w") # open a file for integrated cross sections if ('ALL' in config['selection'] or 'DCS' in config['selection']): out31 = open(root_name + "_dcs1.csv", "w") # open a file for differential cross-sections in state -1 out32 = open(root_name + "_dcs2.csv", "w") # open a file for differential cross-sections in state +1 if ('ALL' in config['selection'] or 'RAP' in config['selection']): out91 = open(root_name + "_dcs2.csv", "w") # open a file for differential cross-sections in state +1 out92 = open(root_name + "_dcs1.csv", "w") # open a file for differential cross-sections in state -1 # Define the quantities that you need to extract alam = 0.0 # Parsing loop until the end of the OCLU file while (file_line != ""): file_line = oclu_file.readline() # read the next OCLU file line if (file_line.startswith(" VK=")): # we found VK, so we calculate lambda # extract VK as a number from a string section, after # replacing FORTRAN's D with E vk = float(file_line[5:20].replace("D", "E")) alam = 2.0 * math.pi / vk if ("CLUSTER (ENSEMBLE AVERAGE, MODE 0)" in file_line): # we are in average section. We start a nested loop to # extract the average values found_averages = False while (not found_averages): file_line = oclu_file.readline() if ("----- SCC ----- ABC ----- EXC ----- ALBEDC --" in file_line): # we know we are in LIN -1 because it is the first one # we also know that the next line contains the values # we are looking for, so we read it file_line = oclu_file.readline() # we now extract the values from string sections scasm = float(file_line[1:15].replace("D", "E")) abssm = float(file_line[17:30].replace("D", "E")) extsm = float(file_line[32:45].replace("D", "E")) # we can write the average values, similarly to fort.22 # note that \n puts a new line at the end of the string output_line = "{0:15.4E}{1:15.4E}{2:15.4E}{3:15.4E}\n".format(alam, scasm, abssm, extsm) if (out22 is not None): out22.write(output_line) found_averages = True # terminate the inner loop # the averages were written. We look for CLUSTER section # using another inner loop found_differentials = False while (not found_differentials): file_line = oclu_file.readline() if (" CLUSTER" in file_line): # we found CLUSTER. We know cross-sections for # polarization state -1 will be after 2 more lines for i in range(3): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() scc31 = float(file_line[1:15].replace("D", "E")) abc31 = float(file_line[17:30].replace("D", "E")) exc31 = float(file_line[32:45].replace("D", "E")) # we can write the differential values, similarly to fort.31 output_line = "{0:15.4E}{1:15.4E}{2:15.4E}{3:15.4E}\n".format(alam, scc31, abc31, exc31) if (out31 is not None): out31.write(output_line) # we know that RAPRS values for polarization state -1 # are after 9 more lines for i in range(9): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() raprs92 = float(file_line[33:46].replace("D", "E")) # we can write the RAPRS values output_line = " ALAM={0:15.7E}, RAPRS={1:15.7E}\n".format(alam, raprs92) if (out92 is not None): out92.write(output_line) # we know the differential values for polarization # state 1 are after 9 more lines for i in range(9): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() scc32 = float(file_line[1:15].replace("D", "E")) abc32 = float(file_line[17:30].replace("D", "E")) exc32 = float(file_line[32:45].replace("D", "E")) # we can write the differential values, similarly to fort.31 output_line = "{0:15.4E}{1:15.4E}{2:15.4E}{3:15.4E}\n".format(alam, scc32, abc32, exc32) if (out32 is not None): out32.write(output_line) # we know that RAPRS values for polarization state 1 # are after 9 more lines for i in range(9): file_line = oclu_file.readline() # the following check is needed to parse C++ output if ("INSERTION" in file_line): file_line = oclu_file.readline() raprs91 = float(file_line[33:46].replace("D", "E")) # we can write the RAPRS values output_line = " ALAM={0:15.7E}, RAPRS={1:15.7E}\n".format(alam, raprs91) if (out91 is not None): out91.write(output_line) found_differentials = True # terminate the inner loop # The parsing loop ends here if (out22 is not None): out22.close() if (out31 is not None): out31.close() if (out32 is not None): out32.close() if (out91 is not None): out91.close() if (out92 is not None): out92.close() oclu_file.close() # close the OCLU file except Error as ex: print(ex) errors += 1 return errors ## \brief Print a command-line help summary. def print_help(): print(" ") print("*** PARSE RESULTS ***") print(" ") print("Parse the results of a NPTMcode model calculation.") print(" ") print("Usage: \"./parse_results.py --in INPUT --out OUTPUT [OPTIONS]\" ") print(" ") print("Valid options are: ") print("--in INPUT File containing the results of the model calculation (mandatory).") print("--out OUTPUT Root name for the output CSV data files (mandatory). ") print("--app=[SPH|CLU|INCLU] Application whose output needs to be parsed (mandatory). ") print("--format=[HDF5|LEGACY] Format of the result file to be parsed (autodetected by default).") print("--help Print this help and exit. ") print(" ") # ### PROGRAM EXECUTION ### ## \cond res = main() ## \endcond exit(res)