From c11dbd76718387038943822db366dd6147ef4d17 Mon Sep 17 00:00:00 2001 From: Craig Warren Date: Thu, 10 Oct 2019 16:45:25 +0100 Subject: [PATCH] Incomplete mods and updates as discussed will John. Not implemented new config yet. --- gprMax/cmds_geometry/add_grass.py | 13 +- gprMax/cmds_geometry/add_surface_roughness.py | 15 +- gprMax/cmds_geometry/add_surface_water.py | 10 +- gprMax/cmds_multiple.py | 320 ++++++++---------- gprMax/cmds_single_use.py | 78 ++--- gprMax/config.pxd | 6 - gprMax/config.py | 123 +++---- gprMax/contexts.py | 36 +- gprMax/cython/fields_updates_hsg.pyx | 5 +- gprMax/cython/fields_updates_normal.pyx | 2 +- gprMax/cython/geometry_outputs.pyx | 1 + gprMax/cython/geometry_primitives.pyx | 5 +- .../cython/pml_updates_electric_HORIPML.pyx | 6 +- gprMax/cython/pml_updates_electric_MRIPML.pyx | 8 +- gprMax/cython/snapshots.pyx | 2 - gprMax/cython/yee_cell_build.pyx | 2 +- gprMax/cython/yee_cell_setget_rigid.pxd | 12 +- gprMax/cython/yee_cell_setget_rigid.pyx | 12 +- gprMax/gprMax.py | 4 + gprMax/grid.py | 251 +++++++------- gprMax/input_cmds_file.py | 2 +- gprMax/model_build_run.py | 61 ++-- gprMax/pml.py | 2 +- gprMax/solvers.py | 10 +- gprMax/subgrids/base.py | 10 - gprMax/subgrids/multi.py | 4 - gprMax/subgrids/precursor_nodes.py | 2 +- .../fields_updates_dispersive_template | 1 + gprMax/updates.py | 50 +-- gprMax/utilities.py | 36 +- setup.py | 22 +- 31 files changed, 537 insertions(+), 574 deletions(-) diff --git a/gprMax/cmds_geometry/add_grass.py b/gprMax/cmds_geometry/add_grass.py index cdaab566..32d2387f 100644 --- a/gprMax/cmds_geometry/add_grass.py +++ b/gprMax/cmds_geometry/add_grass.py @@ -16,8 +16,9 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . +import logging + import numpy as np -from tqdm import tqdm import gprMax.config as config from .cmds_geometry import UserObjectGeometry @@ -28,6 +29,8 @@ from ..materials import Material from ..utilities import round_value +log = logging.getLogger(__name__) + class AddGrass(UserObjectGeometry): """Allows you to add grass with roots to a :class:`gprMax.cmds_geometry.fractal_box.FractalBox` in the model. @@ -68,12 +71,12 @@ class AddGrass(UserObjectGeometry): except KeyError: seed = None - # grab the correct fractal volume + # Get the correct fractal volume volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id] if volumes: volume = volumes[0] else: - raise CmdInputError(self.__str__() + ' Cant find FractalBox {}'.format(fractal_box_id)) + raise CmdInputError(self.__str__() + f' cannot find FractalBox {fractal_box_id}') p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 @@ -165,7 +168,7 @@ class AddGrass(UserObjectGeometry): # Set the fractal surface using the pre-calculated spatial distribution and a random height surface.fractalsurface = np.zeros((surface.fractalsurface.shape[0], surface.fractalsurface.shape[1])) for i in range(len(bladesindex[0])): - surface.fractalsurface[bladesindex[0][i], bladesindex[1][i]] = R.randint(surface.fractalrange[0], surface.fractalrange[1], size=1) + surface.fractalsurface[bladesindex[0][i], bladesindex[1][i]] = R.randint(surface.fractalrange[0], surface.fractalrange[1], size=1) # Create grass geometry parameters g = Grass(n_blades) @@ -193,4 +196,4 @@ class AddGrass(UserObjectGeometry): volume.fractalsurfaces.append(surface) if config.is_messages(): - tqdm.write('{} blades of grass on surface from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m with fractal dimension {:g}, fractal seeding {}, and range {:g}m to {:g}m, added to {}.'.format(n_blades, xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, surface.dimension, surface.seed, limits[0], limits[1], surface.operatingonID)) + tqdm.write(f'{n_blades} blades of grass on surface from {xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m with fractal dimension {surface.dimension:g}, fractal seeding {surface.seed}, and range {limits[0]:g}m to {limits[1]:g}m, added to {surface.operatingonID}.') diff --git a/gprMax/cmds_geometry/add_surface_roughness.py b/gprMax/cmds_geometry/add_surface_roughness.py index 0e8d815e..04c99532 100644 --- a/gprMax/cmds_geometry/add_surface_roughness.py +++ b/gprMax/cmds_geometry/add_surface_roughness.py @@ -16,8 +16,9 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . +import logging + import numpy as np -from tqdm import tqdm import gprMax.config as config from .cmds_geometry import UserObjectGeometry @@ -26,6 +27,8 @@ from ..exceptions import CmdInputError from ..fractals import FractalSurface from ..utilities import round_value +log = logging.getLogger(__name__) + class AddSurfaceRoughness(UserObjectGeometry): """Allows you to add grass with roots to a :class:`gprMax.cmds_geometry.fractal_box.FractalBox` in the model. @@ -62,19 +65,19 @@ class AddSurfaceRoughness(UserObjectGeometry): limits = np.array(self.kwargs['limits']) fractal_box_id = self.kwargs['fractal_box_id'] except KeyError: - raise CmdInputError(self.__str__() + ' Incorrect parameters') + raise CmdInputError(self.__str__() + ' incorrect parameters') try: seed = self.kwargs['seed'] except KeyError: seed = None - # grab the correct fractal volume + # Get the correct fractal volume volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id] if volumes: volume = volumes[0] else: - raise CmdInputError(self.__str__() + ' Cant find FractalBox {}'.format(fractal_box_id)) + raise CmdInputError(self.__str__() + ' cannot find FractalBox {fractal_box_id}') p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 @@ -152,10 +155,10 @@ class AddSurfaceRoughness(UserObjectGeometry): # List of existing surfaces IDs existingsurfaceIDs = [x.surfaceID for x in volume.fractalsurfaces] if surface.surfaceID in existingsurfaceIDs: - raise CmdInputError(self.__str__() + ' has already been used on the {} surface'.format(surface.surfaceID)) + raise CmdInputError(self.__str__() + f' has already been used on the {surface.surfaceID} surface') surface.generate_fractal_surface(grid) volume.fractalsurfaces.append(surface) if config.is_messages(): - tqdm.write('Fractal surface from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m with fractal dimension {:g}, fractal weightings {:g}, {:g}, fractal seeding {}, and range {:g}m to {:g}m, added to {}.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, surface.dimension, surface.weighting[0], surface.weighting[1], surface.seed, limits[0], limits[1], surface.operatingonID)) + log.info(f'Fractal surface from {xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m with fractal dimension {surface.dimension:g}, fractal weightings {surface.weighting[0]:g}, {surface.weighting[1]:g}, fractal seeding {surface.seed}, and range {limits[0]:g}m to {limits[1]:g}m, added to {surface.operatingonID}.') diff --git a/gprMax/cmds_geometry/add_surface_water.py b/gprMax/cmds_geometry/add_surface_water.py index 654e566c..e549b06e 100644 --- a/gprMax/cmds_geometry/add_surface_water.py +++ b/gprMax/cmds_geometry/add_surface_water.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . -from tqdm import tqdm +import logging import gprMax.config as config from .cmds_geometry import UserObjectGeometry @@ -25,6 +25,8 @@ from ..materials import Material from ..utilities import round_value +log = logging.getLogger(__name__) + class AddSurfaceWater(UserObjectGeometry): """Allows you to add surface water to a :class:`gprMax.cmds_geometry.fractal_box.FractalBox` in the model. @@ -54,12 +56,12 @@ class AddSurfaceWater(UserObjectGeometry): except KeyError: raise CmdInputError(self.__str__() + ' requires exactly eight parameters') - # grab the correct fractal volume + # Get the correct fractal volume volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id] if volumes: volume = volumes[0] else: - raise CmdInputError(self.__str__() + ' Cant find FractalBox {}'.format(fractal_box_id)) + raise CmdInputError(self.__str__() + f' cannot find FractalBox {fractal_box_id}') p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 @@ -143,4 +145,4 @@ class AddSurfaceWater(UserObjectGeometry): raise CmdInputError(self.__str__() + ' requires the time step for the model to be less than the relaxation time required to model water.') if config.is_messages(): - tqdm.write('Water on surface from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m with depth {:g}m, added to {}.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, filldepth, surface.operatingonID)) + log.info('Water on surface from {xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m with depth {filldepth:g}m, added to {surface.operatingonID}.') diff --git a/gprMax/cmds_multiple.py b/gprMax/cmds_multiple.py index f67ff313..7fe1624a 100644 --- a/gprMax/cmds_multiple.py +++ b/gprMax/cmds_multiple.py @@ -15,14 +15,13 @@ # # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . + +import logging import sys + import numpy as np -from tqdm import tqdm import gprMax.config as config -from .config import z0 -from .config import dtypes - from .cmds_geometry.cmds_geometry import UserObjectGeometry from .exceptions import CmdInputError from .geometry_outputs import GeometryObjects as GeometryObjectsUser @@ -40,18 +39,16 @@ from .subgrids.base import SubGridBase from .utilities import round_value from .waveforms import Waveform as WaveformUser -floattype = dtypes['float_or_double'] +log = logging.getLogger(__name__) class UserObjectMulti: """Object that can occur multiple times in a model.""" def __init__(self, **kwargs): - """Constructor.""" self.kwargs = kwargs self.order = None self.hash = '#example' - # auto translate self.autotranslate = True @@ -63,7 +60,7 @@ class UserObjectMulti: v = ' '.join([str(el) for el in v]) s += str(v) + ' ' - return '{}: {}'.format(self.hash, s[:-1]) + return f'{self.hash}: {s[:-1]}' def create(self, grid, uip): """Create the object and add it to the grid.""" @@ -96,16 +93,15 @@ class Waveform(UserObjectMulti): amp = self.kwargs['amp'] freq = self.kwargs['freq'] ID = self.kwargs['id'] - except KeyError: - raise CmdInputError(self.params_str() + ' requires exactly four parameters') + raise CmdInputError(f"'{self.params_str()}' requires exactly four parameters") if wavetype not in WaveformUser.types: - raise CmdInputError(self.__str__() + ' must have one of the following types {}'.format(','.join(WaveformUser.types))) + raise CmdInputError(f"'{self.params_str()}' must have one of the following types {','.join(WaveformUser.types)}") if freq <= 0: - raise CmdInputError(self.__str__() + ' requires an excitation frequency value of greater than zero') + raise CmdInputError(f"'{self.params_str()}' requires an excitation frequency value of greater than zero") if any(x.ID == ID for x in grid.waveforms): - raise CmdInputError(self.__str__() + ' with ID {} already exists'.format(ID)) + raise CmdInputError(f"'{self.params_str()}' with ID {ID} already exists") w = WaveformUser() w.ID = ID @@ -113,8 +109,7 @@ class Waveform(UserObjectMulti): w.amp = amp w.freq = freq - if config.is_messages(): - print('Waveform {} of type {} with maximum amplitude scaling {:g}, frequency {:g}Hz created.'.format(w.ID, w.type, w.amp, w.freq)) + log.info(f'Waveform {w.ID} of type {w.type} with maximum amplitude scaling {w.amp:g}, frequency {w.freq:g}Hz created.') grid.waveforms.append(w) @@ -142,34 +137,32 @@ class VoltageSource(UserObjectMulti): self.hash = '#voltage_source' def create(self, grid, uip): - """Create voltage source and add it to the grid.""" try: p1 = self.kwargs['p1'] polarisation = self.kwargs['polarisation'].lower() resistance = self.kwargs['resistance'] waveform_id = self.kwargs['waveform_id'] - except KeyError: - raise CmdInputError(self.__str__() + " requires at least six parameters") + raise CmdInputError(f"'{self.__str__()}' requires at least six parameters") # Check polarity & position parameters if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be x, y, or z") if '2D TMx' in grid.mode and (polarisation == 'y' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be x in 2D TMx mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be x in 2D TMx mode") elif '2D TMy' in grid.mode and (polarisation == 'x' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be y in 2D TMy mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be y in 2D TMy mode") elif '2D TMz' in grid.mode and (polarisation == 'x' or polarisation == 'y'): - raise CmdInputError("'{}' polarisation must be z in 2D TMz mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be z in 2D TMz mode") xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.__str__()) if resistance < 0: - raise CmdInputError("'{}' requires a source resistance of zero or greater".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' requires a source resistance of zero or greater") # Check if there is a waveformID in the waveforms list if not any(x.ID == waveform_id for x in grid.waveforms): - raise CmdInputError("'{}' there is no waveform with the identifier {}'.format(tmp[5]".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' there is no waveform with the identifier {tmp[5]}") v = VoltageSourceUser() v.polarisation = polarisation @@ -185,17 +178,17 @@ class VoltageSource(UserObjectMulti): stop = self.kwargs['stop'] # Check source start & source remove time parameters if start < 0: - raise CmdInputError("'{}' delay of the initiation of the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' delay of the initiation of the source should not be less than zero") if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' time to remove the source should not be less than zero") if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' duration of the source should not be zero or less") v.start = start if stop > grid.timewindow: v.stop = grid.timewindow else: v.stop = stop - startstop = ' start time {:g} secs, finish time {:g} secs '.format(v.start, v.stop) + startstop = f' start time {v.start:g} secs, finish time {v.stop:g} secs ' except KeyError: v.start = 0 v.stop = grid.timewindow @@ -203,8 +196,7 @@ class VoltageSource(UserObjectMulti): v.calculate_waveform_values(grid) - if config.is_messages(): - print('Voltage source with polarity {} at {:g}m, {:g}m, {:g}m, resistance {:.1f} Ohms,'.format(v.polarisation, v.xcoord * grid.dx, v.ycoord * grid.dy, v.zcoord * grid.dz, v.resistance) + startstop + 'using waveform {} created.'.format(v.waveformID)) + log.info('Voltage source with polarity {} at {:g}m, {:g}m, {:g}m, resistance {:.1f} Ohms,'.format(v.polarisation, v.xcoord * grid.dx, v.ycoord * grid.dy, v.zcoord * grid.dz, v.resistance) + startstop + 'using waveform {} created.'.format(v.waveformID)) grid.voltagesources.append(v) @@ -226,36 +218,33 @@ class HertzianDipole(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 2 self.hash = '#hertzian_dipole' def create(self, grid, uip): - """Create HertzianDipole and add it to the grid.""" try: polarisation = self.kwargs['polarisation'].lower() p1 = self.kwargs['p1'] waveform_id = self.kwargs['waveform_id'] - except KeyError: - raise CmdInputError("'{}' requires at least 3 parameters".format(self.params_str())) + raise CmdInputError(f"'{self.__str__()}' requires at least 3 parameters") # Check polarity & position parameters if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be x, y, or z") if '2D TMx' in grid.mode and (polarisation == 'y' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be x in 2D TMx mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be x in 2D TMx mode") elif '2D TMy' in grid.mode and (polarisation == 'x' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be y in 2D TMy mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be y in 2D TMy mode") elif '2D TMz' in grid.mode and (polarisation == 'x' or polarisation == 'y'): - raise CmdInputError("'{}' polarisation must be z in 2D TMz mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be z in 2D TMz mode") xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.__str__()) # Check if there is a waveformID in the waveforms list if not any(x.ID == waveform_id for x in grid.waveforms): - raise CmdInputError("'{}' there is no waveform with the identifier {}'.format(tmp[4]".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' there is no waveform with the identifier {tmp[4]}') h = HertzianDipoleUser() h.polarisation = polarisation @@ -282,17 +271,17 @@ class HertzianDipole(UserObjectMulti): start = self.kwargs['start'] stop = self.kwargs['stop'] if start < 0: - raise CmdInputError("'{}' delay of the initiation of the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' delay of the initiation of the source should not be less than zero") if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()self.__str__()}' time to remove the source should not be less than zero") if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' duration of the source should not be zero or less") h.start = start if stop > grid.timewindow: h.stop = grid.timewindow else: h.stop = stop - startstop = ' start time {:g} secs, finish time {:g} secs '.format(h.start, h.stop) + startstop = f' start time {h.start:g} secs, finish time {h.stop:g} secs ' except KeyError: h.start = 0 h.stop = grid.timewindow @@ -300,11 +289,10 @@ class HertzianDipole(UserObjectMulti): h.calculate_waveform_values(grid) - if config.is_messages(): - if grid.mode == '2D': - print('Hertzian dipole is a line source in 2D with polarity {} at {:g}m, {:g}m, {:g}m,'.format(h.polarisation, h.xcoord * grid.dx, h.ycoord * grid.dy, h.zcoord * grid.dz) + startstop + 'using waveform {} created.'.format(h.waveformID)) - else: - print('Hertzian dipole with polarity {} at {:g}m, {:g}m, {:g}m,'.format(h.polarisation, h.xcoord * grid.dx, h.ycoord * grid.dy, h.zcoord * grid.dz) + startstop + 'using waveform {} created.'.format(h.waveformID)) + if grid.mode == '2D': + log.info(f'Hertzian dipole is a line source in 2D with polarity {h.polarisation} at {h.xcoord * grid.dx:g}m, {h.ycoord * grid.dy:g}m, {h.zcoord * grid.dz:g}m,' + startstop + f'using waveform {h.waveformID} created.') + else: + log.info(f'Hertzian dipole with polarity {h.polarisation} at {h.xcoord * grid.dx:g}m, {h.ycoord * grid.dy:g}m, {h.zcoord * grid.dz:g}m,' + startstop + f'using waveform {h.waveformID} created.') grid.hertziandipoles.append(h) @@ -326,35 +314,33 @@ class MagneticDipole(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 3 self.hash = '#magnetic_dipole' def create(self, grid, uip): - """Create Magnetic Dipole and add it the grid.""" try: polarisation = self.kwargs['polarisation'].lower() p1 = self.kwargs['p1'] waveform_id = self.kwargs['waveform_id'] except KeyError: - raise CmdInputError("'{}' requires at least five parameters".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' requires at least five parameters") # Check polarity & position parameters if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be x, y, or z") if '2D TMx' in grid.mode and (polarisation == 'y' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be x in 2D TMx mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be x in 2D TMx mode") elif '2D TMy' in grid.mode and (polarisation == 'x' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be y in 2D TMy mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be y in 2D TMy mode") elif '2D TMz' in grid.mode and (polarisation == 'x' or polarisation == 'y'): - raise CmdInputError("'{}' polarisation must be z in 2D TMz mode".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' polarisation must be z in 2D TMz mode") xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.__str__()) # Check if there is a waveformID in the waveforms list if not any(x.ID == waveform_id for x in grid.waveforms): - raise CmdInputError("'{}' there is no waveform with the identifier {}".format(self.__str__(), waveform_id)) + raise CmdInputError(f"'{self.__str__()}' there is no waveform with the identifier {waveform_id}") m = MagneticDipoleUser() m.polarisation = polarisation @@ -372,17 +358,17 @@ class MagneticDipole(UserObjectMulti): start = self.kwargs['start'] stop = self.kwargs['stop'] if start < 0: - raise CmdInputError("'{}' delay of the initiation of the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' delay of the initiation of the source should not be less than zero") if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' time to remove the source should not be less than zero") if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' duration of the source should not be zero or less") m.start = start if stop > grid.timewindow: m.stop = grid.timewindow else: m.stop = stop - startstop = ' start time {:g} secs, finish time {:g} secs '.format(m.start, m.stop) + startstop = f' start time {m.start:g} secs, finish time {m.stop:g} secs ' except KeyError: m.start = 0 m.stop = grid.timewindow @@ -390,8 +376,7 @@ class MagneticDipole(UserObjectMulti): m.calculate_waveform_values(grid) - if config.is_messages(): - print('Magnetic dipole with polarity {} at {:g}m, {:g}m, {:g}m,'.format(m.polarisation, m.xcoord * grid.dx, m.ycoord * grid.dy, m.zcoord * grid.dz) + startstop + 'using waveform {} created.'.format(m.waveformID)) + log.info(f'Magnetic dipole with polarity {m.polarisation} at {m.xcoord * grid.dx:g}m, {m.ycoord * grid.dy:g}m, {m.zcoord * grid.dz:g}m,' + startstop + f'using waveform {m.waveformID} created.') grid.magneticdipoles.append(m) @@ -415,44 +400,41 @@ class TransmissionLine(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 4 self.hash = '#transmission_line' def create(self, grid, uip): - try: polarisation = self.kwargs['polarisation'].lower() p1 = self.kwargs['p1'] waveform_id = self.kwargs['waveform_id'] resistance = self.kwargs['resistance'] - except KeyError: - raise CmdInputError("'{}' requires at least six parameters".format(self.params_str())) + raise CmdInputError(f"'{self.params_str()}' requires at least six parameters") # Warn about using a transmission line on GPU if grid.gpu is not None: - raise CmdInputError("'{}' A #transmission_line cannot currently be used with GPU solving. Consider using a #voltage_source instead.".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' A #transmission_line cannot currently be used with GPU solving. Consider using a #voltage_source instead.") # Check polarity & position parameters if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' polarisation must be x, y, or z") if '2D TMx' in grid.mode and (polarisation == 'y' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be x in 2D TMx mode".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' polarisation must be x in 2D TMx mode") elif '2D TMy' in grid.mode and (polarisation == 'x' or polarisation == 'z'): - raise CmdInputError("'{}' polarisation must be y in 2D TMy mode".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' polarisation must be y in 2D TMy mode") elif '2D TMz' in grid.mode and (polarisation == 'x' or polarisation == 'y'): - raise CmdInputError("'{}' polarisation must be z in 2D TMz mode".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' polarisation must be z in 2D TMz mode") xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.__str__()) if resistance <= 0 or resistance >= z0: - raise CmdInputError("'{}' requires a resistance greater than zero and less than the impedance of free space, i.e. 376.73 Ohms".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires a resistance greater than zero and less than the impedance of free space, i.e. 376.73 Ohms") # Check if there is a waveformID in the waveforms list if not any(x.ID == waveform_id for x in grid.waveforms): - raise CmdInputError("'{}' there is no waveform with the identifier {}'.format(tmp[5]".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' there is no waveform with the identifier {tmp[5]}") t = TransmissionLineUser(grid) t.polarisation = polarisation @@ -468,17 +450,17 @@ class TransmissionLine(UserObjectMulti): start = self.kwargs['start'] stop = self.kwargs['stop'] if start < 0: - raise CmdInputError("'{}' delay of the initiation of the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' delay of the initiation of the source should not be less than zero") if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' time to remove the source should not be less than zero") if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' duration of the source should not be zero or less") t.start = start if stop > grid.timewindow: t.stop = grid.timewindow else: t.stop = stop - startstop = ' start time {:g} secs, finish time {:g} secs '.format(t.start, t.stop) + startstop = f' start time {t.start:g} secs, finish time {t.stop:g} secs ' except KeyError: t.start = 0 t.stop = grid.timewindow @@ -487,8 +469,7 @@ class TransmissionLine(UserObjectMulti): t.calculate_waveform_values(grid) t.calculate_incident_V_I(grid) - if config.is_messages(): - print('Transmission line with polarity {} at {:g}m, {:g}m, {:g}m, resistance {:.1f} Ohms,'.format(t.polarisation, t.xcoord * grid.dx, t.ycoord * grid.dy, t.zcoord * grid.dz, t.resistance) + startstop + 'using waveform {} created.'.format(t.waveformID)) + log.info(f'Transmission line with polarity {t.polarisation} at {t.xcoord * grid.dx:g}m, {t.ycoord * grid.dy:g}m, {t.zcoord * grid.dz:g}m, resistance {t.resistance:.1f} Ohms,' + startstop + f'using waveform {t.waveformID} created.') grid.transmissionlines.append(t) @@ -508,7 +489,6 @@ class Rx(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 5 self.hash = '#rx' @@ -518,7 +498,7 @@ class Rx(UserObjectMulti): try: p1 = self.kwargs['p1'] except KeyError: - raise CmdInputError("'{}' has an incorrect number of parameters".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' has an incorrect number of parameters") p = uip.check_src_rx_point(p1, self.__str__()) @@ -537,17 +517,16 @@ class Rx(UserObjectMulti): # Check and add field output names for field in outputs: if field in allowableoutputs: - r.outputs[field] = np.zeros(grid.iterations, dtype=floattype) + r.outputs[field] = np.zeros(grid.iterations, dtype=config.dtypes['float_or_double']) else: - raise CmdInputError("{} contains an output type that is not allowable. Allowable outputs in current context are {}".format(self.__str__(), allowableoutputs)) - + raise CmdInputError(f"'{self.__str__()}' contains an output type that is not allowable. Allowable outputs in current context are {allowableoutputs}") # If no ID or outputs are specified, use default except KeyError: r.ID = r.__class__.__name__ + '(' + str(r.xcoord) + ',' + str(r.ycoord) + ',' + str(r.zcoord) + ')' for key in RxUser.defaultoutputs: - r.outputs[key] = np.zeros(grid.iterations, dtype=floattype) - if config.is_messages(): - print('Receiver at {:g}m, {:g}m, {:g}m with output component(s) {} created.'.format(r.xcoord * grid.dx, r.ycoord * grid.dy, r.zcoord * grid.dz, ', '.join(r.outputs))) + r.outputs[key] = np.zeros(grid.iterations, dtype=config.dtypes['float_or_double']) + + log.info(f"Receiver at {r.xcoord * grid.dx:g}m, {r.ycoord * grid.dy:g}m, {r.zcoord * grid.dz:g}m with output component(s) {', '.join(r.outputs)} created.") grid.rxs.append(r) @@ -566,47 +545,43 @@ class RxArray(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 6 self.hash = '#rx_array' def create(self, grid, uip): - try: p1 = self.kwargs['p1'] p2 = self.kwargs['p2'] dl = self.kwargs['dl'] - except KeyError: - raise CmdInputError("'{}' requires exactly 9 parameters".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' requires exactly 9 parameters") xs, ys, zs = uip.check_src_rx_point(p1, self.__str__(), 'lower') xf, yf, zf = uip.check_src_rx_point(p2, self.__str__(), 'upper') dx, dy, dz = uip.discretise_point(dl) if xs > xf or ys > yf or zs > zf: - raise CmdInputError("'{}' the lower coordinates should be less than the upper coordinates".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the lower coordinates should be less than the upper coordinates") if dx < 0 or dy < 0 or dz < 0: - raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the step size should not be less than zero") if dx < 1: if dx == 0: dx = 1 else: - raise CmdInputError("'{}' the step size should not be less than the spatial discretisation".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the step size should not be less than the spatial discretisation") if dy < 1: if dy == 0: dy = 1 else: - raise CmdInputError("'{}' the step size should not be less than the spatial discretisation".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the step size should not be less than the spatial discretisation") if dz < 1: if dz == 0: dz = 1 else: - raise CmdInputError("'{}' the step size should not be less than the spatial discretisation".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the step size should not be less than the spatial discretisation") - if config.is_messages(): - print('Receiver array {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m with steps {:g}m, {:g}m, {:g}m'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, dx * grid.dx, dy * grid.dy, dz * grid.dz)) + log.info(f'Receiver array {xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m with steps {dx * grid.dx:g}m, {dy * grid.dy:g}m, {dz * grid.dz:g}m') for x in range(xs, xf + 1, dx): for y in range(ys, yf + 1, dy): @@ -620,9 +595,8 @@ class RxArray(UserObjectMulti): r.zcoordorigin = z r.ID = r.__class__.__name__ + '(' + str(x) + ',' + str(y) + ',' + str(z) + ')' for key in RxUser.defaultoutputs: - r.outputs[key] = np.zeros(grid.iterations, dtype=floattype) - if config.is_messages(): - print(' Receiver at {:g}m, {:g}m, {:g}m with output component(s) {} created.'.format(r.xcoord * grid.dx, r.ycoord * grid.dy, r.zcoord * grid.dz, ', '.join(r.outputs))) + r.outputs[key] = np.zeros(grid.iterations, dtype=config.dtypes['float_or_double']) + log.info(f" Receiver at {r.xcoord * grid.dx:g}m, {r.ycoord * grid.dy:g}m, {r.zcoord * grid.dz:g}m with output component(s) {', '.join(r.outputs)} created.") grid.rxs.append(r) @@ -644,22 +618,20 @@ class Snapshot(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 19 self.hash = '#snapshot' def create(self, grid, uip): - if isinstance(grid, SubGridBase): - raise CmdInputError("'{}' Do not add Snapshots to Subgrids.".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' do not add Snapshots to Subgrids.") try: p1 = self.kwargs['p1'] p2 = self.kwargs['p2'] dl = self.kwargs['dl'] filename = self.kwargs['filename'] except KeyError: - raise CmdInputError("'{}' requires exactly 11 parameters".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' requires exactly 11 parameters") p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 @@ -674,18 +646,18 @@ class Snapshot(UserObjectMulti): try: time = self.kwargs['time'] except KeyError: - raise CmdInputError("'{}' requires exactly 5 parameters".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' requires exactly 5 parameters") if time > 0: iterations = round_value((time / grid.dt)) + 1 else: - raise CmdInputError("'{}' time value must be greater than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' time value must be greater than zero") if dx < 0 or dy < 0 or dz < 0: - raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the step size should not be less than zero") if dx < 1 or dy < 1 or dz < 1: - raise CmdInputError("'{}' the step size should not be less than the spatial discretisation".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' the step size should not be less than the spatial discretisation") if iterations <= 0 or iterations > grid.iterations: - raise CmdInputError("'{}' time value is not valid".format(self.__str__())) + raise CmdInputError(f"'{self.__str__()}' time value is not valid") # Replace with old style snapshots if there are subgrids #if grid.subgrids: @@ -694,8 +666,7 @@ class Snapshot(UserObjectMulti): #else: s = SnapshotUser(xs, ys, zs, xf, yf, zf, dx, dy, dz, iterations, filename) - if config.is_messages(): - print('Snapshot from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m, discretisation {:g}m, {:g}m, {:g}m, at {:g} secs with filename {} created.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, dx * grid.dx, dy * grid.dy, dz * grid.dz, s.time * grid.dt, s.basefilename)) + log.info(f'Snapshot from {xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m, discretisation {dx * grid.dx:g}m, {dy * grid.dy:g}m, {dz * grid.dz:g}m, at {s.time * grid.dt:g} secs with filename {s.basefilename} created.') grid.snapshots.append(s) @@ -714,7 +685,6 @@ class Material(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 8 self.hash = '#material' @@ -727,22 +697,22 @@ class Material(UserObjectMulti): sm = self.kwargs['sm'] material_id = self.kwargs['id'] except KeyError: - raise CmdInputError('{} requires exactly five parameters'.format(self.params_str())) + raise CmdInputError(f"'{self.params_str()}' requires exactly five parameters") if er < 1: - raise CmdInputError('{} requires a positive value of one or greater for static (DC) permittivity'.format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires a positive value of one or greater for static (DC) permittivity") if se != 'inf': se = float(se) if se < 0: - raise CmdInputError('{} requires a positive value for conductivity'.format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires a positive value for conductivity") else: se = float('inf') if mr < 1: - raise CmdInputError('{} requires a positive value of one or greater for permeability'.format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires a positive value of one or greater for permeability") if sm < 0: - raise CmdInputError('{} requires a positive value for magnetic conductivity'.format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires a positive value for magnetic conductivity") if any(x.ID == material_id for x in grid.materials): - raise CmdInputError('{} with ID {} already exists'.format(material_id).format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' with ID {material_id} already exists") # Create a new instance of the Material class material (start index after pec & free_space) m = MaterialUser(len(grid.materials), material_id) @@ -755,8 +725,7 @@ class Material(UserObjectMulti): if m.se == float('inf'): m.averagable = False - if config.is_messages(): - tqdm.write('Material {} with eps_r={:g}, sigma={:g} S/m; mu_r={:g}, sigma*={:g} Ohm/m created.'.format(m.ID, m.er, m.se, m.mr, m.sm)) + log.info(f'Material {m.ID} with eps_r={m.er:g}, sigma={m.se:g} S/m; mu_r={m.mr:g}, sigma*={m.sm:g} Ohm/m created.') # Append the new material object to the materials list grid.materials.append(m) @@ -776,31 +745,28 @@ class AddDebyeDispersion(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 9 self.hash = '#add_dispersion_debye' def create(self, grid, uip): - try: poles = self.kwargs['n_poles'] er_delta = self.kwargs['er_delta'] tau = self.kwargs['tau'] material_ids = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires at least four parameters') + raise CmdInputError(f"'{self.params_str()}' requires at least four parameters") if poles < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for number of poles') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for number of poles") # Look up requested materials in existing list of material instances materials = [y for x in material_ids for y in grid.materials if y.ID == x] if len(materials) != len(material_ids): notfound = [x for x in material_ids if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + raise CmdInputError(f"'{self.params_str()}' material(s) {notfound} do not exist") for material in materials: material.type = 'debye' @@ -812,12 +778,11 @@ class AddDebyeDispersion(UserObjectMulti): material.deltaer.append(er_delta[i]) material.tau.append(tau[i]) else: - raise CmdInputError(self.__str__() + ' requires positive values for the permittivity difference.') + raise CmdInputError(f"'{self.params_str()}' requires positive values for the permittivity difference.") if material.poles > MaterialUser.maxpoles: MaterialUser.maxpoles = material.poles - if config.is_messages(): - tqdm.write('Debye disperion added to {} with delta_eps_r={}, and tau={} secs created.'.format(material.ID, ', '.join('%4.2f' % deltaer for deltaer in material.deltaer), ', '.join('%4.3e' % tau for tau in material.tau))) + log.info(f"Debye disperion added to {material.ID} with delta_eps_r={', '.join('%4.2f' % deltaer for deltaer in material.deltaer)}, and tau={', '.join('%4.3e' % tau for tau in material.tau)} secs created.") class AddLorentzDispersion(UserObjectMulti): @@ -836,13 +801,11 @@ class AddLorentzDispersion(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 10 self.hash = '#add_dispersion_lorentz' def create(self, grid, uip): - try: poles = self.kwargs['n_poles'] er_delta = self.kwargs['er_delta'] @@ -850,17 +813,17 @@ class AddLorentzDispersion(UserObjectMulti): alpha = self.kwargs['delta'] material_ids = self.kwargs['material_ids'] except KeyError: - raise CmdInputError(self.__str__() + ' requires at least five parameters') + raise CmdInputError(f"'{self.params_str()}' requires at least five parameters") if poles < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for number of poles') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for number of poles") # Look up requested materials in existing list of material instances materials = [y for x in material_ids for y in grid.materials if y.ID == x] if len(materials) != len(material_ids): notfound = [x for x in material_ids if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + raise CmdInputError(f"'{self.params_str()}' material(s) {notfound} do not exist") for material in materials: material.type = 'lorentz' @@ -872,12 +835,11 @@ class AddLorentzDispersion(UserObjectMulti): material.tau.append(tau[i]) material.alpha.append(alpha[i]) else: - raise CmdInputError(self.__str__() + ' requires positive values for the permittivity difference and frequencies, and associated times that are greater than the time step for the model.') + raise CmdInputError(f"'{self.params_str()}' requires positive values for the permittivity difference and frequencies, and associated times that are greater than the time step for the model.") if material.poles > MaterialUser.maxpoles: MaterialUser.maxpoles = material.poles - if config.is_messages(): - tqdm.write('Lorentz disperion added to {} with delta_eps_r={}, omega={} secs, and gamma={} created.'.format(material.ID, ', '.join('%4.2f' % deltaer for deltaer in material.deltaer), ', '.join('%4.3e' % tau for tau in material.tau), ', '.join('%4.3e' % alpha for alpha in material.alpha))) + log.info(f"Lorentz disperion added to {material.ID} with delta_eps_r={', '.join('%4.2f' % deltaer for deltaer in material.deltaer)}, omega={', '.join('%4.3e' % tau for tau in material.tau)} secs, and gamma={', '.join('%4.3e' % alpha for alpha in material.alpha)} created.") class AddDrudeDispersion(UserObjectMulti): @@ -894,29 +856,28 @@ class AddDrudeDispersion(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 11 self.hash = '#add_dispersion_Drude' def create(self, grid, uip): - try: poles = self.kwargs['n_poles'] tau = self.kwargs['tau'] alpha = self.kwargs['alpha'] material_ids = self.kwargs['material_ids'] except KeyError: - raise CmdInputError(self.__str__() + ' requires at least four parameters') + raise CmdInputError(f"'{self.params_str()}' requires at least four parameters") + if poles < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for number of poles') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for number of poles") # Look up requested materials in existing list of material instances materials = [y for x in material_ids for y in grid.materials if y.ID == x] if len(materials) != len(material_ids): notfound = [x for x in material_ids if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + raise CmdInputError(f"'{self.params_str()}' material(s) {notfound} do not exist") for material in materials: material.type = 'drude' @@ -927,12 +888,11 @@ class AddDrudeDispersion(UserObjectMulti): material.tau.append(tau[i]) material.alpha.append(alpha[i]) else: - raise CmdInputError(self.__str__() + ' requires positive values for the frequencies, and associated times that are greater than the time step for the model.') + raise CmdInputError(f"'{self.params_str()}' requires positive values for the frequencies, and associated times that are greater than the time step for the model.") if material.poles > MaterialUser.maxpoles: MaterialUser.maxpoles = material.poles - if config.is_messages(): - tqdm.write('Drude disperion added to {} with omega={} secs, and gamma={} secs created.'.format(material.ID, ', '.join('%4.3e' % tau for tau in material.tau), ', '.join('%4.3e' % alpha for alpha in material.alpha))) + log.info(f"Drude disperion added to {material.ID} with omega={', '.join('%4.3e' % tau for tau in material.tau)} secs, and gamma={', '.join('%4.3e' % alpha for alpha in material.alpha)} secs created.") class SoilPeplinski(UserObjectMulti): @@ -953,13 +913,11 @@ class SoilPeplinski(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 12 self.hash = '#soil_peplinski' def create(self, grid, uip): - try: sand_fraction = self.kwargs['sand_fraction'] clay_fraction = self.kwargs['clay_fraction'] @@ -968,30 +926,28 @@ class SoilPeplinski(UserObjectMulti): water_fraction_lower = self.kwargs['water_fraction_lower'] water_fraction_upper = self.kwargs['water_fraction_upper'] ID = self.kwargs['id'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires at exactly seven parameters') + raise CmdInputError(f"'{self.params_str()}' requires at exactly seven parameters") if sand_fraction < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the sand fraction') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for the sand fraction") if clay_fraction < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the clay fraction') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for the clay fraction") if bulk_density < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the bulk density') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for the bulk density") if sand_density < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the sand particle density') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for the sand particle density") if water_fraction_lower < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the lower limit of the water volumetric fraction') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for the lower limit of the water volumetric fraction") if water_fraction_upper < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the upper limit of the water volumetric fraction') + raise CmdInputError(f"'{self.params_str()}' requires a positive value for the upper limit of the water volumetric fraction") if any(x.ID == ID for x in grid.mixingmodels): - raise CmdInputError(self.__str__() + ' with ID {} already exists'.format(ID)) + raise CmdInputError(f"'{self.params_str()}' with ID {ID} already exists") # Create a new instance of the Material class material (start index after pec & free_space) s = PeplinskiSoilUser(ID, sand_fraction, clay_fraction, bulk_density, sand_density, (water_fraction_lower, water_fraction_upper)) - if config.is_messages(): - print('Mixing model (Peplinski) used to create {} with sand fraction {:g}, clay fraction {:g}, bulk density {:g}g/cm3, sand particle density {:g}g/cm3, and water volumetric fraction {:g} to {:g} created.'.format(s.ID, s.S, s.C, s.rb, s.rs, s.mu[0], s.mu[1])) + log.info(f'Mixing model (Peplinski) used to create {s.ID} with sand fraction {s.S:g}, clay fraction {s.C:g}, bulk density {s.rb:g}g/cm3, sand particle density {s.rs:g}g/cm3, and water volumetric fraction {s.mu[0]:g} to {s.mu[1]:g} created.') # Append the new material object to the materials list grid.mixingmodels.append(s) @@ -1014,7 +970,6 @@ class GeometryView(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 18 self.hash = '#geometry_view' @@ -1026,9 +981,9 @@ class GeometryView(UserObjectMulti): self.kwargs['multi_grid'] # there is no voxel output for multi grid output if isinstance(grid, SubGridBase): - raise CmdInputError("'{}' Do not add multi_grid output to subgrid user object. Please add to Scene".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' do not add multi_grid output to subgrid user object. Please add to Scene") if output_type == 'n': - raise CmdInputError("'{}' Voxel output type (n) is not supported for multigrid output :(".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' voxel output type (n) is not supported for multigrid output.") # Change constructor to the multi grid output from .geometry_outputs import GeometryViewFineMultiGrid as GeometryViewUser self.multi_grid = True @@ -1046,7 +1001,7 @@ class GeometryView(UserObjectMulti): output_type = self.kwargs['output_type'].lower() filename = self.kwargs['filename'] except KeyError: - raise CmdInputError("'{}' requires exactly eleven parameters".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires exactly eleven parameters") GeometryViewUser = self.geometry_view_constructor(grid, output_type) @@ -1057,15 +1012,15 @@ class GeometryView(UserObjectMulti): dx, dy, dz = uip.discretise_point(dl) if dx < 0 or dy < 0 or dz < 0: - raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' the step size should not be less than zero") if dx > grid.nx or dy > grid.ny or dz > grid.nz: - raise CmdInputError("'{}' the step size should be less than the domain size".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' the step size should be less than the domain size") if dx < 1 or dy < 1 or dz < 1: - raise CmdInputError("'{}' the step size should not be less than the spatial discretisation".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' the step size should not be less than the spatial discretisation") if output_type != 'n' and output_type != 'f': - raise CmdInputError("'{}' requires type to be either n (normal) or f (fine)".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires type to be either n (normal) or f (fine)") if output_type == 'f' and (dx * grid.dx != grid.dx or dy * grid.dy != grid.dy or dz * grid.dz != grid.dz): - raise CmdInputError("'{}' requires the spatial discretisation for the geometry view to be the same as the model for geometry view of type f (fine)".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires the spatial discretisation for the geometry view to be the same as the model for geometry view of type f (fine)") # Set type of geometry file if output_type == 'n': @@ -1075,8 +1030,7 @@ class GeometryView(UserObjectMulti): g = GeometryViewUser(xs, ys, zs, xf, yf, zf, dx, dy, dz, filename, fileext, grid) - if config.is_messages(): - print('Geometry view from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m, discretisation {:g}m, {:g}m, {:g}m, multi_grid {}, grid={}, with filename base {} created.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, dx * grid.dx, dy * grid.dy, dz * grid.dz, self.multi_grid, grid.name, g.basefilename)) + log.info(f'Geometry view from {xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m, discretisation {dx * grid.dx:g}m, {dy * grid.dy:g}m, {dz * grid.dz:g}m, multi_grid={self.multi_grid}, grid={grid.name}, with filename base {g.basefilename} created.') # Append the new GeometryView object to the geometry views list grid.geometryviews.append(g) @@ -1095,7 +1049,6 @@ class GeometryObjectsWrite(UserObjectMulti): """ def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 14 self.hash = '#geometry_objects_write' @@ -1106,7 +1059,7 @@ class GeometryObjectsWrite(UserObjectMulti): p2 = self.kwargs['p2'] filename = self.kwargs['filename'] except KeyError: - raise CmdInputError("'{}' requires exactly seven parameters".format(self.__str__())) + raise CmdInputError(f"'{self.params_str()}' requires exactly seven parameters") p1, p2 = uip.check_box_points(p1, p2, self.__str__()) x0, y0, z0 = p1 @@ -1114,8 +1067,7 @@ class GeometryObjectsWrite(UserObjectMulti): g = GeometryObjectsUser(x0, y0, z0, x1, y1, z1, filename) - if config.is_messages(): - print('Geometry objects in the volume from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m, will be written to {}, with materials written to {}'.format(p1[0] * grid.dx, p1[1] * grid.dy, p1[2] * grid.dz, p2[0] * grid.dx, p2[1] * grid.dy, p2[2] * grid.dz, g.filename, g.materialsfilename)) + log.info(f'Geometry objects in the volume from {p1[0] * grid.dx:g}m, {p1[1] * grid.dy:g}m, {p1[2] * grid.dz:g}m, to {p2[0] * grid.dx:g}m, {p2[1] * grid.dy:g}m, {p2[2] * grid.dz:g}m, will be written to {g.filename}, with materials written to {g.materialsfilename}') # Append the new GeometryView object to the geometry objects to write list grid.geometryobjectswrite.append(g) @@ -1153,16 +1105,14 @@ class PMLCFS(UserObjectMulti): count = 0 def __init__(self, **kwargs): - """Constructor.""" super().__init__(**kwargs) self.order = 15 self.hash = '#pml_cfs' PMLCFS.count += 1 if PMLCFS.count == 2: - raise CmdInputError(self.__str__() + ' can only be used up to two times, for up to a 2nd order PML') + raise CmdInputError(f"'{self.params_str()}' can only be used up to two times, for up to a 2nd order PML") def create(self, grid, uip): - try: alphascalingprofile = self.kwargs['alphascalingprofile'] alphascalingdirection = self.kwargs['alphascalingdirection'] @@ -1176,18 +1126,17 @@ class PMLCFS(UserObjectMulti): sigmascalingdirection = self.kwargs['sigmascalingdirection'] sigmamin = self.kwargs['sigmamin'] sigmamax = self.kwargs['sigmamax'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires exactly twelve parameters') + raise CmdInputError(f"'{self.params_str()}' requires exactly twelve parameters") if alphascalingprofile not in CFSParameter.scalingprofiles.keys() or kappascalingprofile not in CFSParameter.scalingprofiles.keys() or sigmascalingprofile not in CFSParameter.scalingprofiles.keys(): - raise CmdInputError(self.__str__() + ' must have scaling type {}'.format(','.join(CFSParameter.scalingprofiles.keys()))) + raise CmdInputError(f"'{self.params_str()}' must have scaling type {','.join(CFSParameter.scalingprofiles.keys())}") if alphascalingdirection not in CFSParameter.scalingdirections or kappascalingdirection not in CFSParameter.scalingdirections or sigmascalingdirection not in CFSParameter.scalingdirections: - raise CmdInputError(self.__str__() + ' must have scaling type {}'.format(','.join(CFSParameter.scalingdirections))) + raise CmdInputError(f"'{self.params_str()}' must have scaling type {','.join(CFSParameter.scalingdirections)}") if float(alphamin) < 0 or float(alphamax) < 0 or float(kappamin) < 0 or float(kappamax) < 0 or float(sigmamin) < 0: - raise CmdInputError(self.__str__() + ' minimum and maximum scaling values must be greater than zero') + raise CmdInputError(f"'{self.params_str()}' minimum and maximum scaling values must be greater than zero") if float(kappamin) < 1: - raise CmdInputError(self.__str__() + ' minimum scaling value for kappa must be greater than or equal to one') + raise CmdInputError(f"'{self.params_str()}' minimum scaling value for kappa must be greater than or equal to one") cfsalpha = CFSParameter() cfsalpha.ID = 'alpha' @@ -1215,8 +1164,7 @@ class PMLCFS(UserObjectMulti): cfs.kappa = cfskappa cfs.sigma = cfssigma - if config.is_messages(): - print('PML CFS parameters: alpha (scaling: {}, scaling direction: {}, min: {:g}, max: {:g}), kappa (scaling: {}, scaling direction: {}, min: {:g}, max: {:g}), sigma (scaling: {}, scaling direction: {}, min: {:g}, max: {}) created.'.format(cfsalpha.scalingprofile, cfsalpha.scalingdirection, cfsalpha.min, cfsalpha.max, cfskappa.scalingprofile, cfskappa.scalingdirection, cfskappa.min, cfskappa.max, cfssigma.scalingprofile, cfssigma.scalingdirection, cfssigma.min, cfssigma.max)) + log.info(f'PML CFS parameters: alpha (scaling: {cfsalpha.scalingprofile}, scaling direction: {cfsalpha.scalingdirection}, min: {cfsalpha.min:g}, max: {cfsalpha.max:g}), kappa (scaling: {cfskappa.scalingprofile}, scaling direction: {cfskappa.scalingdirection}, min: {cfskappa.min:g}, max: {cfskappa.max:g}), sigma (scaling: {cfssigma.scalingprofile}, scaling direction: {cfssigma.scalingdirection}, min: {cfssigma.min:g}, max: {cfssigma.max:g}) created.') grid.cfs.append(cfs) @@ -1234,7 +1182,7 @@ class Subgrid(UserObjectMulti): elif isinstance(node, UserObjectGeometry): self.children_geometry.append(node) else: - raise Exception('This Object is Unknown to gprMax') + raise GeneralError('This object is unknown to gprMax') class SubgridHSG(UserObjectMulti): diff --git a/gprMax/cmds_single_use.py b/gprMax/cmds_single_use.py index 82de288d..f92b8762 100644 --- a/gprMax/cmds_single_use.py +++ b/gprMax/cmds_single_use.py @@ -18,6 +18,7 @@ import decimal as d import inspect +import logging import os import sys @@ -26,17 +27,16 @@ from colorama import Fore from colorama import Style init() import numpy as np +from scipy.constants import c from scipy import interpolate import gprMax.config as config -from .config import c -from .config import dtypes -from .config import hostinfo from .exceptions import CmdInputError from .waveforms import Waveform from .utilities import round_value -floattype = dtypes['float_or_double'] + +log = logging.getLogger(__name__) class Properties: @@ -82,7 +82,7 @@ class Domain(UserObjectSingle): self.kwargs['p1'][1], self.kwargs['p1'][2]) except KeyError: - print('error message') + log.warning('error message') return s @@ -90,40 +90,35 @@ class Domain(UserObjectSingle): try: G.nx, G.ny, G.nz = uip.discretise_point(self.kwargs['p1']) except KeyError: - raise CmdInputError(self.__str__ + ' Please specify a point') + raise CmdInputError(f"'{self.params_str()}' please specify a point") if G.nx == 0 or G.ny == 0 or G.nz == 0: - raise CmdInputError(self.__str__ + ' requires at least one cell in every dimension') - if config.is_messages(): - print('Domain size: {:g} x {:g} x {:g}m ({:d} x {:d} x {:d} = {:g} cells)'.format(self.kwargs['p1'][0], self.kwargs['p1'][1], self.kwargs['p1'][2], G.nx, G.ny, G.nz, (G.nx * G.ny * G.nz))) + raise CmdInputError(f"'{self.params_str()}' requires at least one cell in every dimension") - # Time step CFL limit (either 2D or 3D); switch off appropriate PMLs for 2D + log.info(f'Domain size: {self.kwargs['p1'][0]:g} x {self.kwargs['p1'][1]:g} x {self.kwargs['p1'][2]:g}m ({G.nx:d} x {G.ny:d} x {G.nz:d} = {(G.nx * G.ny * G.nz):g} cells)') + + # Calculate time step at CFL limit; switch off appropriate PMLs for 2D if G.nx == 1: - G.dt = 1 / (c * np.sqrt((1 / G.dy) * (1 / G.dy) + (1 / G.dz) * (1 / G.dz))) + G.calculate_dt() G.mode = '2D TMx' G.pmlthickness['x0'] = 0 G.pmlthickness['xmax'] = 0 elif G.ny == 1: - G.dt = 1 / (c * np.sqrt((1 / G.dx) * (1 / G.dx) + (1 / G.dz) * (1 / G.dz))) + G.calculate_dt() G.mode = '2D TMy' G.pmlthickness['y0'] = 0 G.pmlthickness['ymax'] = 0 elif G.nz == 1: - G.dt = 1 / (c * np.sqrt((1 / G.dx) * (1 / G.dx) + (1 / G.dy) * (1 / G.dy))) + G.calculate_dt() G.mode = '2D TMz' G.pmlthickness['z0'] = 0 G.pmlthickness['zmax'] = 0 else: - G.dt = 1 / (c * np.sqrt((1 / G.dx) * (1 / G.dx) + (1 / G.dy) * (1 / G.dy) + (1 / G.dz) * (1 / G.dz))) + G.calculate_dt() G.mode = '3D' - # Round down time step to nearest float with precision one less than hardware maximum. - # Avoids inadvertently exceeding the CFL due to binary representation of floating point number. - G.dt = round_value(G.dt, decimalplaces=d.getcontext().prec - 1) - - if config.is_messages(): - print('Mode: {}'.format(G.mode)) - print('Time step (at CFL limit): {:g} secs'.format(G.dt)) + log.info(f'Mode: {G.mode}') + log.info(f'Time step (at CFL limit): {G.dt:g} secs') # Number of threads (OpenMP) to use if sys.platform == 'darwin': @@ -134,7 +129,7 @@ class Domain(UserObjectSingle): # os.environ['OMP_DISPLAY_ENV'] = 'TRUE' # Prints OMP version and environment variables (useful for debug) # Catch bug with Windows Subsystem for Linux (https://github.com/Microsoft/BashOnWindows/issues/785) - if 'Microsoft' in hostinfo['osversion']: + if 'Microsoft' in config.hostinfo['osversion']: os.environ['KMP_AFFINITY'] = 'disabled' del os.environ['OMP_PLACES'] del os.environ['OMP_PROC_BIND'] @@ -143,13 +138,12 @@ class Domain(UserObjectSingle): G.nthreads = int(os.environ.get('OMP_NUM_THREADS')) else: # Set number of threads to number of physical CPU cores - G.nthreads = hostinfo['physicalcores'] + G.nthreads = config.hostinfo['physicalcores'] os.environ['OMP_NUM_THREADS'] = str(G.nthreads) - if config.is_messages(): - print('Number of CPU (OpenMP) threads: {}'.format(G.nthreads)) - if G.nthreads > hostinfo['physicalcores']: - print(Fore.RED + 'WARNING: You have specified more threads ({}) than available physical CPU cores ({}). This may lead to degraded performance.'.format(G.nthreads, hostinfo['physicalcores']) + Style.RESET_ALL) + log.info(f'Number of CPU (OpenMP) threads: {G.nthreads}') + if G.nthreads > config.hostinfo['physicalcores']: + log.warning(Fore.RED + f'You have specified more threads ({G.nthreads}) than available physical CPU cores ({config.hostinfo['physicalcores']}). This may lead to degraded performance.' + Style.RESET_ALL) class Discretisation(UserObjectSingle): @@ -169,7 +163,7 @@ class Discretisation(UserObjectSingle): self.kwargs['p1'][1], self.kwargs['p1'][2]) except KeyError: - print('error message') + log.info('error message') return s @@ -188,7 +182,7 @@ class Discretisation(UserObjectSingle): raise CmdInputError('Discretisation requires the z-direction spatial step to be greater than zero') if config.is_messages(): - print('Spatial discretisation: {:g} x {:g} x {:g}m'.format(*G.dl)) + log.info('Spatial discretisation: {:g} x {:g} x {:g}m'.format(*G.dl)) class TimeWindow(UserObjectSingle): @@ -211,7 +205,7 @@ class TimeWindow(UserObjectSingle): try: s = '#time_window: {}'.format(self.kwargs['iterations']) except KeyError: - print('time window error') + log.info('time window error') return s @@ -242,7 +236,7 @@ class TimeWindow(UserObjectSingle): raise CmdInputError('TimeWindow: Specify a time or number of iterations') if config.is_messages(): - print('Time window: {:g} secs ({} iterations)'.format(G.timewindow, G.iterations)) + log.info('Time window: {:g} secs ({} iterations)'.format(G.timewindow, G.iterations)) class Messages(UserObjectSingle): @@ -260,7 +254,7 @@ class Messages(UserObjectSingle): try: s = '#messages: {}'.format(self.kwargs['yn']) except KeyError: - print('messages problem') + log.info('messages problem') def create(self, G, uip): try: @@ -296,7 +290,7 @@ class Title(UserObjectSingle): pass if config.is_messages(): - print('Model title: {}'.format(G.title)) + log.info('Model title: {}'.format(G.title)) class NumThreads(UserObjectSingle): @@ -332,14 +326,14 @@ class NumThreads(UserObjectSingle): os.environ['OMP_NUM_THREADS'] = str(G.nthreads) if config.is_messages(): - print('Number of CPU (OpenMP) threads: {}'.format(G.nthreads)) - if G.nthreads > hostinfo['physicalcores']: - print(Fore.RED + 'WARNING: You have specified more threads ({}) than available physical CPU cores ({}). This may lead to degraded performance.'.format(G.nthreads, hostinfo['physicalcores']) + Style.RESET_ALL) + log.info('Number of CPU (OpenMP) threads: {}'.format(G.nthreads)) + if G.nthreads > config.hostinfo['physicalcores']: + log.info(Fore.RED + 'WARNING: You have specified more threads ({}) than available physical CPU cores ({}). This may lead to degraded performance.'.format(G.nthreads, config.hostinfo['physicalcores']) + Style.RESET_ALL) # Print information about any GPU in use if config.is_messages(): if G.gpu is not None: - print('GPU solving using: {} - {}'.format(G.gpu.deviceID, G.gpu.name)) + log.info('GPU solving using: {} - {}'.format(G.gpu.deviceID, G.gpu.name)) class TimeStepStabilityFactor(UserObjectSingle): @@ -369,7 +363,7 @@ class TimeStepStabilityFactor(UserObjectSingle): raise CmdInputError(self.__str__() + ' requires the value of the time step stability factor to be between zero and one') G.dt = G.dt * f if config.is_messages(): - print('Time step (modified): {:g} secs'.format(G.dt)) + log.info('Time step (modified): {:g} secs'.format(G.dt)) class PMLCells(UserObjectSingle): @@ -442,7 +436,7 @@ class SrcSteps(UserObjectSingle): raise CmdInputError('#src_steps: requires exactly three parameters') if config.is_messages(): - print('Simple sources will step {:g}m, {:g}m, {:g}m for each model run.'.format(G.srcsteps[0] * G.dx, G.srcsteps[1] * G.dy, G.srcsteps[2] * G.dz)) + log.info('Simple sources will step {:g}m, {:g}m, {:g}m for each model run.'.format(G.srcsteps[0] * G.dx, G.srcsteps[1] * G.dy, G.srcsteps[2] * G.dz)) class RxSteps(UserObjectSingle): @@ -463,7 +457,7 @@ class RxSteps(UserObjectSingle): raise CmdInputError('#rx_steps: requires exactly three parameters') if config.is_messages(): - print('All receivers will step {:g}m, {:g}m, {:g}m for each model run.'.format(G.rxsteps[0] * G.dx, G.rxsteps[1] * G.dy, G.rxsteps[2] * G.dz)) + log.info('All receivers will step {:g}m, {:g}m, {:g}m for each model run.'.format(G.rxsteps[0] * G.dx, G.rxsteps[1] * G.dy, G.rxsteps[2] * G.dz)) class ExcitationFile(UserObjectSingle): @@ -498,7 +492,7 @@ class ExcitationFile(UserObjectSingle): excitationfile = os.path.abspath(os.path.join(G.inputdirectory, excitationfile)) if config.is_messages(): - print('\nExcitation file: {}'.format(excitationfile)) + log.info('\nExcitation file: {}'.format(excitationfile)) # Get waveform names with open(excitationfile, 'r') as f: @@ -538,7 +532,7 @@ class ExcitationFile(UserObjectSingle): w.userfunc = interpolate.interp1d(waveformtime, singlewaveformvalues, **kwargs) if config.is_messages(): - print('User waveform {} created using {} and, if required, interpolation parameters (kind: {}, fill value: {}).'.format(w.ID, timestr, kwargs['kind'], kwargs['fill_value'])) + log.info('User waveform {} created using {} and, if required, interpolation parameters (kind: {}, fill value: {}).'.format(w.ID, timestr, kwargs['kind'], kwargs['fill_value'])) G.waveforms.append(w) diff --git a/gprMax/config.pxd b/gprMax/config.pxd index 35f85fb5..5d7d64c2 100644 --- a/gprMax/config.pxd +++ b/gprMax/config.pxd @@ -27,9 +27,3 @@ ctypedef fused float_or_double: ctypedef fused float_or_double_complex: float complex double complex - -ctypedef fused real_or_complex: - float - double - float complex - double complex diff --git a/gprMax/config.py b/gprMax/config.py index 7bce848b..6783a992 100644 --- a/gprMax/config.py +++ b/gprMax/config.py @@ -16,9 +16,7 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . -import os from pathlib import Path -import sys from colorama import init from colorama import Fore @@ -27,31 +25,37 @@ init() import cython import numpy as np from scipy.constants import c -from scipy.constants import mu_0 as m0 from scipy.constants import epsilon_0 as e0 +from scipy.constants import mu_0 as m0 from .utilities import get_host_info from .utilities import get_terminal_width -# Impedance of free space (Ohms) -z0 = np.sqrt(m0 / e0) - -# General setting for the simulation +# General settings for the simulation # inputfilepath: path to inputfile location # outputfilepath: path to outputfile location # messages: whether to print all messages as output to stdout or not # progressbars: whether to show progress bars on stdoout or not # mode: 2D TMx, 2D TMy, 2D TMz, or 3D # cpu, cuda, opencl: solver type +# precision: data type for electromagnetic field output (single/double) # autotranslate: auto translate objects with main grid coordinates # to their equivalent local grid coordinate within the subgrid. If this option is off # users must specify sub-grid object point within the global subgrid space. -general = {'inputfilepath': 'gprMax', 'outputfilepath': 'gprMax', 'messages': True, - 'progressbars': True, 'mode': '3D', 'cpu': True, 'cuda': False, 'opencl': False, 'autotranslate': False} +# z0: impedance of free space (Ohms) +general = {'messages': True, + 'progressbars': True, + 'mode': '3D', + 'cpu': True, + 'cuda': False, + 'opencl': False, + 'precision': 'single', + 'autotranslate': False, + 'z0': np.sqrt(m0 / e0)} def is_messages(): - """Function to return messages.""" + """Return messages.""" return general['messages'] # Store information about host machine @@ -64,37 +68,30 @@ hostinfo = get_host_info() # on the memory of the GPU. If True this will slow performance significantly cuda = {'gpus': None, 'snapsgpu2cpu': False} -# Numerical dispersion analysis parameters -# highestfreqthres: threshold (dB) down from maximum power (0dB) of main frequency used -# to calculate highest frequency for numerical dispersion analysis -# maxnumericaldisp: maximum allowable percentage physical phase-velocity phase error -# mingridsampling: minimum grid sampling of smallest wavelength for physical wave propagation -numdispersion = {'highestfreqthres': 40, 'maxnumericaldisp': 2, 'mingridsampling': 3} - -# Materials -# maxpoles: Maximum number of dispersive material poles in a model -materials = {'maxpoles': 0, 'dispersivedtype': None, 'dispersiveCdtype': None} - -# Data types +# Data type (precision) for electromagnetic field output # Solid and ID arrays use 32-bit integers (0 to 4294967295) # Rigid arrays use 8-bit integers (the smallest available type to store true/false) -# Fractal and dispersive coefficient arrays use complex numbers (complex) -# which are represented as two floats -# Main field arrays use floats (float_or_double) and complex numbers (complex) -# Precision of float_or_double and complex: single or double for numpy and C (CUDA) arrays -precision = 'double' +# Fractal arrays use complex numbers +# Dispersive coefficient arrays use either float or complex numbers +# Main field arrays use floats -if precision == 'single': - dtypes = {'float_or_double': np.float32, 'complex': np.complex64, - 'cython_float_or_double': cython.float, 'cython_complex': cython.floatcomplex, - 'C_float_or_double': 'float', 'C_complex': 'pycuda::complex'} -elif precision == 'double': - dtypes = {'float_or_double': np.float64, 'complex': np.complex128, - 'cython_float_or_double': cython.double, 'cython_complex': cython.doublecomplex, - 'C_float_or_double': 'double', 'C_complex': 'pycuda::complex'} +if general['precision'] == 'single': + dtypes = {'float_or_double': np.float32, + 'complex': np.complex64, + 'cython_float_or_double': cython.float, + 'cython_complex': cython.floatcomplex, + 'C_float_or_double': 'float', + 'C_complex': 'pycuda::complex'} +elif general['precision'] == 'double': + dtypes = {'float_or_double': np.float64, + 'complex': np.complex128, + 'cython_float_or_double': cython.double, + 'cython_complex': cython.doublecomplex, + 'C_float_or_double': 'double', + 'C_complex': 'pycuda::complex'} -class ModelConfig(): +class ModelConfig: """Configuration parameters for a model. N.B. Multiple models can exist within a simulation """ @@ -107,8 +104,8 @@ class ModelConfig(): """ self.sim_config = sim_config - self.reuse_geometry = False self.i = i # Indexed from 0 + self.reuse_geometry = False if not sim_config.single_model: self.appendmodelnumber = str(self.i + 1) # Indexed from 1 @@ -128,16 +125,33 @@ class ModelConfig(): self.inputfilestr = inputfilestr_f.format(self.i + 1, self.sim_config.model_end, self.sim_config.input_file_path) self.next_model = Fore.GREEN + '{} {}\n'.format(self.inputfilestr, '-' * (get_terminal_width() - 1 - len(self.inputfilestr))) + Style.RESET_ALL + # Numerical dispersion analysis parameters + # highestfreqthres: threshold (dB) down from maximum power (0dB) of main frequency used + # to calculate highest frequency for numerical dispersion analysis + # maxnumericaldisp: maximum allowable percentage physical phase-velocity phase error + # mingridsampling: minimum grid sampling of smallest wavelength for physical wave propagation + self.numdispersion = {'highestfreqthres': 40, + 'maxnumericaldisp': 2, + 'mingridsampling': 3} + + # General information to configure materials + # maxpoles: Maximum number of dispersive material poles in a model + # dispersivedtype: Data type for dispersive materials + # dispersiveCdtype: Data type for dispersive materials in Cython + self.materials = {'maxpoles': 0, + 'dispersivedtype': None, + 'dispersiveCdtype': None} + def get_scene(self): if self.sim_config.scenes: return self.sim_config.scenes[self.i] else: return None def get_usernamespace(self): - return {'c': c, - 'e0': e0, - 'm0': m0, - 'z0': z0, + return {'c': c, # Speed of light in free space (m/s) + 'e0': e0, # Permittivity of free space (F/m) + 'm0': m0, # Permeability of free space (H/m) + 'z0': general['z0'], # Impedance of free space (Ohms) 'number_model_runs': self.sim_config.model_end + 1, 'current_model_run': self.i + 1, 'inputfile': self.sim_config.input_file_path.resolve()} @@ -155,16 +169,10 @@ class SimulationConfig: """ self.args = args - self.n_models = args.n - self.inputfile = args.inputfile - self.gpu = args.gpu - self.mpi = args.mpi - self.mpi_no_spawn = args.mpi_no_spawn - self.general = {} - self.general['messages'] = general['messages'] - self.geometry_fixed = args.geometry_fixed - self.geometry_only = args.geometry_only - self.write_processed = args.write_processed + self.general = general + self.hostinfo = hostinfo + self.cuda = cuda + self.dtypes = dtypes # Subgrid parameter may not exist if user enters via CLI try: @@ -191,7 +199,7 @@ class SimulationConfig: self.single_model = False def set_model_start_end(self): - # Set range for number of models to run (internally 0 index) + """Set range for number of models to run (internally 0 index).""" if self.args.task: # Job array feeds args.n number of single tasks modelstart = self.args.task - 1 @@ -206,23 +214,20 @@ class SimulationConfig: self.model_start = modelstart self.model_end = modelend - def set_precision(self): - pass - def set_input_file_path(self): - # If the API is in use an id for the simulation must be provided. + """If the API is in use an id for the simulation must be provided.""" if self.args.inputfile is None: self.input_file_path = Path(self.args.outputfile) else: self.input_file_path = Path(self.args.inputfile) def set_output_file_path(self): - # Output file path can be provided by the user. If they havent provided one - # use the inputfile file path instead + """Output file path can be provided by the user. If they havent provided one + use the inputfile file path instead.""" try: self.output_file_path = Path(self.args.outputfile) except AttributeError: - self.output_file_path = Path(self.args.inputfile) + self.output_file_path = Path(self.input_file_path) class SimulationConfigMPI(SimulationConfig): diff --git a/gprMax/contexts.py b/gprMax/contexts.py index ed2e7f1e..7ca31ba0 100644 --- a/gprMax/contexts.py +++ b/gprMax/contexts.py @@ -18,15 +18,17 @@ import datetime +from ._version import __version__, codename from .config import create_model_config from .model_build_run import ModelBuildRun from .solvers import create_solver from .solvers import create_G from .utilities import get_terminal_width +from .utilities import logo from .utilities import timer -class Context(): +class Context: """Generic context for the model to run in. Sub-class with specific contexts e.g. an MPI context. """ @@ -43,18 +45,23 @@ class Context(): self.tsimstart = 1 def run(self): - """Function to run the simulation in the correct context.""" + """Run the simulation in the correct context.""" + self.print_logo_copyright() self.tsimstart = timer() self._run() self.tsimend = timer() + def print_logo_copyright(self): + """Print gprMax logo, version, and copyright/licencing information.""" + logo(__version__ + ' (' + codename + ')') + def print_time_report(self): - """Function to print the total simulation time based on context.""" - s = self.make_time_report(sim_time) - print(s) + """Print the total simulation time based on context.""" + s = self.make_time_report() + log.info(s) def make_time_report(self): - """Function to generate a string for the total simulation time.""" + """Generate a string for the total simulation time.""" pass @@ -63,6 +70,9 @@ class NoMPIContext(Context): is parallelised using either OpenMP (CPU) or CUDA (GPU). """ + def __init__(self): + super().__init__() + def _run(self): """Specialise how the models are farmed out.""" @@ -83,7 +93,7 @@ class NoMPIContext(Context): solver = create_solver(G, self.sim_config) if not self.sim_config.geometry_only: - model.run_model(solver) + model.solve(solver) def make_time_report(self): """Function to specialise the time reporting for the standard Simulation @@ -91,9 +101,8 @@ class NoMPIContext(Context): """ sim_time = datetime.timedelta(seconds=self.tsimend - self.tsimstart) - s = '\n=== Simulation on {} completed in [HH:MM:SS]: {}' - s = s.format(self.simconfig.hostinfo['hostname'], sim_time) - return '{} {}\n'.format(s, '=' * (get_terminal_width() - 1 - len(s))) + s = f'\n=== Simulation on {self.simconfig.hostinfo['hostname']} completed in [HH:MM:SS]: {sim_time}' + return f'{s} {'=' * (get_terminal_width() - 1 - len(s))}\n' class MPIContext(Context): @@ -116,6 +125,9 @@ class MPIContext(Context): class MPINoSpawnContext(Context): + def __init__(self): + super().__init__() + def _run(self): pass @@ -133,9 +145,9 @@ def create_context(sim_config): context (Context): Context for the model to run in. """ - if sim_config.mpi: + if sim_config.args.mpi: context = MPIContext(sim_config) - elif sim_config.mpi_no_spawn: + elif sim_config.args.mpi_no_spawn: context = MPINoSpawnContext(sim_config) else: context = NoMPIContext(sim_config) diff --git a/gprMax/cython/fields_updates_hsg.pyx b/gprMax/cython/fields_updates_hsg.pyx index ea0379ff..fa58bc05 100644 --- a/gprMax/cython/fields_updates_hsg.pyx +++ b/gprMax/cython/fields_updates_hsg.pyx @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2017: The University of Edinburgh +# Copyright (C) 2015-2019: The University of Edinburgh # Authors: Craig Warren and Antonis Giannopoulos # # This file is part of gprMax. @@ -15,10 +15,13 @@ # # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . + import numpy as np cimport numpy as np + from cython.parallel import prange + cpdef void cython_update_electric_os( np.float64_t[:, :] updatecoeffsE, np.uint32_t[:, :, :, :] ID, diff --git a/gprMax/cython/fields_updates_normal.pyx b/gprMax/cython/fields_updates_normal.pyx index ccd65ee1..771b7f0d 100644 --- a/gprMax/cython/fields_updates_normal.pyx +++ b/gprMax/cython/fields_updates_normal.pyx @@ -1,4 +1,4 @@ -cdef float phi# Copyright (C) 2015-2019: The University of Edinburgh +# Copyright (C) 2015-2019: The University of Edinburgh # Authors: Craig Warren and Antonis Giannopoulos # # This file is part of gprMax. diff --git a/gprMax/cython/geometry_outputs.pyx b/gprMax/cython/geometry_outputs.pyx index 21490909..994afcd6 100644 --- a/gprMax/cython/geometry_outputs.pyx +++ b/gprMax/cython/geometry_outputs.pyx @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . +import numpy as np cimport numpy as np diff --git a/gprMax/cython/geometry_primitives.pyx b/gprMax/cython/geometry_primitives.pyx index d6e65532..586ff1db 100644 --- a/gprMax/cython/geometry_primitives.pyx +++ b/gprMax/cython/geometry_primitives.pyx @@ -18,8 +18,8 @@ import numpy as np cimport numpy as np +np.seterr(divide='raise') -from gprMax.utilities import round_value from gprMax.cython.yee_cell_setget_rigid cimport set_rigid_Ex from gprMax.cython.yee_cell_setget_rigid cimport set_rigid_Ey from gprMax.cython.yee_cell_setget_rigid cimport set_rigid_Ez @@ -30,8 +30,7 @@ from gprMax.cython.yee_cell_setget_rigid cimport set_rigid_E from gprMax.cython.yee_cell_setget_rigid cimport unset_rigid_E from gprMax.cython.yee_cell_setget_rigid cimport set_rigid_H from gprMax.cython.yee_cell_setget_rigid cimport unset_rigid_H - -np.seterr(divide='raise') +from gprMax.utilities import round_value cpdef bint are_clockwise( diff --git a/gprMax/cython/pml_updates_electric_HORIPML.pyx b/gprMax/cython/pml_updates_electric_HORIPML.pyx index 5bc9ea9e..3c13021c 100644 --- a/gprMax/cython/pml_updates_electric_HORIPML.pyx +++ b/gprMax/cython/pml_updates_electric_HORIPML.pyx @@ -4,16 +4,16 @@ # This file is part of gprMax. # # gprMax is free software: you can redistribute it and/or modify -# it under the terms of the GNU GenRAl Public License as published by +# 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 GenRAl Public License for more details. +# GNU General Public License for more details. # -# You should have received a copy of the GNU GenRAl Public License +# You should have received a copy of the GNU General Public License # along with gprMax. If not, see . import numpy as np diff --git a/gprMax/cython/pml_updates_electric_MRIPML.pyx b/gprMax/cython/pml_updates_electric_MRIPML.pyx index 805a36ea..c2ddbfde 100644 --- a/gprMax/cython/pml_updates_electric_MRIPML.pyx +++ b/gprMax/cython/pml_updates_electric_MRIPML.pyx @@ -1,19 +1,19 @@ -cdef float_or_double# Copyright (C) 2015-2019: The University of Edinburgh +# Copyright (C) 2015-2019: The University of Edinburgh # Authors: Craig Warren and Antonis Giannopoulos # # This file is part of gprMax. # # gprMax is free software: you can redistribute it and/or modify -# it under the terms of the GNU GenRAl Public License as published by +# 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 GenRAl Public License for more details. +# GNU General Public License for more details. # -# You should have received a copy of the GNU GenRAl Public License +# You should have received a copy of the GNU General Public License # along with gprMax. If not, see . import numpy as np diff --git a/gprMax/cython/snapshots.pyx b/gprMax/cython/snapshots.pyx index 05a9f516..12332329 100644 --- a/gprMax/cython/snapshots.pyx +++ b/gprMax/cython/snapshots.pyx @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . -cimport numpy as np - from gprMax.config cimport float_or_double diff --git a/gprMax/cython/yee_cell_build.pyx b/gprMax/cython/yee_cell_build.pyx index 39202e41..5c812b61 100644 --- a/gprMax/cython/yee_cell_build.pyx +++ b/gprMax/cython/yee_cell_build.pyx @@ -19,13 +19,13 @@ import numpy as np cimport numpy as np -from gprMax.materials import Material from gprMax.cython.yee_cell_setget_rigid cimport get_rigid_Ex from gprMax.cython.yee_cell_setget_rigid cimport get_rigid_Ey from gprMax.cython.yee_cell_setget_rigid cimport get_rigid_Ez from gprMax.cython.yee_cell_setget_rigid cimport get_rigid_Hx from gprMax.cython.yee_cell_setget_rigid cimport get_rigid_Hy from gprMax.cython.yee_cell_setget_rigid cimport get_rigid_Hz +from gprMax.materials import Material cpdef void create_electric_average(int i, int j, int k, int numID1, int numID2, int numID3, int numID4, int componentID, G): diff --git a/gprMax/cython/yee_cell_setget_rigid.pxd b/gprMax/cython/yee_cell_setget_rigid.pxd index 818113e0..df70ba6a 100644 --- a/gprMax/cython/yee_cell_setget_rigid.pxd +++ b/gprMax/cython/yee_cell_setget_rigid.pxd @@ -19,8 +19,9 @@ import numpy as np cimport numpy as np -# Get and set functions for the rigid electric component array. The rigid array is 4D with the 1st dimension holding -# the 12 electric edge components of a cell - Ex1, Ex2, Ex3, Ex4, Ey1, Ey2, Ey3, Ey4, Ez1, Ez2, Ez3, Ez4 +# Get and set functions for the rigid electric component array. The rigid array +# is 4D with the 1st dimension holding the 12 electric edge components of a +# cell - Ex1, Ex2, Ex3, Ex4, Ey1, Ey2, Ey3, Ey4, Ez1, Ez2, Ez3, Ez4 cdef bint get_rigid_Ex(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE) cdef bint get_rigid_Ey(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE) cdef bint get_rigid_Ez(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE) @@ -30,8 +31,9 @@ cdef void set_rigid_Ez(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE) cdef void set_rigid_E(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE) cdef void unset_rigid_E(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE) -# Get and set functions for the rigid magnetic component array. The rigid array is 4D with the 1st dimension holding -# the 6 magnetic edge components - Hx1, Hx2, Hy1, Hy2, Hz1, Hz2 +# Get and set functions for the rigid magnetic component array. The rigid array +# is 4D with the 1st dimension holding the 6 magnetic edge components - Hx1, +# Hx2, Hy1, Hy2, Hz1, Hz2 cdef bint get_rigid_Hx(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) cdef bint get_rigid_Hy(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) cdef bint get_rigid_Hz(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) @@ -40,5 +42,3 @@ cdef void set_rigid_Hy(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) cdef void set_rigid_Hz(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) cdef void set_rigid_H(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) cdef void unset_rigid_H(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH) - - diff --git a/gprMax/cython/yee_cell_setget_rigid.pyx b/gprMax/cython/yee_cell_setget_rigid.pyx index 707fff52..6858b7e8 100644 --- a/gprMax/cython/yee_cell_setget_rigid.pyx +++ b/gprMax/cython/yee_cell_setget_rigid.pyx @@ -19,8 +19,10 @@ import numpy as np cimport numpy as np -# Get and set functions for the rigid electric component array. The rigid array is 4D with the 1st dimension holding -# the 12 electric edge components of a cell - Ex1, Ex2, Ex3, Ex4, Ey1, Ey2, Ey3, Ey4, Ez1, Ez2, Ez3, Ez4 + +# Get and set functions for the rigid electric component array. The rigid array +# is 4D with the 1st dimension holding the 12 electric edge components of a +# cell - Ex1, Ex2, Ex3, Ex4, Ey1, Ey2, Ey3, Ey4, Ez1, Ez2, Ez3, Ez4 cdef bint get_rigid_Ex(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE): cdef bint result result = False @@ -102,8 +104,9 @@ cdef void set_rigid_E(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE): cdef void unset_rigid_E(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidE): rigidE[:, i, j, k] = False -# Get and set functions for the rigid magnetic component array. The rigid array is 4D with the 1st dimension holding -# the 6 magnetic edge components - Hx1, Hx2, Hy1, Hy2, Hz1, Hz2 +# Get and set functions for the rigid magnetic component array. The rigid array +# is 4D with the 1st dimension holding the 6 magnetic edge components - Hx1, +# Hx2, Hy1, Hy2, Hz1, Hz2 cdef bint get_rigid_Hx(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH): cdef bint result result = False @@ -154,4 +157,3 @@ cdef void set_rigid_H(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH): cdef void unset_rigid_H(int i, int j, int k, np.int8_t[:, :, :, ::1] rigidH): rigidH[:, i, j, k] = False - diff --git a/gprMax/gprMax.py b/gprMax/gprMax.py index fcbc6375..7f8d1963 100644 --- a/gprMax/gprMax.py +++ b/gprMax/gprMax.py @@ -17,11 +17,14 @@ # along with gprMax. If not, see . import argparse +import logging from .config import create_simulation_config from .contexts import create_context +logging.basicConfig(level=logging.INFO) + def run( scenes=None, inputfile=None, @@ -137,3 +140,4 @@ def run_main(args): sim_config = create_simulation_config(args) context = create_context(sim_config) context.run() + context.print_time_report() diff --git a/gprMax/grid.py b/gprMax/grid.py index 643cf425..d6b9f784 100644 --- a/gprMax/grid.py +++ b/gprMax/grid.py @@ -36,80 +36,16 @@ from .utilities import human_size from .utilities import round_value -class Grid(object): - """Generic grid/mesh.""" - - def __init__(self, grid): - self.nx = grid.shape[0] - self.ny = grid.shape[1] - self.nz = grid.shape[2] - self.dx = 1 - self.dy = 1 - self.dz = 1 - self.i_max = self.nx - 1 - self.j_max = self.ny - 1 - self.k_max = self.nz - 1 - self.grid = grid - - def n_edges(self): - i = self.nx - j = self.ny - k = self.nz - e = (i * j * (k - 1)) + (j * k * (i - 1)) + (i * k * (j - 1)) - return e - - def n_nodes(self): - return self.nx * self.ny * self.nz - - def n_cells(self): - return (self.nx - 1) * (self.ny - 1) * (self.nz - 1) - - def get(self, i, j, k): - return self.grid[i, j, k] - - def within_bounds(self, p): - if p[0] < 0 or p[0] > self.nx: - raise ValueError('x') - if p[1] < 0 or p[1] > self.ny: - raise ValueError('y') - if p[2] < 0 or p[2] > self.nz: - raise ValueError('z') - - def discretise_point(self, p): - x = round_value(float(p[0]) / self.dx) - y = round_value(float(p[1]) / self.dy) - z = round_value(float(p[2]) / self.dz) - return (x, y, z) - - def round_to_grid(self, p): - p = self.discretise_point(p) - p_r = (p[0] * self.dx, - p[1] * self.dy, - p[2] * self.dz) - return p_r - - - def within_pml(self, p): - if (p[0] < self.pmlthickness['x0'] or - p[0] > self.nx - self.pmlthickness['xmax'] or - p[1] < self.pmlthickness['y0'] or - p[1] > self.ny - self.pmlthickness['ymax'] or - p[2] < self.pmlthickness['z0'] or - p[2] > self.nz - self.pmlthickness['zmax']): - return True - else: - return False - - -class FDTDGrid(Grid): - """Holds attributes associated with entire grid. A convenient - way for accessing regularly used parameters. +class FDTDGrid: + """Holds attributes associated with entire grid. A convenient way for + accessing regularly used parameters. """ def __init__(self): self.title = '' self.memoryusage = 0 self.name = 'Main' + self.mode = '' # 2D TMx, 2D TMy, 2D TMz, or 3D self.gpu = None self.outputdirectory = '' @@ -150,6 +86,50 @@ class FDTDGrid(Grid): self.snapshots = [] self.subgrids = [] + def n_edges(self): + i = self.nx + j = self.ny + k = self.nz + e = (i * j * (k - 1)) + (j * k * (i - 1)) + (i * k * (j - 1)) + return e + + def n_nodes(self): + return self.nx * self.ny * self.nz + + def n_cells(self): + return (self.nx - 1) * (self.ny - 1) * (self.nz - 1) + + def within_bounds(self, p): + if p[0] < 0 or p[0] > self.nx: + raise ValueError('x') + if p[1] < 0 or p[1] > self.ny: + raise ValueError('y') + if p[2] < 0 or p[2] > self.nz: + raise ValueError('z') + + def discretise_point(self, p): + x = round_value(float(p[0]) / self.dx) + y = round_value(float(p[1]) / self.dy) + z = round_value(float(p[2]) / self.dz) + return (x, y, z) + + def round_to_grid(self, p): + p = self.discretise_point(p) + p_r = (p[0] * self.dx, + p[1] * self.dy, + p[2] * self.dz) + return p_r + + def within_pml(self, p): + if (p[0] < self.pmlthickness['x0'] or + p[0] > self.nx - self.pmlthickness['xmax'] or + p[1] < self.pmlthickness['y0'] or + p[1] > self.ny - self.pmlthickness['ymax'] or + p[2] < self.pmlthickness['z0'] or + p[2] > self.nz - self.pmlthickness['zmax']): + return True + else: + return False def initialise_geometry_arrays(self): """Initialise an array for volumetric material IDs (solid); @@ -173,9 +153,8 @@ class FDTDGrid(Grid): self.Hy = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) self.Hz = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) - def initialise_grids(self): - """Function to call the initialisation of all grids.""" + """Initialise all grids.""" for g in [self] + self.subgrids: g.initialise_geometry_arrays() g.initialise_field_arrays() @@ -227,32 +206,75 @@ class FDTDGrid(Grid): self.memoryusage = int(stdoverhead + fieldarrays + solidarray + rigidarrays + pmlarrays) - def memory_check(self, snapsmemsize=0): - """Check if the required amount of memory (RAM) is available on the host and GPU if specified. - - Args: - snapsmemsize (int): amount of memory (bytes) required to store all requested snapshots + def memory_check(self): + """Check if the required amount of memory (RAM) is available to build + and/or run model on the host. """ - - # Check if model can be built and/or run on host if self.memoryusage > config.hostinfo['ram']: - raise GeneralError('Memory (RAM) required ~{} exceeds {} detected!\n'.format(human_size(self.memoryusage), human_size(config.hostinfo['ram'], a_kilobyte_is_1024_bytes=True))) + raise GeneralError(f'Memory (RAM) required ~{human_size(self.memoryusage)} exceeds {human_size(config.hostinfo['ram'], a_kilobyte_is_1024_bytes=True)} detected!\n') - # Check if model can be run on specified GPU if required - if config.cuda['gpus'] is not None: - if self.memoryusage - snapsmemsize > config.cuda['gpus'].totalmem: - raise GeneralError('Memory (RAM) required ~{} exceeds {} detected on specified {} - {} GPU!\n'.format(human_size(self.memoryusage), human_size(config.cuda['gpus'].totalmem, a_kilobyte_is_1024_bytes=True), config.cuda['gpus'].deviceID, config.cuda['gpus'].name)) + def tmx(self): + """Add PEC boundaries to invariant direction in 2D TMx mode. + N.B. 2D modes are a single cell slice of 3D grid. + """ + # Ey & Ez components + self.ID[1, 0, :, :] = 0 + self.ID[1, 1, :, :] = 0 + self.ID[2, 0, :, :] = 0 + self.ID[2, 1, :, :] = 0 - # If the required memory without the snapshots will fit on the GPU then transfer and store snaphots on host - if snapsmemsize != 0 and self.memoryusage - snapsmemsize < config.cuda['gpus'].totalmem: - config.cuda['snapsgpu2cpu'] = True + def tmy(self): + """Add PEC boundaries to invariant direction in 2D TMy mode. + N.B. 2D modes are a single cell slice of 3D grid. + """ + # Ex & Ez components + self.ID[0, :, 0, :] = 0 + self.ID[0, :, 1, :] = 0 + self.ID[2, :, 0, :] = 0 + self.ID[2, :, 1, :] = 0 - def gpu_set_blocks_per_grid(self): + def tmz(self): + """Add PEC boundaries to invariant direction in 2D TMz mode. + N.B. 2D modes are a single cell slice of 3D grid. + """ + # Ex & Ey components + self.ID[0, :, :, 0] = 0 + self.ID[0, :, :, 1] = 0 + self.ID[1, :, :, 0] = 0 + self.ID[1, :, :, 1] = 0 + + def reset_fields(self): + """Clear arrays for field components and PMLs.""" + # Clear arrays for field components + self.initialise_field_arrays() + + # Clear arrays for fields in PML + for pml in self.pmls: + pml.initialise_field_arrays() + + def calculate_dt(self): + """Calculate time step at the CFL limit.""" + self.dt = (1 / (c * np.sqrt( + (1 / self.dx) * (1 / self.dx) + + (1 / self.dy) * (1 / self.dy) + + (1 / self.dz) * (1 / self.dz)))) + + # Round down time step to nearest float with precision one less than + # hardware maximum. Avoids inadvertently exceeding the CFL due to + # binary representation of floating point number. + self.dt = round_value(self.dt, decimalplaces=d.getcontext().prec - 1) + + +class CUDAGrid(FDTDGrid): + """Additional grid methods for solving on GPU using CUDA.""" + + def set_blocks_per_grid(self): """Set the blocks per grid size used for updating the electric and magnetic field arrays on a GPU.""" + config.cuda['gpus'].bpg = (int(np.ceil(((self.nx + 1) * (self.ny + 1) * (self.nz + 1)) / config.cuda['gpus'].tpb[0])), 1, 1) - def gpu_initialise_arrays(self): - """Initialise standard field arrays on GPU.""" + def initialise_arrays(self): + """Initialise geometry and field arrays on GPU.""" import pycuda.gpuarray as gpuarray @@ -264,7 +286,7 @@ class FDTDGrid(Grid): self.Hy_gpu = gpuarray.to_gpu(self.Hy) self.Hz_gpu = gpuarray.to_gpu(self.Hz) - def gpu_initialise_dispersive_arrays(self): + def initialise_dispersive_arrays(self): """Initialise dispersive material coefficient arrays on GPU.""" import pycuda.gpuarray as gpuarray @@ -274,46 +296,19 @@ class FDTDGrid(Grid): self.Tz_gpu = gpuarray.to_gpu(self.Tz) self.updatecoeffsdispersive_gpu = gpuarray.to_gpu(self.updatecoeffsdispersive) - # Add PEC boundaries to invariant direction in 2D modes - # N.B. 2D modes are a single cell slice of 3D grid - def tmx(self): - # Ey & Ez components - self.ID[1, 0, :, :] = 0 - self.ID[1, 1, :, :] = 0 - self.ID[2, 0, :, :] = 0 - self.ID[2, 1, :, :] = 0 + def memory_check(self, snapsmemsize=0): + """Check if model can be run on specified GPU.""" - def tmy(self): - # Ex & Ez components - self.ID[0, :, 0, :] = 0 - self.ID[0, :, 1, :] = 0 - self.ID[2, :, 0, :] = 0 - self.ID[2, :, 1, :] = 0 + super().memory_check() - def tmz(self): - # Ex & Ey components - self.ID[0, :, :, 0] = 0 - self.ID[0, :, :, 1] = 0 - self.ID[1, :, :, 0] = 0 - self.ID[1, :, :, 1] = 0 + if config.cuda['gpus'] is not None: + if self.memoryusage - snapsmemsize > config.cuda['gpus'].totalmem: + raise GeneralError(f'Memory (RAM) required ~{human_size(self.memoryusage)} exceeds {human_size(config.cuda['gpus'].totalmem, a_kilobyte_is_1024_bytes=True} detected on specified {config.cuda['gpus'].deviceID} - {config.cuda['gpus'].name} GPU!\n') - - def reset_fields(self): - # Clear arrays for field components - self.initialise_field_arrays() - - # Clear arrays for fields in PML - for pml in self.pmls: - pml.initialise_field_arrays() - - def calculate_dt(self): - self.dt = 1 / (c * np.sqrt((1 / self.dx) * (1 / self.dx) + (1 / self.dy) * (1 / self.dy) + (1 / self.dz) * (1 / self.dz))) - - def round_time_step(self): - # Round down time step to nearest float with precision one less than hardware maximum. Avoids inadvertently exceeding the CFL due to binary representation of floating point number. - # Round down time step to nearest float with precision one less than hardware maximum. - # Avoids inadvertently exceeding the CFL due to binary representation of floating point number. - self.dt = round_value(self.dt, decimalplaces=d.getcontext().prec - 1) + # If the required memory for the model without the snapshots will + # fit on the GPU then transfer and store snaphots on host + if snapsmemsize != 0 and self.memoryusage - snapsmemsize < config.cuda['gpus'].totalmem: + config.cuda['snapsgpu2cpu'] = True def dispersion_analysis(G): @@ -487,7 +482,3 @@ def Iz(x, y, z, Hx, Hy, Hz, G): Iz = G.dx * (Hx[x, y - 1, z] - Hx[x, y, z]) + G.dy * (Hy[x, y, z] - Hy[x - 1, y, z]) return Iz - - -class GPUGrid(FDTDGrid): - pass diff --git a/gprMax/input_cmds_file.py b/gprMax/input_cmds_file.py index e2e32ed4..89bcef9a 100644 --- a/gprMax/input_cmds_file.py +++ b/gprMax/input_cmds_file.py @@ -308,7 +308,7 @@ def parse_hash_commands(model_config, G, scene): # Write a file containing the input commands after Python or include # file commands have been processed - if sim_config.write_processed: + if sim_config.args.write_processed: write_processed_file(processedlines, model_config.appendmodelnumber, G) user_objs = get_user_objects(processedlines, check=True) diff --git a/gprMax/model_build_run.py b/gprMax/model_build_run.py index 8cc65f23..7545af3a 100644 --- a/gprMax/model_build_run.py +++ b/gprMax/model_build_run.py @@ -19,6 +19,7 @@ import datetime from importlib import import_module import itertools +import logging import os import psutil import sys @@ -74,6 +75,8 @@ from .utilities import timer from .utilities import Printer +log = logging.getLogger(__name__) + class ModelBuildRun: """Builds and runs (solves) a model.""" @@ -159,7 +162,7 @@ class ModelBuildRun: gb.grid.initialise_std_update_coeff_arrays() # Set datatype for dispersive arrays if there are any dispersive materials. - if config.materials['maxpoles'] != 0: + if self.model_config.materials['maxpoles'] != 0: drudelorentz = any([m for m in G.materials if 'drude' in m.type or 'lorentz' in m.type]) if drudelorentz: config.materials['dispersivedtype'] = config.dtypes['complex'] @@ -206,8 +209,8 @@ class ModelBuildRun: G = self.G # Reset iteration number G.iteration = 0 - self.model_config.inputfilestr = '\n--- Model {}/{}, input file (not re-processed, i.e. geometry fixed): {}'.format(self.model_config.appendmodelnumber, self.sim_config.model_end, self.sim_config.input_file_path) - self.printer.print(Fore.GREEN + '{} {}'.format(self.model_config.inputfilestr, '-' * (get_terminal_width() - 1 - len(self.model_config.inputfilestr))) + Style.RESET_ALL) + self.model_config.inputfilestr = f'\n--- Model {self.model_config.appendmodelnumber}/{self.sim_config.model_end}, input file (not re-processed, i.e. geometry fixed): {self.sim_config.input_file_path}' + log.info(Fore.GREEN + f'{self.model_config.inputfilestr} {'-' * (get_terminal_width() - 1 - len(self.model_config.inputfilestr))}' + Style.RESET_ALL) for grid in [G] + G.subgrids: grid.reset_fields() @@ -232,52 +235,52 @@ class ModelBuildRun: # Check and set output directory and filename try: os.mkdir(self.G.outputdirectory) - self.printer.print('\nCreated output directory: {}'.format(self.G.outputdirectory)) + log.info(f'\nCreated output directory: {self.G.outputdirectory}') except FileExistsError: pass # Modify the output path (hack) self.model_config.output_file_path_ext = Path(self.G.outputdirectory, self.model_config.output_file_path_ext) - def run_model(self, solver): - G = self.G - self.create_output_directory() - self.printer.print('Output file: {}\n'.format(self.model_config.output_file_path_ext)) + def write_output_data(self): + """Write output data, i.e. field data for receivers and snapshots + to file(s). + """ - # Run solver - tsolve = self.solve(solver) + G = self.G # Write an output file in HDF5 format write_hdf5_outputfile(self.model_config.output_file_path_ext, G) # Write any snapshots to file if G.snapshots: - # Create directory and construct filename from user-supplied name and model run number + # Create directory and construct filename from user-supplied name + # and model run number snapshotdir = self.model_config.snapshot_dir if not os.path.exists(snapshotdir): os.mkdir(snapshotdir) - self.printer.print('') + log.info('') for i, snap in enumerate(G.snapshots): fn = snapshotdir / Path(self.model_config.output_file_path.stem + '_' + snap.basefilename) snap.filename = fn.with_suffix('.vti') pbar = tqdm(total=snap.vtkdatawritesize, leave=True, unit='byte', unit_scale=True, desc='Writing snapshot file {} of {}, {}'.format(i + 1, len(G.snapshots), os.path.split(snap.filename)[1]), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) snap.write_vtk_imagedata(pbar, G) pbar.close() - self.printer.print('') + log.info('') + + def print_resource_info(self): + """Print resource information on runtime and memory usage.""" memGPU = '' if config.cuda['gpus']: - memGPU = ' host + ~{} GPU'.format(human_size(self.solver.get_memsolve())) + memGPU = f' host + ~{human_size(self.solver.get_memsolve())} GPU' - self.printer.print('\nMemory (RAM) used: ~{}{}'.format(human_size(self.p.memory_full_info().uss), memGPU)) - self.printer.print('Solving time [HH:MM:SS]: {}'.format(datetime.timedelta(seconds=tsolve))) - - return tsolve + log.info(f'\nMemory (RAM) used: ~{human_size(self.p.memory_full_info().uss)}{memGPU}') + log.info(f'Solving time [HH:MM:SS]: {datetime.timedelta(seconds=tsolve)}') def solve(self, solver): """ - Solving using FDTD method on CPU. Parallelised using Cython (OpenMP) for - electric and magnetic field updates, and PML updates. + Solve using FDTD method. Args: solver (Solver): solver object. @@ -294,7 +297,17 @@ class ModelBuildRun: else: iterator = range(0, G.iterations) - tsolve = solver.solve(iterator) + self.create_output_directory() + log.info(f'Output file: {self.model_config.output_file_path_ext}\n') + + # Run solver + tsolve = self.solve(solver) + + # Write output data, i.e. field data for receivers and snapshots to file(s) + self.write_output_data + + # Print resource information on runtime and memory usage + self.print_resource_info return tsolve @@ -318,7 +331,7 @@ class GridBuilder: def build_components(self): # Build the model, i.e. set the material properties (ID) for every edge # of every Yee cell - self.printer.print('') + log.info('') pbar = tqdm(total=2, desc='Building {} Grid'.format(self.grid.name), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) build_electric_components(self.grid.solid, self.grid.rigidE, self.grid.ID, self.grid) pbar.update() @@ -348,5 +361,5 @@ class GridBuilder: materialstable.outer_border = False materialstable.justify_columns[0] = 'right' - self.printer.print('\n{} Grid Materials:'.format(self.grid.name)) - self.printer.print(materialstable.table) + log.info(f'\n{self.grid.name} Grid Materials:') + log.info(materialstable.table) diff --git a/gprMax/pml.py b/gprMax/pml.py index 4484db73..7370a759 100644 --- a/gprMax/pml.py +++ b/gprMax/pml.py @@ -81,7 +81,7 @@ class CFS(object): # Calculation of the maximum value of sigma from http://dx.doi.org/10.1109/8.546249 m = CFSParameter.scalingprofiles[self.sigma.scalingprofile] - self.sigma.max = (0.8 * (m + 1)) / (config.z0 * d * np.sqrt(er * mr)) + self.sigma.max = (0.8 * (m + 1)) / (config.general['z0'] * d * np.sqrt(er * mr)) def scaling_polynomial(self, order, Evalues, Hvalues): """Applies the polynomial to be used for the scaling profile for diff --git a/gprMax/solvers.py b/gprMax/solvers.py index 44064072..4e58d7cf 100644 --- a/gprMax/solvers.py +++ b/gprMax/solvers.py @@ -18,10 +18,10 @@ import gprMax.config as config from .grid import FDTDGrid -from .grid import GPUGrid +from .grid import CUDAGrid from .subgrids.updates import create_updates as create_subgrid_updates from .updates import CPUUpdates -from .updates import GPUUpdates +from .updates import CUDAUpdates def create_G(sim_config): @@ -34,8 +34,8 @@ def create_G(sim_config): G (Grid): Holds essential parameters describing the model. """ - if sim_config.gpu: - G = GPUGrid() + if sim_config.general['cuda']: + G = CUDAGrid() elif sim_config.subgrid: G = FDTDGrid() else: @@ -56,7 +56,7 @@ def create_solver(G, sim_config): """ if sim_config.gpu: - updates = GPUUpdates(G) + updates = CUDAUpdates(G) solver = Solver(updates) elif sim_config.subgrid: updates = create_subgrid_updates(G) diff --git a/gprMax/subgrids/base.py b/gprMax/subgrids/base.py index 7a3bd666..86176d63 100644 --- a/gprMax/subgrids/base.py +++ b/gprMax/subgrids/base.py @@ -63,19 +63,9 @@ class SubGridBase(FDTDGrid): self.interpolation = kwargs['interpolation'] - def calculate_dt(self): - """Calculate dt at the CFL limit.""" - self.dt = (1 / (c * np.sqrt( - (1 / self.dx) * (1 / self.dx) + - (1 / self.dy) * (1 / self.dy) + - (1 / self.dz) * (1 / self.dz)))) - def main_grid_index_to_subgrid_index(self, i, j, k): """Calculate local subgrid index from global main grid index.""" i_s = self.n_boundary_cells_x + (i - self.i0) * self.ratio j_s = self.n_boundary_cells_y + (j - self.j0) * self.ratio k_s = self.n_boundary_cells_z + (k - self.k0) * self.ratio return (i_s, j_s, k_s) - - def initialise_geometry_arrays(self): - super().initialise_geometry_arrays() diff --git a/gprMax/subgrids/multi.py b/gprMax/subgrids/multi.py index 9d92825e..0e95b59b 100644 --- a/gprMax/subgrids/multi.py +++ b/gprMax/subgrids/multi.py @@ -27,10 +27,6 @@ class ReferenceRx(Rx): position as the coarse grid. """ - def __init__(self): - """Constructor.""" - super().__init__() - def get_field(self, str_id, field): """Return the field value at the equivalent coarse yee cell. diff --git a/gprMax/subgrids/precursor_nodes.py b/gprMax/subgrids/precursor_nodes.py index a14a06b4..a3e83ac1 100644 --- a/gprMax/subgrids/precursor_nodes.py +++ b/gprMax/subgrids/precursor_nodes.py @@ -28,7 +28,7 @@ def calculate_weighting_coefficients(x1, x): return (c1, c2) -class PrecusorNodesBase(object): +class PrecusorNodesBase: def __init__(self, fdtd_grid, sub_grid): self.G = fdtd_grid diff --git a/gprMax/templates/fields_updates_dispersive_template b/gprMax/templates/fields_updates_dispersive_template index 9906ced5..05363ffd 100644 --- a/gprMax/templates/fields_updates_dispersive_template +++ b/gprMax/templates/fields_updates_dispersive_template @@ -18,6 +18,7 @@ import numpy as np cimport numpy as np + from cython.parallel import prange cdef extern from "complex.h" nogil: diff --git a/gprMax/updates.py b/gprMax/updates.py index bf7cb0d7..a30ac8a0 100644 --- a/gprMax/updates.py +++ b/gprMax/updates.py @@ -17,6 +17,8 @@ # along with gprMax. If not, see . from importlib import import_module +import logging +import sys import gprMax.config as config from .cuda.fields_updates import kernel_template_fields @@ -34,6 +36,8 @@ from .sources import gpu_initialise_src_arrays from .utilities import timer +log = logging.getLogger(__name__) + class CPUUpdates: """Defines update functions for CPU-based solver.""" @@ -226,7 +230,7 @@ class CPUUpdates: pass -class GPUUpdates: +class CUDAUpdates: """Defines update functions for GPU-based (CUDA) solver.""" def __init__(self, G): @@ -235,6 +239,10 @@ class GPUUpdates: G (FDTDGrid): FDTD grid object """ + self.grid = G + self.dispersive_update_a = None + self.dispersive_update_b = None + import pycuda.driver as drv from pycuda.compiler import SourceModule drv.init() @@ -249,10 +257,6 @@ class GPUUpdates: self.dev = drv.Device(self.grid.gpu.deviceID) self.ctx = dev.make_context() - self.grid = G - self.dispersive_update_a = None - self.dispersive_update_b = None - # Initialise arrays on GPU, prepare kernels, and get kernel functions self.set_field_kernels() self.set_pml_kernels() @@ -261,7 +265,9 @@ class GPUUpdates: self.set_snapshot_kernel() def set_field_kernels(self): - # Electric and magnetic field updates - prepare kernels, and get kernel functions + """Electric and magnetic field updates - prepare kernels, and + get kernel functions. + """ if config.materials['maxpoles'] > 0: kernels_fields = SourceModule(kernels_template_fields.substitute(REAL=cudafloattype, COMPLEX=cudacomplextype, N_updatecoeffsE=self.grid.updatecoeffsE.size, N_updatecoeffsH=self.grid.updatecoeffsH.size, NY_MATCOEFFS=self.grid.updatecoeffsE.shape[1], NY_MATDISPCOEFFS=self.grid.updatecoeffsdispersive.shape[1], NX_FIELDS=self.grid.nx + 1, NY_FIELDS=self.grid.ny + 1, NZ_FIELDS=self.grid.nz + 1, NX_ID=self.grid.ID.shape[1], NY_ID=self.grid.ID.shape[2], NZ_ID=self.grid.ID.shape[3], NX_T=self.grid.Tx.shape[1], NY_T=self.grid.Tx.shape[2], NZ_T=self.grid.Tx.shape[3]), options=self.compiler_opts) else: # Set to one any substitutions for dispersive materials @@ -269,7 +275,7 @@ class GPUUpdates: self.update_electric = kernels_fields.get_function("update_electric") self.update_magnetic = kernels_fields.get_function("update_magnetic") if self.grid.updatecoeffsE.nbytes + self.grid.updatecoeffsH.nbytes > self.grid.gpu.constmem: - raise GeneralError('Too many materials in the model to fit onto constant memory of size {} on {} - {} GPU'.format(human_size(self.grid.gpu.constmem), self.grid.gpu.deviceID, self.grid.gpu.name)) + raise GeneralError(log.exception(f'Too many materials in the model to fit onto constant memory of size {human_size(self.grid.gpu.constmem)} on {self.grid.gpu.deviceID} - {self.grid.gpu.name} GPU')) self.copy_mat_coeffs() # Electric and magnetic field updates - dispersive materials - get kernel functions and initialise array on GPU @@ -283,8 +289,8 @@ class GPUUpdates: self.grid.gpu_initialise_arrays() def set_pml_kernels(self): + """PMLS - prepare kernels and get kernel functions.""" if self.grid.pmls: - # PMLS - prepare kernels and get kernel functions pmlmodulelectric = 'gprMax.cuda.pml_updates_electric_' + self.grid.pmlformulation kernelelectricfunc = getattr(import_module(pmlmodulelectric), 'kernels_template_pml_electric_' + self.grid.pmlformulation) pmlmodulemagnetic = 'gprMax.cuda.pml_updates_magnetic_' + self.grid.pmlformulation @@ -299,14 +305,18 @@ class GPUUpdates: pml.gpu_set_blocks_per_grid(self.grid) def set_rx_kernel(self): - # Receivers - initialise arrays on GPU, prepare kernel and get kernel function + """Receivers - initialise arrays on GPU, prepare kernel and + get kernel function. + """ if self.grid.rxs: rxcoords_gpu, rxs_gpu = gpu_initialise_rx_arrays(self.grid) kernel_store_outputs = SourceModule(kernel_template_store_outputs.substitute(REAL=cudafloattype, NY_RXCOORDS=3, NX_RXS=6, NY_RXS=self.grid.iterations, NZ_RXS=len(self.grid.rxs), NX_FIELDS=self.grid.nx + 1, NY_FIELDS=self.grid.ny + 1, NZ_FIELDS=self.grid.nz + 1), options=self.compiler_opts) self.store_outputs = kernel_store_outputs.get_function("store_outputs") def set_src_kernels(self): - # Sources - initialise arrays on GPU, prepare kernel and get kernel function + """Sources - initialise arrays on GPU, prepare kernel and + get kernel function. + """ if self.grid.voltagesources + self.grid.hertziandipoles + self.grid.magneticdipoles: kernels_sources = SourceModule(kernels_template_sources.substitute(REAL=cudafloattype, N_updatecoeffsE=self.grid.updatecoeffsE.size, N_updatecoeffsH=self.grid.updatecoeffsH.size, NY_MATCOEFFS=self.grid.updatecoeffsE.shape[1], NY_SRCINFO=4, NY_SRCWAVES=self.grid.iterations, NX_FIELDS=self.grid.nx + 1, NY_FIELDS=self.grid.ny + 1, NZ_FIELDS=self.grid.nz + 1, NX_ID=self.grid.ID.shape[1], NY_ID=self.grid.ID.shape[2], NZ_ID=self.grid.ID.shape[3]), options=self.compiler_opts) self.copy_mat_coeffs() @@ -321,14 +331,18 @@ class GPUUpdates: self.update_voltage_source_gpu = kernels_sources.get_function("update_voltage_source") def set_snapshot_kernel(self): + """Snapshots - initialise arrays on GPU, prepare kernel and + get kernel function. + """ if self.grid.snapshots: - # Snapshots - initialise arrays on GPU, prepare kernel and get kernel function self.snapEx_gpu, self.snapEy_gpu, self.snapEz_gpu, self.snapHx_gpu, self.snapHy_gpu, self.snapHz_gpu = gpu_initialise_snapshot_array(self.grid) kernel_store_snapshot = SourceModule(kernel_template_store_snapshot.substitute(REAL=cudafloattype, NX_SNAPS=Snapshot.nx_max, NY_SNAPS=Snapshot.ny_max, NZ_SNAPS=Snapshot.nz_max, NX_FIELDS=self.grid.nx + 1, NY_FIELDS=self.grid.ny + 1, NZ_FIELDS=self.grid.nz + 1), options=self.compiler_opts) self.store_snapshot_gpu = kernel_store_snapshot.get_function("store_snapshot") def copy_mat_coeffs(self): - # Copy material coefficient arrays to constant memory of GPU (must be <64KB) + """Copy material coefficient arrays to constant memory of GPU + (must be <64KB). + """ updatecoeffsE = kernels_sources.get_global('updatecoeffsE')[0] updatecoeffsH = kernels_sources.get_global('updatecoeffsH')[0] self.drv.memcpy_htod(updatecoeffsE, self.grid.updatecoeffsE) @@ -396,7 +410,6 @@ class GPUUpdates: def update_magnetic(self): """Update magnetic field components.""" - self.update_magnetic(np.int32(self.grid.nx), np.int32(self.grid.ny), np.int32(self.grid.nz), @@ -412,13 +425,11 @@ class GPUUpdates: def update_magnetic_pml(self): """Update magnetic field components with the PML correction.""" - for pml in self.grid.pmls: pml.gpu_update_magnetic(self.grid) def update_magnetic_sources(self): """Update magnetic field components from sources.""" - if self.grid.magneticdipoles: self.update_magnetic_dipole_gpu(np.int32(len(self.grid.magneticdipoles)), np.int32(self.grid.iteration), @@ -437,7 +448,6 @@ class GPUUpdates: def update_electric_a(self): """Update electric field components.""" - # All materials are non-dispersive so do standard update. if config.materials['maxpoles'] == 0: self.update_electric(np.int32(self.grid.nx), @@ -476,7 +486,6 @@ class GPUUpdates: def update_electric_pml(self): """Update electric field components with the PML correction.""" - for pml in self.grid.pmls: pml.gpu_update_electric(self.grid) @@ -484,7 +493,6 @@ class GPUUpdates: """Update electric field components from sources - update any Hertzian dipole sources last. """ - if self.grid.voltagesources: self.update_voltage_source_gpu(np.int32(len(self.grid.voltagesources)), np.int32(self.grid.iteration), @@ -525,7 +533,6 @@ class GPUUpdates: field values. Therefore it can only be completely updated after the electric field has been updated by the PML and source updates. """ - if config.materials['maxpoles'] != 0: self.dispersive_update_b(np.int32(self.grid.nx), np.int32(self.grid.ny), @@ -543,11 +550,14 @@ class GPUUpdates: grid=self.grid.bpg) def time_start(self): + """Start event timers used to calculate solving time for model.""" self.iterstart = self.drv.Event() self.iterend = self.drv.Event() self.iterstart.record() + self.iterstart.synchronize() def calculate_tsolve(self): + """Calculate solving time for model.""" self.iterend.record() self.iterend.synchronize() tsolve = self.iterstart.time_till(self.iterend) * 1e-3 @@ -555,6 +565,7 @@ class GPUUpdates: return tsolve def finalise(self): + """Copy data from GPU back to CPU to save to file(s).""" # Copy output from receivers array back to correct receiver objects if self.grid.rxs: gpu_get_rx_array(self.rxs_gpu.get(), @@ -574,6 +585,7 @@ class GPUUpdates: snap) def cleanup(self): + """Cleanup GPU context.""" # Remove context from top of stack and delete self.ctx.pop() del self.ctx diff --git a/gprMax/utilities.py b/gprMax/utilities.py index 71630ee0..6f2fdfb5 100644 --- a/gprMax/utilities.py +++ b/gprMax/utilities.py @@ -27,7 +27,7 @@ import subprocess from shutil import get_terminal_size import sys import textwrap -from time import perf_counter +from time import process_time from colorama import init from colorama import Fore @@ -38,7 +38,6 @@ import numpy as np from .exceptions import GeneralError - def get_terminal_width(): """Get/set width of terminal being used. @@ -75,14 +74,14 @@ def logo(version): |___/|_| v""" + version - print('{} {}\n'.format(description, '=' * (get_terminal_width() - len(description) - 1))) - print(Fore.CYAN + '{}\n'.format(logo)) - print(Style.RESET_ALL + textwrap.fill(copyright, width=get_terminal_width() - 1, initial_indent=' ')) - print(textwrap.fill(authors, width=get_terminal_width() - 1, initial_indent=' ')) - print() - print(textwrap.fill(licenseinfo1, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ')) - print(textwrap.fill(licenseinfo2, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ')) - print(textwrap.fill(licenseinfo3, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ')) + log.info(f'{description} {'=' * (get_terminal_width() - len(description) - 1)}\n') + log.info(Fore.CYAN + f'{logo}\n') + log.info(Style.RESET_ALL + textwrap.fill(copyright, width=get_terminal_width() - 1, initial_indent=' ')) + log.info(textwrap.fill(authors, width=get_terminal_width() - 1, initial_indent=' ')) + log.info('') + log.info(textwrap.fill(licenseinfo1, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ')) + log.info(textwrap.fill(licenseinfo2, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ')) + log.info(textwrap.fill(licenseinfo3, width=get_terminal_width() - 1, initial_indent=' ', subsequent_indent=' ')) @contextmanager @@ -401,7 +400,7 @@ def detect_check_gpus(deviceIDs): # Check if requested device ID(s) exist for ID in deviceIDs: if ID not in deviceIDsavail: - raise GeneralError('GPU with device ID {} does not exist'.format(ID)) + raise GeneralError(f'GPU with device ID {ID} does not exist') # Gather information about selected/detected GPUs gpus = [] @@ -411,22 +410,11 @@ def detect_check_gpus(deviceIDs): gpu.get_gpu_info(drv) if ID in deviceIDs: gpus.append(gpu) - allgpustext.append('{} - {}, {}'.format(gpu.deviceID, gpu.name, human_size(gpu.totalmem, a_kilobyte_is_1024_bytes=True))) + allgpustext.append(f'{gpu.deviceID} - {gpu.name}, {human_size(gpu.totalmem, a_kilobyte_is_1024_bytes=True)}') return gpus, allgpustext def timer(): """Function to return the current process wide time in fractional seconds.""" - return perf_counter() - - -class Printer(): - """Printing information messages.""" - - def __init__(self, config): - self.printing = config.is_messages() - - def print(self, str): - if self.printing: - print(str) + return process_time() diff --git a/setup.py b/setup.py index 34dcc9b0..0f8a2217 100644 --- a/setup.py +++ b/setup.py @@ -33,14 +33,18 @@ import pathlib import re import shutil import sys -from jinja2 import Environment, PackageLoader, select_autoescape + +from jinja2 import Environment +from jinja2 import PackageLoader +from jinja2 import select_autoescape def build_dispersive_material_templates(): + """Function to generate Cython .pyx files for dispersive media update. + Jinja2 templates are used to render the various dispersive update + functions. """ - Function to generate Cython .pyx files for dispersive media update. - Jinja2 templates are used to render the various dispersive update functions. - """ + env = Environment( loader=PackageLoader(__name__, 'gprMax/templates'), ) @@ -152,25 +156,25 @@ if 'cleanall' in sys.argv: if os.path.isfile(filebase + '.c'): try: os.remove(filebase + '.c') - print('Removed: {}'.format(filebase + '.c')) + print(f'Removed: {filebase} + ".c"') except OSError: - print('Could not remove: {}'.format(filebase + '.c')) + print(f'Could not remove: {filebase} + ".c"') # Remove compiled Cython modules libfile = glob.glob(os.path.join(os.getcwd(), os.path.splitext(file)[0]) + '*.pyd') + glob.glob(os.path.join(os.getcwd(), os.path.splitext(file)[0]) + '*.so') if libfile: libfile = libfile[0] try: os.remove(libfile) - print('Removed: {}'.format(os.path.abspath(libfile))) + print(f'Removed: {os.path.abspath(libfile)}') except OSError: - print('Could not remove: {}'.format(os.path.abspath(libfile))) + print(f'Could not remove: {os.path.abspath(libfile)}') # Remove build, dist, egg and __pycache__ directories shutil.rmtree(os.path.join(os.getcwd(), 'build'), ignore_errors=True) shutil.rmtree(os.path.join(os.getcwd(), 'dist'), ignore_errors=True) shutil.rmtree(os.path.join(os.getcwd(), 'gprMax.egg-info'), ignore_errors=True) for p in pathlib.Path(os.getcwd()).rglob('__pycache__'): shutil.rmtree(p, ignore_errors=True) - print('Removed: {}'.format(p)) + print(f'Removed: {p}') # Now do a normal clean sys.argv[1] = 'clean' # this is what distutils understands