Source code for mesh2hrtf.Output2HRTF.write_output_report

import os
import re
import numpy as np
import glob
import json


[docs]def write_output_report(folder=None): r""" Generate project report from NumCalc output files. NumCalc (Mesh2HRTF's numerical core) writes information about the simulations to the files `NC*.out` located under `NumCalc/source_*`. The file `NC.out` exists if NumCalc was ran without the additional command line parameters ``-istart`` and ``-iend``. If these parameters were used, there is at least one `NC\*-\*.out`. If this is the case, information from `NC\*-\*.out` overwrites information from NC.out in the project report. .. note:: The project reports are written to the files `Output2HRTF/report_source_*.csv`. If issues were detected, they are listed in `Output2HRTF/report_issues.csv`. The report contain the following information Frequency step The index of the frequency. Frequency in Hz The frequency in Hz. NC input Name of the input file from which the information was taken. Input check passed Contains a 1 if the check of the input data passed and a 0 otherwise. If the check failed for one frequency, the following frequencies might be affected as well. Converged Contains a 1 if the simulation converged and a 0 otherwise. If the simulation did not converge, the relative error might be high. Num. iterations The number of iterations that were required to converge relative error The relative error of the final simulation Comp. time total The total computation time in seconds Comp. time assembling The computation time for assembling the matrices in seconds Comp. time solving The computation time for solving the matrices in seconds Comp. time post-proc The computation time for post-processing the results in seconds Parameters ---------- folder : str, optional The path of the Mesh2HRTF project folder, i.e., the folder containing the subfolders EvaluationsGrids, NumCalc, and ObjectMeshes. The default, ``None`` uses the current working directory. Returns ------- found_issues : bool ``True`` if issues were found, ``False`` otherwise report : str The report or an empty string if no issues were found """ if folder is None: folder = os.getcwd() # get sources and number of sources and frequencies sources = glob.glob(os.path.join(folder, "NumCalc", "source_*")) num_sources = len(sources) with open(os.path.join(folder, "parameters.json"), "r") as file: params = json.load(file) # sort source files (not read in correct order in some cases) nums = [int(source.split("_")[-1]) for source in sources] sources = np.array(sources) sources = sources[np.argsort(nums)] # parse all NC*.out files for all sources all_files, fundamentals, out, out_names = _parse_nc_out_files( sources, num_sources, params["numFrequencies"]) # write report as csv file _write_project_reports(folder, all_files, out, out_names) # look for errors report = _check_project_report(folder, fundamentals, out) found_issues = True if report else False return found_issues, report
def _parse_nc_out_files(sources, num_sources, num_frequencies): """ Parse all NC*.out files for all sources. This function should never raise a value error, regardless of how mess NC*.out files are. Looking for error is done at a later step. Parameters ---------- sources : list of strings full path to the source folders num_sources : int number of sources - len(num_sources) num_frequencies : int number of frequency steps Returns ------- out : numpy array containing the extracted information for each frequency step out_names : list of string verbal information about the columns of `out` """ # array for reporting fundamental errors fundamentals = [] all_files = [] # array for saving the detailed report out_names = ["frequency step", # 0 "frequency in Hz", # 1 "NC input file", # 2 "Input check passed", # 3 "Converged", # 4 "Num. iterations", # 5 "relative error", # 6 "Comp. time total", # 7 "Comp. time assembling", # 8 "Comp. time solving", # 9 "Comp. time post-proc."] # 10 out = -np.ones((num_frequencies, 11, num_sources)) # values for steps out[:, 0] = np.arange(1, num_frequencies + 1)[..., np.newaxis] # values indicating failed input check and non-convergence out[:, 3] = 0 out[:, 4] = 0 # regular expression for finding a number that can be int or float re_number = r"(\d+(?:\.\d+)?)" # loop sources for ss, source in enumerate(sources): # list of NC*.out files for parsing files = glob.glob(os.path.join(source, "NC*.out")) # make sure that NC.out is first nc_out = os.path.join(source, "NC.out") if nc_out in files and files.index(nc_out): files = [files.pop(files.index(nc_out))] + files # update fundamentals fundamentals.append([0 for f in range(len(files))]) all_files.append([os.path.basename(f) for f in files]) # get content from all NC*.out for ff, file in enumerate(files): # read the file and join all lines with open(file, "r") as f_id: lines = f_id.readlines() lines = "".join(lines) # split header and steps lines = lines.split( ">> S T E P N U M B E R A N D F R E Q U E N C Y <<") # look for fundamental errors if len(lines) == 1: fundamentals[ss][ff] = 1 continue # parse frequencies (skip header) for line in lines[1:]: # find frequency step idx = re.search(r'Step \d+,', line) if idx: step = int(line[idx.start()+5:idx.end()-1]) # write number of input file (replaced by string later) out[step-1, 2, ss] = ff # find frequency idx = re.search(f'Frequency = {re_number} Hz', line) if idx: out[step-1, 1, ss] = float( line[idx.start()+12:idx.end()-3]) # check if the input data was ok if "Too many integral points in the theta" not in line: out[step-1, 3, ss] = 1 # check and write convergence if 'Maximum number of iterations is reached!' not in line: out[step-1, 4, ss] = 1 # check iterations idx = re.search(r'number of iterations = \d+,', line) if idx: out[step-1, 5, ss] = int(line[idx.start()+23:idx.end()-1]) # check relative error idx = re.search('relative error = .+', line) if idx: out[step-1, 6, ss] = float(line[idx.start()+17:idx.end()]) # check time stats # -- assembling idx = re.search( r'Assembling the equation system : \d+', line) if idx: out[step-1, 8, ss] = float(line[idx.start()+41:idx.end()]) # -- solving idx = re.search( r'Solving the equation system : \d+', line) if idx: out[step-1, 9, ss] = float(line[idx.start()+41:idx.end()]) # -- post-pro idx = re.search( r'Post processing : \d+', line) if idx: out[step-1, 10, ss] = float(line[idx.start()+41:idx.end()]) # -- total idx = re.search( r'Total : \d+', line) if idx: out[step-1, 7, ss] = float(line[idx.start()+41:idx.end()]) return all_files, fundamentals, out, out_names def _write_project_reports(folder, all_files, out, out_names): """ Write project report to disk at folder/Output2HRTF/report_source_*.csv For description of input parameter refer to write_output_report and _parse_nc_out_files """ # loop sources for ss in range(out.shape[2]): report = ", ".join(out_names) + "\n" # loop frequencies for ff in range(out.shape[0]): f = out[ff, :, ss] report += ( f"{int(f[0])}, " # frequency step f"{float(f[1])}, " # frequency in Hz f"{all_files[ss][int(f[2])]}," # NC*.out file f"{int(f[3])}, " # input check f"{int(f[4])}, " # convergence f"{int(f[5])}, " # number of iterations f"{float(f[6])}, " # relative error f"{int(f[7])}, " # total computation time f"{int(f[8])}, " # assembling equations time f"{int(f[9])}, " # solving equations time f"{int(f[10])}\n" # post-processing time ) # write to disk report_name = os.path.join( folder, "Output2HRTF", f"report_source_{ss + 1}.csv") with open(report_name, "w") as f_id: f_id.write(report) def _check_project_report(folder, fundamentals, out): # return if there are no fundamental errors or other issues if not all([all(f) for f in fundamentals]) and not np.any(out == -1) \ and np.all(out[:, 3:5]): return # report detailed errors report = "" for ss in range(out.shape[2]): # currently we detect frequencies that were not calculated and # frequencies with convergence issues missing = "Frequency steps that were not calculated:\n" input_test = "Frequency steps with bad input:\n" convergence = "Frequency steps that did not converge:\n" any_missing = False any_input_failed = False any_convergence = False # loop frequencies for ff in range(out.shape[0]): f = out[ff, :, ss] # no value for frequency if f[1] == -1: any_missing = True missing += f"{int(f[0])}, " continue # input data failed if f[3] == 0: any_input_failed = True input_test += f"{int(f[0])}, " # convergence value is zero if f[4] == 0: any_convergence = True convergence += f"{int(f[0])}, " if any_missing or any_input_failed or any_convergence: report += f"Detected issues for source {ss+1}\n" report += "----------------------------\n" if any_missing: report += missing[:-2] + "\n\n" if any_input_failed: report += input_test[:-2] + "\n\n" if any_convergence: report += convergence[:-2] + "\n\n" if not report: report = ("\nDetected unknown issues\n" "-----------------------\n" "Check the project reports in Output2HRTF,\n" "and the NC*.out files in NumCalc/source_*\n\n") report += ("For more information check Output2HRTF/report_source_*.csv " "and the NC*.out files located at NumCalc/source_*") # write to disk report_name = os.path.join( folder, "Output2HRTF", "report_issues.txt") with open(report_name, "w") as f_id: f_id.write(report) return report