From d5ee20d3f9ad4dee42d2b8f5ed8c33e2c2520f8c Mon Sep 17 00:00:00 2001 From: Craig Warren Date: Wed, 10 Mar 2021 15:41:15 +0000 Subject: [PATCH] Split utilities into separate package and sub-modules. --- gprMax/utilities/__init__.py | 0 .../{utilities.py => utilities/host_info.py} | 252 +----------------- gprMax/utilities/logging.py | 102 +++++++ gprMax/utilities/utilities.py | 206 ++++++++++++++ 4 files changed, 310 insertions(+), 250 deletions(-) create mode 100644 gprMax/utilities/__init__.py rename gprMax/{utilities.py => utilities/host_info.py} (58%) create mode 100644 gprMax/utilities/logging.py create mode 100644 gprMax/utilities/utilities.py diff --git a/gprMax/utilities/__init__.py b/gprMax/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gprMax/utilities.py b/gprMax/utilities/host_info.py similarity index 58% rename from gprMax/utilities.py rename to gprMax/utilities/host_info.py index 18ceba5a..a8d6033b 100644 --- a/gprMax/utilities.py +++ b/gprMax/utilities/host_info.py @@ -1,5 +1,5 @@ # Copyright (C) 2015-2021: The University of Edinburgh -# Authors: Craig Warren and Antonis Giannopoulos +# Authors: Craig Warren, Antonis Giannopoulos, and John Hartley # # This file is part of gprMax. # @@ -16,259 +16,16 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . -import datetime -import decimal as d -import logging import os import platform import re import subprocess import sys -import textwrap -import xml.dom.minidom -from shutil import get_terminal_size import gprMax.config as config -import numpy as np import psutil -from colorama import Fore, Style, init -init() -logger = logging.getLogger(__name__) - -try: - from time import thread_time as timer_fn -except ImportError: - from time import perf_counter as timer_fn - logger.debug('"thread_time" not currently available in macOS and bug'\ - ' (https://bugs.python.org/issue36205) with "process_time", so use "perf_counter".') - - -class CustomFormatter(logging.Formatter): - """Logging Formatter to add colors and count warning / errors - (https://stackoverflow.com/a/56944256).""" - - grey = "\x1b[38;21m" - yellow = "\x1b[33;21m" - red = "\x1b[31;21m" - bold_red = "\x1b[31;1m" - reset = "\x1b[0m" - # format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)" - format = "%(message)s" - - FORMATS = { - logging.DEBUG: grey + format + reset, - logging.INFO: grey + format + reset, - logging.WARNING: yellow + format + reset, - logging.ERROR: red + format + reset, - logging.CRITICAL: bold_red + format + reset - } - - def format(self, record): - log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt) - return formatter.format(record) - - -def logging_config(name='gprMax', level=logging.INFO, log_file=False): - """Setup and configure logging. - - Args: - name (str): name of logger to create. - level (logging level): set logging level to stdout. - log_file (bool): additional logging to file. - """ - - # Adds a custom log level to the root logger - # from which new loggers are derived - BASIC_NUM = 25 - logging.addLevelName(BASIC_NUM, "BASIC") - def basic(self, message, *args, **kws): - if self.isEnabledFor(BASIC_NUM): - self._log(BASIC_NUM, message, args, **kws) - logging.Logger.basic = basic - - # Create main top-level logger - logger = logging.getLogger(name) - logger.setLevel(logging.DEBUG) - - # Config for logging to console - handler = logging.StreamHandler(sys.stdout) - formatter = CustomFormatter() - handler.setLevel(level) - handler.setFormatter(formatter) - logger.addHandler(handler) - - # Config for logging to file if required - if log_file: - filename = name + '-log-' + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - handler = logging.FileHandler(filename, mode='w') - formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s: %(message)s') - handler.setLevel(logging.DEBUG) - handler.setFormatter(formatter) - logger.addHandler(handler) - - -def get_terminal_width(): - """Get/set width of terminal being used. - - Returns: - terminalwidth (int): Terminal width - """ - - terminalwidth = get_terminal_size()[0] - if terminalwidth == 0: - terminalwidth = 100 - - return terminalwidth - - -def logo(version): - """Print gprMax logo, version, and licencing/copyright information. - - Args: - version (str): Version number. - - Returns: - (str): Containing logo, version, and licencing/copyright information. - """ - - description = '\n=== Electromagnetic modelling software based on the Finite-Difference Time-Domain (FDTD) method' - current_year = datetime.datetime.now().year - copyright = f'Copyright (C) 2015-{current_year}: The University of Edinburgh' - authors = 'Authors: Craig Warren and Antonis Giannopoulos' - licenseinfo1 = 'gprMax 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.\n' - licenseinfo2 = 'gprMax 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.' - licenseinfo3 = 'You should have received a copy of the GNU General Public License along with gprMax. If not, see www.gnu.org/licenses.' - - logo = """ www.gprmax.com __ __ - __ _ _ __ _ __| \/ | __ ___ __ - / _` | '_ \| '__| |\/| |/ _` \ \/ / - | (_| | |_) | | | | | | (_| |> < - \__, | .__/|_| |_| |_|\__,_/_/\_\\ - |___/|_| - v""" + version + '\n\n' - - str = f"{description} {'=' * (get_terminal_width() - len(description) - 1)}\n\n" - str += Fore.CYAN + f'{logo}' - str += Style.RESET_ALL + textwrap.fill(copyright, width=get_terminal_width() - 1, initial_indent=' ') + '\n' - str += textwrap.fill(authors, width=get_terminal_width() - 1, initial_indent=' ') + '\n\n' - str += textwrap.fill(licenseinfo1, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ') + '\n' - str += textwrap.fill(licenseinfo2, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ') + '\n' - str += textwrap.fill(licenseinfo3, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ') - - return str - - -def pretty_xml(roughxml): - """Nicely format XML string. - - Args: - roughxml (str): XML string to format - - Returns: - prettyxml (str): nicely formatted XML string - """ - - prettyxml = xml.dom.minidom.parseString(roughxml).toprettyxml() - # Remove the weird newline issue - prettyxml = os.linesep.join( - [s for s in prettyxml.splitlines() if s.strip()]) - - return prettyxml - - -def round_value(value, decimalplaces=0): - """Rounding function. - - Args: - value (float): Number to round. - decimalplaces (int): Number of decimal places of float to represent - rounded value. - - Returns: - rounded (int/float): Rounded value. - """ - - # Rounds to nearest integer (half values are rounded downwards) - if decimalplaces == 0: - rounded = int(d.Decimal(value).quantize(d.Decimal('1'), rounding=d.ROUND_HALF_DOWN)) - - # Rounds down to nearest float represented by number of decimal places - else: - precision = '1.{places}'.format(places='0' * decimalplaces) - rounded = float(d.Decimal(value).quantize(d.Decimal(precision), rounding=d.ROUND_FLOOR)) - - return rounded - - -def round32(value): - """Rounds up to nearest multiple of 32.""" - return int(32 * np.ceil(float(value) / 32)) - - -def fft_power(waveform, dt): - """Calculate a FFT of the given waveform of amplitude values; - converted to decibels and shifted so that maximum power is 0dB - - Args: - waveform (ndarray): time domain waveform - dt (float): time step - - Returns: - freqs (ndarray): frequency bins - power (ndarray): power - """ - - # Calculate magnitude of frequency spectra of waveform (ignore warning from taking a log of any zero values) - with np.errstate(divide='ignore'): - power = 10 * np.log10(np.abs(np.fft.fft(waveform))**2) - - # Replace any NaNs or Infs from zero division - power[np.invert(np.isfinite(power))] = 0 - - # Frequency bins - freqs = np.fft.fftfreq(power.size, d=dt) - - # Shift powers so that frequency with maximum power is at zero decibels - power -= np.amax(power) - - return freqs, power - - -def human_size(size, a_kilobyte_is_1024_bytes=False): - """Convert a file size to human-readable form. - - Args: - size (int): file size in bytes. - a_kilobyte_is_1024_bytes (boolean) - true for multiples of 1024, false for multiples of 1000. - - Returns: - Human-readable (string). - """ - - suffixes = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']} - - if size < 0: - raise ValueError('Number must be non-negative.') - - multiple = 1024 if a_kilobyte_is_1024_bytes else 1000 - for suffix in suffixes[multiple]: - size /= multiple - if size < multiple: - return '{:.3g}{}'.format(size, suffix) - - raise ValueError('Number is too large.') - - -def atoi(text): - """Converts a string into an integer.""" - return int(text) if text.isdigit() else text - - -def natural_keys(text): - """Human sorting of a string.""" - return [atoi(c) for c in re.split(r'(\d+)', text)] +from .utilities import human_size def get_host_info(): @@ -599,8 +356,3 @@ def detect_gpus(): gpus.append(gpu) return gpus - - -def timer(): - """Function to return time in fractional seconds.""" - return timer_fn() diff --git a/gprMax/utilities/logging.py b/gprMax/utilities/logging.py new file mode 100644 index 00000000..c82576d0 --- /dev/null +++ b/gprMax/utilities/logging.py @@ -0,0 +1,102 @@ +# Copyright (C) 2015-2021: The University of Edinburgh +# Authors: Craig Warren, Antonis Giannopoulos, and John Hartley +# +# This file is part of gprMax. +# +# gprMax 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. +# +# gprMax 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. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import datetime +import logging +import sys +from copy import copy + +logger = logging.getLogger(__name__) + + +# Adds a custom log level to the root logger from which new loggers are derived +BASIC_NUM = 25 +logging.addLevelName(BASIC_NUM, "BASIC") +logging.BASIC = BASIC_NUM +def basic(self, message, *args, **kws): + if self.isEnabledFor(BASIC_NUM): + self._log(BASIC_NUM, message, args, **kws) +logging.Logger.basic = basic + + +# Colour mapping for different log levels +MAPPING = { + 'DEBUG' : 37, # white + 'BASIC' : 37, # white + 'INFO' : 37, # white + 'WARNING' : 33, # yellow + 'ERROR' : 31, # red + 'CRITICAL': 41, # white on red bg +} +PREFIX = '\033[' +SUFFIX = '\033[0m' + + +class CustomFormatter(logging.Formatter): + """Logging Formatter to add colors and count warning / errors + (https://stackoverflow.com/a/46482050).""" + + def __init__(self, pattern): + logging.Formatter.__init__(self, pattern) + + def format(self, record): + colored_record = copy(record) + levelname = colored_record.levelname + seq = MAPPING.get(levelname, 37) # default white + colored_levelname = ('{0}{1}m{2}{3}').format(PREFIX, seq, levelname, SUFFIX) + colored_record.levelname = colored_levelname + return logging.Formatter.format(self, colored_record) + + +def logging_config(name='gprMax', level=logging.INFO, format_style='std', log_file=False): + """Setup and configure logging. + + Args: + name (str): name of logger to create. + level (logging level): set logging level to stdout. + format_style (str): set formatting - 'std' or 'full' + log_file (bool): additional logging to file. + """ + + # Set format style + if format_style == 'std': + format = "%(message)s" + elif format_style == 'full': + format = "%(asctime)s:%(levelname)s:%(name)s:%(lineno)d: %(message)s" + + # Create main top-level logger + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + logger.propagate = False + + # Config for logging to console + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(level) + handler.setFormatter(CustomFormatter(format)) + if (logger.hasHandlers()): + logger.handlers.clear() + logger.addHandler(handler) + + # Config for logging to file if required + if log_file: + filename = name + '-log-' + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + '.txt' + handler = logging.FileHandler(filename, mode='w') + formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s: %(message)s (%(filename)s:%(lineno)d)') + handler.setLevel(logging.DEBUG) + handler.setFormatter(formatter) + logger.addHandler(handler) \ No newline at end of file diff --git a/gprMax/utilities/utilities.py b/gprMax/utilities/utilities.py new file mode 100644 index 00000000..b3a8c084 --- /dev/null +++ b/gprMax/utilities/utilities.py @@ -0,0 +1,206 @@ +# Copyright (C) 2015-2021: The University of Edinburgh +# Authors: Craig Warren, Antonis Giannopoulos, and John Hartley +# +# This file is part of gprMax. +# +# gprMax 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. +# +# gprMax 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. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import datetime +import decimal as d +import logging +import os +import re +import textwrap +import xml.dom.minidom +from shutil import get_terminal_size + +import numpy as np +from colorama import Fore, Style, init +init() + +logger = logging.getLogger(__name__) + +try: + from time import thread_time as timer_fn +except ImportError: + from time import perf_counter as timer_fn + logger.debug('"thread_time" not currently available in macOS and bug'\ + ' (https://bugs.python.org/issue36205) with "process_time", so use "perf_counter".') + + +def get_terminal_width(): + """Get/set width of terminal being used. + + Returns: + terminalwidth (int): Terminal width + """ + + terminalwidth = get_terminal_size()[0] + if terminalwidth == 0: + terminalwidth = 100 + + return terminalwidth + + +def logo(version): + """Print gprMax logo, version, and licencing/copyright information. + + Args: + version (str): Version number. + + Returns: + (str): Containing logo, version, and licencing/copyright information. + """ + + description = '\n=== Electromagnetic modelling software based on the Finite-Difference Time-Domain (FDTD) method' + current_year = datetime.datetime.now().year + copyright = f'Copyright (C) 2015-{current_year}: The University of Edinburgh' + authors = 'Authors: Craig Warren, Antonis Giannopoulos, and John Hartley' + licenseinfo1 = 'gprMax 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.\n' + licenseinfo2 = 'gprMax 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.' + licenseinfo3 = 'You should have received a copy of the GNU General Public License along with gprMax. If not, see www.gnu.org/licenses.' + + logo = """ www.gprmax.com __ __ + __ _ _ __ _ __| \/ | __ ___ __ + / _` | '_ \| '__| |\/| |/ _` \ \/ / + | (_| | |_) | | | | | | (_| |> < + \__, | .__/|_| |_| |_|\__,_/_/\_\\ + |___/|_| + v""" + version + '\n\n' + + str = f"{description} {'=' * (get_terminal_width() - len(description) - 1)}\n\n" + str += Fore.CYAN + f'{logo}' + str += Style.RESET_ALL + textwrap.fill(copyright, width=get_terminal_width() - 1, initial_indent=' ') + '\n' + str += textwrap.fill(authors, width=get_terminal_width() - 1, initial_indent=' ') + '\n\n' + str += textwrap.fill(licenseinfo1, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ') + '\n' + str += textwrap.fill(licenseinfo2, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ') + '\n' + str += textwrap.fill(licenseinfo3, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ') + + return str + + +def pretty_xml(roughxml): + """Nicely format XML string. + + Args: + roughxml (str): XML string to format + + Returns: + prettyxml (str): nicely formatted XML string + """ + + prettyxml = xml.dom.minidom.parseString(roughxml).toprettyxml() + # Remove the weird newline issue + prettyxml = os.linesep.join( + [s for s in prettyxml.splitlines() if s.strip()]) + + return prettyxml + + +def round_value(value, decimalplaces=0): + """Rounding function. + + Args: + value (float): Number to round. + decimalplaces (int): Number of decimal places of float to represent + rounded value. + + Returns: + rounded (int/float): Rounded value. + """ + + # Rounds to nearest integer (half values are rounded downwards) + if decimalplaces == 0: + rounded = int(d.Decimal(value).quantize(d.Decimal('1'), rounding=d.ROUND_HALF_DOWN)) + + # Rounds down to nearest float represented by number of decimal places + else: + precision = '1.{places}'.format(places='0' * decimalplaces) + rounded = float(d.Decimal(value).quantize(d.Decimal(precision), rounding=d.ROUND_FLOOR)) + + return rounded + + +def round32(value): + """Rounds up to nearest multiple of 32.""" + return int(32 * np.ceil(float(value) / 32)) + + +def fft_power(waveform, dt): + """Calculate a FFT of the given waveform of amplitude values; + converted to decibels and shifted so that maximum power is 0dB + + Args: + waveform (ndarray): time domain waveform + dt (float): time step + + Returns: + freqs (ndarray): frequency bins + power (ndarray): power + """ + + # Calculate magnitude of frequency spectra of waveform (ignore warning from taking a log of any zero values) + with np.errstate(divide='ignore'): + power = 10 * np.log10(np.abs(np.fft.fft(waveform))**2) + + # Replace any NaNs or Infs from zero division + power[np.invert(np.isfinite(power))] = 0 + + # Frequency bins + freqs = np.fft.fftfreq(power.size, d=dt) + + # Shift powers so that frequency with maximum power is at zero decibels + power -= np.amax(power) + + return freqs, power + + +def human_size(size, a_kilobyte_is_1024_bytes=False): + """Convert a file size to human-readable form. + + Args: + size (int): file size in bytes. + a_kilobyte_is_1024_bytes (boolean) - true for multiples of 1024, false for multiples of 1000. + + Returns: + Human-readable (string). + """ + + suffixes = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']} + + if size < 0: + raise ValueError('Number must be non-negative.') + + multiple = 1024 if a_kilobyte_is_1024_bytes else 1000 + for suffix in suffixes[multiple]: + size /= multiple + if size < multiple: + return '{:.3g}{}'.format(size, suffix) + + raise ValueError('Number is too large.') + + +def atoi(text): + """Converts a string into an integer.""" + return int(text) if text.isdigit() else text + + +def natural_keys(text): + """Human sorting of a string.""" + return [atoi(c) for c in re.split(r'(\d+)', text)] + + +def timer(): + """Function to return time in fractional seconds.""" + return timer_fn()