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]
# 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
else:
out[step-1, 3, ss] = 0
# check and write convergence
if 'Maximum number of iterations is reached!' not in line:
out[step-1, 4, ss] = 1
else:
out[step-1, 4, ss] = 0
# 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