Commit cde330cd authored by Giovanni La Mura's avatar Giovanni La Mura
Browse files

Create output parsing script

parent 7289e202
Loading
Loading
Loading
Loading
+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)