diff --git a/gprMax/cmds_geometry/add_grass.py b/gprMax/cmds_geometry/add_grass.py index 9277bfe7..e17493f4 100644 --- a/gprMax/cmds_geometry/add_grass.py +++ b/gprMax/cmds_geometry/add_grass.py @@ -1,164 +1,164 @@ -"""Class for add_grass command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..utilities import round_value -from ..materials import Material -from ..fractals import FractalSurface -from ..fractals import Grass - -from tqdm import tqdm -import numpy as np - - -class AddGrass(UserObjectGeometry): - """User class for Grass command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 12 - self.hash = '#add_grass' - - def create(self, grid, uip): - """Add Grass to fractal box.""" - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - fractal_box_id = self.kwargs['fractal_box_id'] - frac_dim = self.kwargs['frac_dim'] - limits = self.kwargs['limits'] - n_blades = self.kwargs['n_blades'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires at least eleven parameters') - - try: - seed = self.kwargs['seed'] - except KeyError: - seed = None - - # grab 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)) - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - if frac_dim < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal dimension') - if limits[0] < 0 or limits[1] < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the minimum and maximum heights for grass blades') - - # Check for valid orientations - if xs == xf: - if ys == yf or zs == zf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if xs != volume.xs and xs != volume.xf: - raise CmdInputError(self.__str__() + ' must specify external surfaces on a fractal box') - fractalrange = (round_value(limits[0] / grid.dx), round_value(limits[1] / grid.dx)) - # xminus surface - if xs == volume.xs: - raise CmdInputError(self.__str__() + ' grass can only be specified on surfaces in the positive axis direction') - # xplus surface - elif xf == volume.xf: - if fractalrange[1] > grid.nx: - raise CmdInputError(self.__str__() + ' cannot apply grass to fractal box as it would exceed the domain size in the x direction') - requestedsurface = 'xplus' - - elif ys == yf: - if xs == xf or zs == zf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if ys != volume.ys and ys != volume.yf: - raise CmdInputError(self.__str__() + ' must specify external surfaces on a fractal box') - fractalrange = (round_value(limits[0] / grid.dy), round_value(limits[1] / grid.dy)) - # yminus surface - if ys == volume.ys: - raise CmdInputError(self.__str__() + ' grass can only be specified on surfaces in the positive axis direction') - # yplus surface - elif yf == volume.yf: - if fractalrange[1] > grid.ny: - raise CmdInputError(self.__str__() + ' cannot apply grass to fractal box as it would exceed the domain size in the y direction') - requestedsurface = 'yplus' - - elif zs == zf: - if xs == xf or ys == yf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if zs != volume.zs and zs != volume.zf: - raise CmdInputError(self.__str__() + ' must specify external surfaces on a fractal box') - fractalrange = (round_value(limits[0] / grid.dz), round_value(limits[1] / grid.dz)) - # zminus surface - if zs == volume.zs: - raise CmdInputError(self.__str__() + ' grass can only be specified on surfaces in the positive axis direction') - # zplus surface - elif zf == volume.zf: - if fractalrange[1] > grid.nz: - raise CmdInputError(self.__str__() + ' cannot apply grass to fractal box as it would exceed the domain size in the z direction') - requestedsurface = 'zplus' - - else: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - - surface = FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim) - surface.ID = 'grass' - surface.surfaceID = requestedsurface - surface.seed = seed - - # Set the fractal range to scale the fractal distribution between zero and one - surface.fractalrange = (0, 1) - surface.operatingonID = volume.ID - surface.generate_fractal_surface(grid) - if n_blades > surface.fractalsurface.shape[0] * surface.fractalsurface.shape[1]: - raise CmdInputError(self.__str__() + ' the specified surface is not large enough for the number of grass blades/roots specified') - - # Scale the distribution so that the summation is equal to one, i.e. a probability distribution - surface.fractalsurface = surface.fractalsurface / np.sum(surface.fractalsurface) - - # Set location of grass blades using probability distribution - # Create 1D vector of probability values from the 2D surface - probability1D = np.cumsum(np.ravel(surface.fractalsurface)) - - # Create random numbers between zero and one for the number of blades of grass - R = np.random.RandomState(surface.seed) - A = R.random_sample(n_blades) - - # Locate the random numbers in the bins created by the 1D vector of probability values, and convert the 1D index back into a x, y index for the original surface. - bladesindex = np.unravel_index(np.digitize(A, probability1D), (surface.fractalsurface.shape[0], surface.fractalsurface.shape[1])) - - # Set the fractal range to minimum and maximum heights of the grass blades - surface.fractalrange = fractalrange - - # 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) - - # Create grass geometry parameters - g = Grass(n_blades) - g.seed = surface.seed - surface.grass.append(g) - - # Check to see if grass has been already defined as a material - if not any(x.ID == 'grass' for x in grid.materials): - m = Material(len(grid.materials), 'grass') - m.averagable = False - m.type = 'builtin, debye' - m.er = Material.grasseri - m.deltaer.append(Material.grassdeltaer) - m.tau.append(Material.grasstau) - grid.materials.append(m) - if Material.maxpoles == 0: - Material.maxpoles = 1 - - # Check if time step for model is suitable for using grass - grass = next((x for x in grid.materials if x.ID == 'grass')) - testgrass = next((x for x in grass.tau if x < grid.dt), None) - if testgrass: - raise CmdInputError(self.__str__() + ' requires the time step for the model to be less than the relaxation time required to model grass.') - - 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)) +"""Class for add_grass command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..utilities import round_value +from ..materials import Material +from ..fractals import FractalSurface +from ..fractals import Grass + +from tqdm import tqdm +import numpy as np + + +class AddGrass(UserObjectGeometry): + """User class for Grass command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 12 + self.hash = '#add_grass' + + def create(self, grid, uip): + """Add Grass to fractal box.""" + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + fractal_box_id = self.kwargs['fractal_box_id'] + frac_dim = self.kwargs['frac_dim'] + limits = self.kwargs['limits'] + n_blades = self.kwargs['n_blades'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires at least eleven parameters') + + try: + seed = self.kwargs['seed'] + except KeyError: + seed = None + + # grab 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)) + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + if frac_dim < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal dimension') + if limits[0] < 0 or limits[1] < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the minimum and maximum heights for grass blades') + + # Check for valid orientations + if xs == xf: + if ys == yf or zs == zf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if xs != volume.xs and xs != volume.xf: + raise CmdInputError(self.__str__() + ' must specify external surfaces on a fractal box') + fractalrange = (round_value(limits[0] / grid.dx), round_value(limits[1] / grid.dx)) + # xminus surface + if xs == volume.xs: + raise CmdInputError(self.__str__() + ' grass can only be specified on surfaces in the positive axis direction') + # xplus surface + elif xf == volume.xf: + if fractalrange[1] > grid.nx: + raise CmdInputError(self.__str__() + ' cannot apply grass to fractal box as it would exceed the domain size in the x direction') + requestedsurface = 'xplus' + + elif ys == yf: + if xs == xf or zs == zf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if ys != volume.ys and ys != volume.yf: + raise CmdInputError(self.__str__() + ' must specify external surfaces on a fractal box') + fractalrange = (round_value(limits[0] / grid.dy), round_value(limits[1] / grid.dy)) + # yminus surface + if ys == volume.ys: + raise CmdInputError(self.__str__() + ' grass can only be specified on surfaces in the positive axis direction') + # yplus surface + elif yf == volume.yf: + if fractalrange[1] > grid.ny: + raise CmdInputError(self.__str__() + ' cannot apply grass to fractal box as it would exceed the domain size in the y direction') + requestedsurface = 'yplus' + + elif zs == zf: + if xs == xf or ys == yf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if zs != volume.zs and zs != volume.zf: + raise CmdInputError(self.__str__() + ' must specify external surfaces on a fractal box') + fractalrange = (round_value(limits[0] / grid.dz), round_value(limits[1] / grid.dz)) + # zminus surface + if zs == volume.zs: + raise CmdInputError(self.__str__() + ' grass can only be specified on surfaces in the positive axis direction') + # zplus surface + elif zf == volume.zf: + if fractalrange[1] > grid.nz: + raise CmdInputError(self.__str__() + ' cannot apply grass to fractal box as it would exceed the domain size in the z direction') + requestedsurface = 'zplus' + + else: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + + surface = FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim) + surface.ID = 'grass' + surface.surfaceID = requestedsurface + surface.seed = seed + + # Set the fractal range to scale the fractal distribution between zero and one + surface.fractalrange = (0, 1) + surface.operatingonID = volume.ID + surface.generate_fractal_surface(grid) + if n_blades > surface.fractalsurface.shape[0] * surface.fractalsurface.shape[1]: + raise CmdInputError(self.__str__() + ' the specified surface is not large enough for the number of grass blades/roots specified') + + # Scale the distribution so that the summation is equal to one, i.e. a probability distribution + surface.fractalsurface = surface.fractalsurface / np.sum(surface.fractalsurface) + + # Set location of grass blades using probability distribution + # Create 1D vector of probability values from the 2D surface + probability1D = np.cumsum(np.ravel(surface.fractalsurface)) + + # Create random numbers between zero and one for the number of blades of grass + R = np.random.RandomState(surface.seed) + A = R.random_sample(n_blades) + + # Locate the random numbers in the bins created by the 1D vector of probability values, and convert the 1D index back into a x, y index for the original surface. + bladesindex = np.unravel_index(np.digitize(A, probability1D), (surface.fractalsurface.shape[0], surface.fractalsurface.shape[1])) + + # Set the fractal range to minimum and maximum heights of the grass blades + surface.fractalrange = fractalrange + + # 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) + + # Create grass geometry parameters + g = Grass(n_blades) + g.seed = surface.seed + surface.grass.append(g) + + # Check to see if grass has been already defined as a material + if not any(x.ID == 'grass' for x in grid.materials): + m = Material(len(grid.materials), 'grass') + m.averagable = False + m.type = 'builtin, debye' + m.er = Material.grasseri + m.deltaer.append(Material.grassdeltaer) + m.tau.append(Material.grasstau) + grid.materials.append(m) + if Material.maxpoles == 0: + Material.maxpoles = 1 + + # Check if time step for model is suitable for using grass + grass = next((x for x in grid.materials if x.ID == 'grass')) + testgrass = next((x for x in grass.tau if x < grid.dt), None) + if testgrass: + raise CmdInputError(self.__str__() + ' requires the time step for the model to be less than the relaxation time required to model grass.') + + 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)) diff --git a/gprMax/cmds_geometry/add_surface_roughness.py b/gprMax/cmds_geometry/add_surface_roughness.py index bf35549e..90cd10b5 100644 --- a/gprMax/cmds_geometry/add_surface_roughness.py +++ b/gprMax/cmds_geometry/add_surface_roughness.py @@ -1,127 +1,127 @@ -"""Class for surface roughness command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..fractals import FractalSurface -from ..utilities import round_value -import gprMax.config as config - -from tqdm import tqdm -import numpy as np - - -class AddSurfaceRoughness(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 10 - self.hash = '#add_surface_roughness' - - def create(self, grid, uip): - - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - frac_dim = self.kwargs['frac_dim'] - weighting = np.array(self.kwargs['weighting'], dtype=np.float64) - limits = np.array(self.kwargs['limits']) - fractal_box_id = self.kwargs['fractal_box_id'] - except KeyError: - raise CmdInputError(self.__str__() + ' Incorrect parameters') - - try: - seed = self.kwargs['seed'] - except KeyError: - seed = None - - # grab 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)) - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - if frac_dim < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal dimension') - if weighting[0] < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the first direction of the surface') - if weighting[1] < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the second direction of the surface') - - # Check for valid orientations - if xs == xf: - if ys == yf or zs == zf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if xs != volume.xs and xs != volume.xf: - raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') - fractalrange = (round_value(limits[0] / grid.dx), round_value(limits[1] / grid.dx)) - # xminus surface - if xs == volume.xs: - if fractalrange[0] < 0 or fractalrange[1] > volume.xf: - raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the upper coordinates of the fractal box or the domain in the x direction') - requestedsurface = 'xminus' - # xplus surface - elif xf == volume.xf: - if fractalrange[0] < volume.xs or fractalrange[1] > grid.nx: - raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the lower coordinates of the fractal box or the domain in the x direction') - requestedsurface = 'xplus' - - elif ys == yf: - if xs == xf or zs == zf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if ys != volume.ys and ys != volume.yf: - raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') - fractalrange = (round_value(limits[0] / grid.dy), round_value(limits[1] / grid.dy)) - # yminus surface - if ys == volume.ys: - if fractalrange[0] < 0 or fractalrange[1] > volume.yf: - raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the upper coordinates of the fractal box or the domain in the y direction') - requestedsurface = 'yminus' - # yplus surface - elif yf == volume.yf: - if fractalrange[0] < volume.ys or fractalrange[1] > grid.ny: - raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the lower coordinates of the fractal box or the domain in the y direction') - requestedsurface = 'yplus' - - elif zs == zf: - if xs == xf or ys == yf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if zs != volume.zs and zs != volume.zf: - raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') - fractalrange = (round_value(limits[0] / grid.dz), round_value(limits[1] / grid.dz)) - # zminus surface - if zs == volume.zs: - if fractalrange[0] < 0 or fractalrange[1] > volume.zf: - raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the upper coordinates of the fractal box or the domain in the x direction') - requestedsurface = 'zminus' - # zplus surface - elif zf == volume.zf: - if fractalrange[0] < volume.zs or fractalrange[1] > grid.nz: - raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the lower coordinates of the fractal box or the domain in the z direction') - requestedsurface = 'zplus' - - else: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - - surface = FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim) - surface.surfaceID = requestedsurface - surface.fractalrange = fractalrange - surface.operatingonID = volume.ID - surface.seed = seed - surface.weighting = weighting - - # 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)) - - 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)) +"""Class for surface roughness command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..fractals import FractalSurface +from ..utilities import round_value +import gprMax.config as config + +from tqdm import tqdm +import numpy as np + + +class AddSurfaceRoughness(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 10 + self.hash = '#add_surface_roughness' + + def create(self, grid, uip): + + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + frac_dim = self.kwargs['frac_dim'] + weighting = np.array(self.kwargs['weighting'], dtype=np.float64) + limits = np.array(self.kwargs['limits']) + fractal_box_id = self.kwargs['fractal_box_id'] + except KeyError: + raise CmdInputError(self.__str__() + ' Incorrect parameters') + + try: + seed = self.kwargs['seed'] + except KeyError: + seed = None + + # grab 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)) + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + if frac_dim < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal dimension') + if weighting[0] < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the first direction of the surface') + if weighting[1] < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the second direction of the surface') + + # Check for valid orientations + if xs == xf: + if ys == yf or zs == zf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if xs != volume.xs and xs != volume.xf: + raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') + fractalrange = (round_value(limits[0] / grid.dx), round_value(limits[1] / grid.dx)) + # xminus surface + if xs == volume.xs: + if fractalrange[0] < 0 or fractalrange[1] > volume.xf: + raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the upper coordinates of the fractal box or the domain in the x direction') + requestedsurface = 'xminus' + # xplus surface + elif xf == volume.xf: + if fractalrange[0] < volume.xs or fractalrange[1] > grid.nx: + raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the lower coordinates of the fractal box or the domain in the x direction') + requestedsurface = 'xplus' + + elif ys == yf: + if xs == xf or zs == zf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if ys != volume.ys and ys != volume.yf: + raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') + fractalrange = (round_value(limits[0] / grid.dy), round_value(limits[1] / grid.dy)) + # yminus surface + if ys == volume.ys: + if fractalrange[0] < 0 or fractalrange[1] > volume.yf: + raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the upper coordinates of the fractal box or the domain in the y direction') + requestedsurface = 'yminus' + # yplus surface + elif yf == volume.yf: + if fractalrange[0] < volume.ys or fractalrange[1] > grid.ny: + raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the lower coordinates of the fractal box or the domain in the y direction') + requestedsurface = 'yplus' + + elif zs == zf: + if xs == xf or ys == yf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if zs != volume.zs and zs != volume.zf: + raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') + fractalrange = (round_value(limits[0] / grid.dz), round_value(limits[1] / grid.dz)) + # zminus surface + if zs == volume.zs: + if fractalrange[0] < 0 or fractalrange[1] > volume.zf: + raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the upper coordinates of the fractal box or the domain in the x direction') + requestedsurface = 'zminus' + # zplus surface + elif zf == volume.zf: + if fractalrange[0] < volume.zs or fractalrange[1] > grid.nz: + raise CmdInputError(self.__str__() + ' cannot apply fractal surface to fractal box as it would exceed either the lower coordinates of the fractal box or the domain in the z direction') + requestedsurface = 'zplus' + + else: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + + surface = FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim) + surface.surfaceID = requestedsurface + surface.fractalrange = fractalrange + surface.operatingonID = volume.ID + surface.seed = seed + surface.weighting = weighting + + # 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)) + + 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)) diff --git a/gprMax/cmds_geometry/add_surface_water.py b/gprMax/cmds_geometry/add_surface_water.py index b954e2bd..57b8de00 100644 --- a/gprMax/cmds_geometry/add_surface_water.py +++ b/gprMax/cmds_geometry/add_surface_water.py @@ -1,118 +1,118 @@ -"""Class for surface water command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..utilities import round_value -from ..materials import Material - -from tqdm import tqdm - - -class AddSurfaceWater(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 11 - self.hash = '#add_surface_water' - - def create(self, grid, uip): - """"Create surface water on fractal box.""" - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - fractal_box_id = self.kwargs['fractal_box_id'] - depth = self.kwargs['depth'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires exactly eight parameters') - - # grab 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)) - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - if depth <= 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the depth of water') - - # Check for valid orientations - if xs == xf: - if ys == yf or zs == zf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if xs != volume.xs and xs != volume.xf: - raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') - # xminus surface - if xs == volume.xs: - requestedsurface = 'xminus' - # xplus surface - elif xf == volume.xf: - requestedsurface = 'xplus' - filldepthcells = round_value(depth / grid.dx) - filldepth = filldepthcells * grid.dx - - elif ys == yf: - if xs == xf or zs == zf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if ys != volume.ys and ys != volume.yf: - raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') - # yminus surface - if ys == volume.ys: - requestedsurface = 'yminus' - # yplus surface - elif yf == volume.yf: - requestedsurface = 'yplus' - filldepthcells = round_value(depth / grid.dy) - filldepth = filldepthcells * grid.dy - - elif zs == zf: - if xs == xf or ys == yf: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - if zs != volume.zs and zs != volume.zf: - raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') - # zminus surface - if zs == volume.zs: - requestedsurface = 'zminus' - # zplus surface - elif zf == volume.zf: - requestedsurface = 'zplus' - filldepthcells = round_value(depth / grid.dz) - filldepth = filldepthcells * grid.dz - - else: - raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') - - surface = next((x for x in volume.fractalsurfaces if x.surfaceID == requestedsurface), None) - if not surface: - raise CmdInputError(self.__str__() + ' specified surface {} does not have a rough surface applied'.format(requestedsurface)) - - surface.filldepth = filldepthcells - - # Check that requested fill depth falls within range of surface roughness - if surface.filldepth < surface.fractalrange[0] or surface.filldepth > surface.fractalrange[1]: - raise CmdInputError(self.__str__() + ' requires a value for the depth of water that lies with the range of the requested surface roughness') - - # Check to see if water has been already defined as a material - if not any(x.ID == 'water' for x in grid.materials): - m = Material(len(grid.materials), 'water') - m.averagable = False - m.type = 'builtin, debye' - m.er = Material.watereri - m.deltaer.append(Material.waterdeltaer) - m.tau.append(Material.watertau) - grid.materials.append(m) - if Material.maxpoles == 0: - Material.maxpoles = 1 - - # Check if time step for model is suitable for using water - water = next((x for x in grid.materials if x.ID == 'water')) - testwater = next((x for x in water.tau if x < grid.dt), None) - if testwater: - 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)) +"""Class for surface water command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..utilities import round_value +from ..materials import Material + +from tqdm import tqdm + + +class AddSurfaceWater(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 11 + self.hash = '#add_surface_water' + + def create(self, grid, uip): + """"Create surface water on fractal box.""" + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + fractal_box_id = self.kwargs['fractal_box_id'] + depth = self.kwargs['depth'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires exactly eight parameters') + + # grab 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)) + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + if depth <= 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the depth of water') + + # Check for valid orientations + if xs == xf: + if ys == yf or zs == zf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if xs != volume.xs and xs != volume.xf: + raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') + # xminus surface + if xs == volume.xs: + requestedsurface = 'xminus' + # xplus surface + elif xf == volume.xf: + requestedsurface = 'xplus' + filldepthcells = round_value(depth / grid.dx) + filldepth = filldepthcells * grid.dx + + elif ys == yf: + if xs == xf or zs == zf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if ys != volume.ys and ys != volume.yf: + raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') + # yminus surface + if ys == volume.ys: + requestedsurface = 'yminus' + # yplus surface + elif yf == volume.yf: + requestedsurface = 'yplus' + filldepthcells = round_value(depth / grid.dy) + filldepth = filldepthcells * grid.dy + + elif zs == zf: + if xs == xf or ys == yf: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + if zs != volume.zs and zs != volume.zf: + raise CmdInputError(self.__str__() + ' can only be used on the external surfaces of a fractal box') + # zminus surface + if zs == volume.zs: + requestedsurface = 'zminus' + # zplus surface + elif zf == volume.zf: + requestedsurface = 'zplus' + filldepthcells = round_value(depth / grid.dz) + filldepth = filldepthcells * grid.dz + + else: + raise CmdInputError(self.__str__() + ' dimensions are not specified correctly') + + surface = next((x for x in volume.fractalsurfaces if x.surfaceID == requestedsurface), None) + if not surface: + raise CmdInputError(self.__str__() + ' specified surface {} does not have a rough surface applied'.format(requestedsurface)) + + surface.filldepth = filldepthcells + + # Check that requested fill depth falls within range of surface roughness + if surface.filldepth < surface.fractalrange[0] or surface.filldepth > surface.fractalrange[1]: + raise CmdInputError(self.__str__() + ' requires a value for the depth of water that lies with the range of the requested surface roughness') + + # Check to see if water has been already defined as a material + if not any(x.ID == 'water' for x in grid.materials): + m = Material(len(grid.materials), 'water') + m.averagable = False + m.type = 'builtin, debye' + m.er = Material.watereri + m.deltaer.append(Material.waterdeltaer) + m.tau.append(Material.watertau) + grid.materials.append(m) + if Material.maxpoles == 0: + Material.maxpoles = 1 + + # Check if time step for model is suitable for using water + water = next((x for x in grid.materials if x.ID == 'water')) + testwater = next((x for x in water.tau if x < grid.dt), None) + if testwater: + 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)) diff --git a/gprMax/cmds_geometry/box.py b/gprMax/cmds_geometry/box.py index 9fd674ed..c519cc8b 100644 --- a/gprMax/cmds_geometry/box.py +++ b/gprMax/cmds_geometry/box.py @@ -1,93 +1,93 @@ -"""Class for triangle command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..materials import Material -from ..cython.geometry_primitives import build_box -import gprMax.config as config - -from tqdm import tqdm -import numpy as np - - -class Box(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 5 - self.hash = '#box' - - def create(self, grid, uip): - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - - except KeyError: - raise CmdInputError(self.__str__() + ' Please specify two points.') - # check materials have been specified - # isotropic case - try: - materialsrequested = [self.kwargs['material_id']] - except KeyError: - # Anisotropic case - try: - materialsrequested = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' No materials have been specified') - - # check averaging - try: - # go with user specified averaging - averagebox = self.kwargs['averaging'] - except KeyError: - # if they havent specfied - go with the grid default - averagebox = grid.averagevolumeobjects - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - # Look up requested materials in existing list of material instances - materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] - - if len(materials) != len(materialsrequested): - notfound = [x for x in materialsrequested if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) - - # Isotropic case - if len(materials) == 1: - averaging = materials[0].averagable and averagebox - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - averaging = False - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID - averagedmaterial = [x for x in grid.materials if x.ID == requiredID] - if averagedmaterial: - numID = averagedmaterial.numID - else: - numID = len(grid.materials) - m = Material(numID, requiredID) - m.type = 'dielectric-smoothed' - # Create dielectric-smoothed constituents for material - m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) - m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) - m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - - # Append the new material object to the materials list - grid.materials.append(m) - - build_box(xs, xf, ys, yf, zs, zf, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - if averaging: - dielectricsmoothing = 'on' - else: - dielectricsmoothing = 'off' - tqdm.write('Box from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m of material(s) {} created, dielectric smoothing is {}.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, ', '.join(materialsrequested), dielectricsmoothing)) +"""Class for triangle command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..materials import Material +from ..cython.geometry_primitives import build_box +import gprMax.config as config + +from tqdm import tqdm +import numpy as np + + +class Box(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 5 + self.hash = '#box' + + def create(self, grid, uip): + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + + except KeyError: + raise CmdInputError(self.__str__() + ' Please specify two points.') + # check materials have been specified + # isotropic case + try: + materialsrequested = [self.kwargs['material_id']] + except KeyError: + # Anisotropic case + try: + materialsrequested = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' No materials have been specified') + + # check averaging + try: + # go with user specified averaging + averagebox = self.kwargs['averaging'] + except KeyError: + # if they havent specfied - go with the grid default + averagebox = grid.averagevolumeobjects + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + # Look up requested materials in existing list of material instances + materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] + + if len(materials) != len(materialsrequested): + notfound = [x for x in materialsrequested if x not in materials] + raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + + # Isotropic case + if len(materials) == 1: + averaging = materials[0].averagable and averagebox + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + averaging = False + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID + averagedmaterial = [x for x in grid.materials if x.ID == requiredID] + if averagedmaterial: + numID = averagedmaterial.numID + else: + numID = len(grid.materials) + m = Material(numID, requiredID) + m.type = 'dielectric-smoothed' + # Create dielectric-smoothed constituents for material + m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) + m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) + m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + + # Append the new material object to the materials list + grid.materials.append(m) + + build_box(xs, xf, ys, yf, zs, zf, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + if averaging: + dielectricsmoothing = 'on' + else: + dielectricsmoothing = 'off' + tqdm.write('Box from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m of material(s) {} created, dielectric smoothing is {}.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, ', '.join(materialsrequested), dielectricsmoothing)) diff --git a/gprMax/cmds_geometry/build_templates.py b/gprMax/cmds_geometry/build_templates.py index 214d7f00..54ea0547 100644 --- a/gprMax/cmds_geometry/build_templates.py +++ b/gprMax/cmds_geometry/build_templates.py @@ -1,52 +1,52 @@ -from jinja2 import Environment, PackageLoader, select_autoescape -env = Environment( - loader=PackageLoader(__name__, 'templates'), -) - -template = env.get_template('fields_updates_dispersive_template') - -r = template.render( - functions=[ - # name, double, real - { - 'name_a': 'update_electric_dispersive_multipole_A_double_real', - 'name_b': 'update_electric_dispersive_multipole_B_double_real', - 'name_a_1': 'update_electric_dispersive_1pole_A_double_real', - 'name_b_1': 'update_electric_dispersive_1pole_B_double_real', - 'field_type': 'double', - 'dispersive_type': 'double' - }, - # name, float, real - { - 'name_a': 'update_electric_dispersive_multipole_A_float_real', - 'name_b': 'update_electric_dispersive_multipole_B_float_real', - 'name_a_1': 'update_electric_dispersive_1pole_A_float_real', - 'name_b_1': 'update_electric_dispersive_1pole_B_float_real', - 'field_type': 'float', - 'dispersive_type': 'float' - }, - # name, double, complex - { - 'name_a': 'update_electric_dispersive_multipole_A_double_complex', - 'name_b': 'update_electric_dispersive_multipole_B_double_complex', - 'name_a_1': 'update_electric_dispersive_1pole_A_double_complex', - 'name_b_1': 'update_electric_dispersive_1pole_B_double_complex', - 'field_type': 'double', - 'dispersive_type': 'double complex', - 'real_part': 'creal' - }, - # name, float, complex - { - 'name_a': 'update_electric_dispersive_multipole_A_float_complex', - 'name_b': 'update_electric_dispersive_multipole_B_float_complex', - 'name_a_1': 'update_electric_dispersive_1pole_A_float_complex', - 'name_b_1': 'update_electric_dispersive_1pole_B_float_complex', - 'field_type': 'float', - 'dispersive_type': 'float complex', - 'real_part': 'crealf' - }] -) - -f = open('cython/dispersive_updates_test.pyx', 'w') -f.write(r) -f.close() +from jinja2 import Environment, PackageLoader, select_autoescape +env = Environment( + loader=PackageLoader(__name__, 'templates'), +) + +template = env.get_template('fields_updates_dispersive_template') + +r = template.render( + functions=[ + # name, double, real + { + 'name_a': 'update_electric_dispersive_multipole_A_double_real', + 'name_b': 'update_electric_dispersive_multipole_B_double_real', + 'name_a_1': 'update_electric_dispersive_1pole_A_double_real', + 'name_b_1': 'update_electric_dispersive_1pole_B_double_real', + 'field_type': 'double', + 'dispersive_type': 'double' + }, + # name, float, real + { + 'name_a': 'update_electric_dispersive_multipole_A_float_real', + 'name_b': 'update_electric_dispersive_multipole_B_float_real', + 'name_a_1': 'update_electric_dispersive_1pole_A_float_real', + 'name_b_1': 'update_electric_dispersive_1pole_B_float_real', + 'field_type': 'float', + 'dispersive_type': 'float' + }, + # name, double, complex + { + 'name_a': 'update_electric_dispersive_multipole_A_double_complex', + 'name_b': 'update_electric_dispersive_multipole_B_double_complex', + 'name_a_1': 'update_electric_dispersive_1pole_A_double_complex', + 'name_b_1': 'update_electric_dispersive_1pole_B_double_complex', + 'field_type': 'double', + 'dispersive_type': 'double complex', + 'real_part': 'creal' + }, + # name, float, complex + { + 'name_a': 'update_electric_dispersive_multipole_A_float_complex', + 'name_b': 'update_electric_dispersive_multipole_B_float_complex', + 'name_a_1': 'update_electric_dispersive_1pole_A_float_complex', + 'name_b_1': 'update_electric_dispersive_1pole_B_float_complex', + 'field_type': 'float', + 'dispersive_type': 'float complex', + 'real_part': 'crealf' + }] +) + +f = open('cython/dispersive_updates_test.pyx', 'w') +f.write(r) +f.close() diff --git a/gprMax/cmds_geometry/cylinder.py b/gprMax/cmds_geometry/cylinder.py index ec1ea061..18936210 100644 --- a/gprMax/cmds_geometry/cylinder.py +++ b/gprMax/cmds_geometry/cylinder.py @@ -1,98 +1,98 @@ -"""Class for cylinder command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..materials import Material -from ..cython.geometry_primitives import build_cylinder - -from tqdm import tqdm -import numpy as np -import gprMax.config as config - - -class Cylinder(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 6 - self.hash = '#cylinder' - - def create(self, grid, uip): - - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - r = self.kwargs['r'] - - except KeyError: - raise CmdInputError(self.__str__() + ' Please specify 2 points and a radius') - - # check averaging - try: - # go with user specified averaging - averagecylinder = self.kwargs['averaging'] - except KeyError: - # if they havent specfied - go with the grid default - averagecylinder = grid.averagevolumeobjects - - # check materials have been specified - # isotropic case - try: - materialsrequested = [self.kwargs['material_id']] - except KeyError: - # Anisotropic case - try: - materialsrequested = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' No materials have been specified') - - x1, y1, z1 = uip.round_to_grid(p1) - x2, y2, z2 = uip.round_to_grid(p2) - - if r <= 0: - raise CmdInputError(self.__str__() + ' the radius {:g} should be a positive value.'.format(r)) - - # Look up requested materials in existing list of material instances - materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] - - if len(materials) != len(materialsrequested): - notfound = [x for x in materialsrequested if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) - - # Isotropic case - if len(materials) == 1: - averaging = materials[0].averagable and averagecylinder - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - averaging = False - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID - averagedmaterial = [x for x in grid.materials if x.ID == requiredID] - if averagedmaterial: - numID = averagedmaterial.numID - else: - numID = len(grid.materials) - m = Material(numID, requiredID) - m.type = 'dielectric-smoothed' - # Create dielectric-smoothed constituents for material - m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) - m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) - m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - - # Append the new material object to the materials list - grid.materials.append(m) - - build_cylinder(x1, y1, z1, x2, y2, z2, r, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - if averaging: - dielectricsmoothing = 'on' - else: - dielectricsmoothing = 'off' - tqdm.write('Cylinder with face centres {:g}m, {:g}m, {:g}m and {:g}m, {:g}m, {:g}m, with radius {:g}m, of material(s) {} created, dielectric smoothing is {}.'.format(x1, y1, z1, x2, y2, z2, r, ', '.join(materialsrequested), dielectricsmoothing)) +"""Class for cylinder command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..materials import Material +from ..cython.geometry_primitives import build_cylinder + +from tqdm import tqdm +import numpy as np +import gprMax.config as config + + +class Cylinder(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 6 + self.hash = '#cylinder' + + def create(self, grid, uip): + + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + r = self.kwargs['r'] + + except KeyError: + raise CmdInputError(self.__str__() + ' Please specify 2 points and a radius') + + # check averaging + try: + # go with user specified averaging + averagecylinder = self.kwargs['averaging'] + except KeyError: + # if they havent specfied - go with the grid default + averagecylinder = grid.averagevolumeobjects + + # check materials have been specified + # isotropic case + try: + materialsrequested = [self.kwargs['material_id']] + except KeyError: + # Anisotropic case + try: + materialsrequested = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' No materials have been specified') + + x1, y1, z1 = uip.round_to_grid(p1) + x2, y2, z2 = uip.round_to_grid(p2) + + if r <= 0: + raise CmdInputError(self.__str__() + ' the radius {:g} should be a positive value.'.format(r)) + + # Look up requested materials in existing list of material instances + materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] + + if len(materials) != len(materialsrequested): + notfound = [x for x in materialsrequested if x not in materials] + raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + + # Isotropic case + if len(materials) == 1: + averaging = materials[0].averagable and averagecylinder + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + averaging = False + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID + averagedmaterial = [x for x in grid.materials if x.ID == requiredID] + if averagedmaterial: + numID = averagedmaterial.numID + else: + numID = len(grid.materials) + m = Material(numID, requiredID) + m.type = 'dielectric-smoothed' + # Create dielectric-smoothed constituents for material + m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) + m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) + m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + + # Append the new material object to the materials list + grid.materials.append(m) + + build_cylinder(x1, y1, z1, x2, y2, z2, r, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + if averaging: + dielectricsmoothing = 'on' + else: + dielectricsmoothing = 'off' + tqdm.write('Cylinder with face centres {:g}m, {:g}m, {:g}m and {:g}m, {:g}m, {:g}m, with radius {:g}m, of material(s) {} created, dielectric smoothing is {}.'.format(x1, y1, z1, x2, y2, z2, r, ', '.join(materialsrequested), dielectricsmoothing)) diff --git a/gprMax/cmds_geometry/cylindrical_sector.py b/gprMax/cmds_geometry/cylindrical_sector.py index c572417c..082607cc 100644 --- a/gprMax/cmds_geometry/cylindrical_sector.py +++ b/gprMax/cmds_geometry/cylindrical_sector.py @@ -1,137 +1,137 @@ -"""Class for cylinder command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..materials import Material -from ..cython.geometry_primitives import build_cylindrical_sector - -from tqdm import tqdm -import numpy as np - - -class CylindricalSector(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 7 - self.hash = '#cylindrical_sector' - - def create(self, grid, uip): - - try: - normal = self.kwargs['normal'].lower() - ctr1 = self.kwargs['ctr1'] - ctr2 = self.kwargs['ctr2'] - extent1 = self.kwargs['extent1'] - extent2 = self.kwargs['extent2'] - start = self.kwargs['start'] - end = self.kwargs['end'] - r = self.kwargs['r'] - thickness = extent2 - extent1 - except KeyError: - raise CmdInputError(self.__str__()) - - # check averaging - try: - # go with user specified averaging - averagecylindricalsector = self.kwargs['averaging'] - except KeyError: - # if they havent specfied - go with the grid default - averagecylindricalsector = grid.averagevolumeobjects - - # check materials have been specified - # isotropic case - try: - materialsrequested = [self.kwargs['material_id']] - except KeyError: - # Anisotropic case - try: - materialsrequested = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' No materials have been specified') - - sectorstartangle = 2 * np.pi * (start / 360) - sectorangle = 2 * np.pi * (end / 360) - - if normal != 'x' and normal != 'y' and normal != 'z': - raise CmdInputError(self.__str__() + ' the normal direction must be either x, y or z.') - if r <= 0: - raise CmdInputError(self.__str__() + ' the radius {:g} should be a positive value.'.format(r)) - if sectorstartangle < 0 or sectorangle <= 0: - raise CmdInputError(self.__str__() + ' the starting angle and sector angle should be a positive values.') - if sectorstartangle >= 2 * np.pi or sectorangle >= 2 * np.pi: - raise CmdInputError(self.__str__() + ' the starting angle and sector angle must be less than 360 degrees.') - - # Look up requested materials in existing list of material instances - materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] - - if len(materials) != len(materialsrequested): - notfound = [x for x in materialsrequested if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) - - if thickness > 0: - # Isotropic case - if len(materials) == 1: - averaging = materials[0].averagable and averagecylindricalsector - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - averaging = False - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID - averagedmaterial = [x for x in grid.materials if x.ID == requiredID] - if averagedmaterial: - numID = averagedmaterial.numID - else: - numID = len(grid.materials) - m = Material(numID, requiredID) - m.type = 'dielectric-smoothed' - # Create dielectric-smoothed constituents for material - m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) - m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) - m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - - # Append the new material object to the materials list - grid.materials.append(m) - else: - averaging = False - # Isotropic case - if len(materials) == 1: - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - # numID requires a value but it will not be used - numID = None - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - - # yz-plane cylindrical sector - if normal == 'x': - level, ctr1, ctr2 = uip.round_to_grid((extent1, ctr1, ctr2)) - - # xz-plane cylindrical sector - elif normal == 'y': - ctr1, level, ctr2 = uip.round_to_grid((ctr1, extent1, ctr2)) - - # xy-plane cylindrical sector - elif normal == 'z': - ctr1, ctr2, level = uip.round_to_grid((ctr1, ctr2, extent1)) - - build_cylindrical_sector(ctr1, ctr2, level, sectorstartangle, sectorangle, r, normal, thickness, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - if thickness > 0: - if averaging: - dielectricsmoothing = 'on' - else: - dielectricsmoothing = 'off' - tqdm.write('Cylindrical sector with centre {:g}m, {:g}m, radius {:g}m, starting angle {:.1f} degrees, sector angle {:.1f} degrees, thickness {:g}m, of material(s) {} created, dielectric smoothing is {}.'.format(ctr1, ctr2, r, (sectorstartangle / (2 * np.pi)) * 360, (sectorangle / (2 * np.pi)) * 360, thickness, ', '.join(materialsrequested), dielectricsmoothing)) - else: - tqdm.write('Cylindrical sector with centre {:g}m, {:g}m, radius {:g}m, starting angle {:.1f} degrees, sector angle {:.1f} degrees, of material(s) {} created.'.format(ctr1, ctr2, r, (sectorstartangle / (2 * np.pi)) * 360, (sectorangle / (2 * np.pi)) * 360, ', '.join(materialsrequested))) +"""Class for cylinder command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..materials import Material +from ..cython.geometry_primitives import build_cylindrical_sector + +from tqdm import tqdm +import numpy as np + + +class CylindricalSector(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 7 + self.hash = '#cylindrical_sector' + + def create(self, grid, uip): + + try: + normal = self.kwargs['normal'].lower() + ctr1 = self.kwargs['ctr1'] + ctr2 = self.kwargs['ctr2'] + extent1 = self.kwargs['extent1'] + extent2 = self.kwargs['extent2'] + start = self.kwargs['start'] + end = self.kwargs['end'] + r = self.kwargs['r'] + thickness = extent2 - extent1 + except KeyError: + raise CmdInputError(self.__str__()) + + # check averaging + try: + # go with user specified averaging + averagecylindricalsector = self.kwargs['averaging'] + except KeyError: + # if they havent specfied - go with the grid default + averagecylindricalsector = grid.averagevolumeobjects + + # check materials have been specified + # isotropic case + try: + materialsrequested = [self.kwargs['material_id']] + except KeyError: + # Anisotropic case + try: + materialsrequested = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' No materials have been specified') + + sectorstartangle = 2 * np.pi * (start / 360) + sectorangle = 2 * np.pi * (end / 360) + + if normal != 'x' and normal != 'y' and normal != 'z': + raise CmdInputError(self.__str__() + ' the normal direction must be either x, y or z.') + if r <= 0: + raise CmdInputError(self.__str__() + ' the radius {:g} should be a positive value.'.format(r)) + if sectorstartangle < 0 or sectorangle <= 0: + raise CmdInputError(self.__str__() + ' the starting angle and sector angle should be a positive values.') + if sectorstartangle >= 2 * np.pi or sectorangle >= 2 * np.pi: + raise CmdInputError(self.__str__() + ' the starting angle and sector angle must be less than 360 degrees.') + + # Look up requested materials in existing list of material instances + materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] + + if len(materials) != len(materialsrequested): + notfound = [x for x in materialsrequested if x not in materials] + raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + + if thickness > 0: + # Isotropic case + if len(materials) == 1: + averaging = materials[0].averagable and averagecylindricalsector + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + averaging = False + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID + averagedmaterial = [x for x in grid.materials if x.ID == requiredID] + if averagedmaterial: + numID = averagedmaterial.numID + else: + numID = len(grid.materials) + m = Material(numID, requiredID) + m.type = 'dielectric-smoothed' + # Create dielectric-smoothed constituents for material + m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) + m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) + m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + + # Append the new material object to the materials list + grid.materials.append(m) + else: + averaging = False + # Isotropic case + if len(materials) == 1: + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + # numID requires a value but it will not be used + numID = None + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + + # yz-plane cylindrical sector + if normal == 'x': + level, ctr1, ctr2 = uip.round_to_grid((extent1, ctr1, ctr2)) + + # xz-plane cylindrical sector + elif normal == 'y': + ctr1, level, ctr2 = uip.round_to_grid((ctr1, extent1, ctr2)) + + # xy-plane cylindrical sector + elif normal == 'z': + ctr1, ctr2, level = uip.round_to_grid((ctr1, ctr2, extent1)) + + build_cylindrical_sector(ctr1, ctr2, level, sectorstartangle, sectorangle, r, normal, thickness, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + if thickness > 0: + if averaging: + dielectricsmoothing = 'on' + else: + dielectricsmoothing = 'off' + tqdm.write('Cylindrical sector with centre {:g}m, {:g}m, radius {:g}m, starting angle {:.1f} degrees, sector angle {:.1f} degrees, thickness {:g}m, of material(s) {} created, dielectric smoothing is {}.'.format(ctr1, ctr2, r, (sectorstartangle / (2 * np.pi)) * 360, (sectorangle / (2 * np.pi)) * 360, thickness, ', '.join(materialsrequested), dielectricsmoothing)) + else: + tqdm.write('Cylindrical sector with centre {:g}m, {:g}m, radius {:g}m, starting angle {:.1f} degrees, sector angle {:.1f} degrees, of material(s) {} created.'.format(ctr1, ctr2, r, (sectorstartangle / (2 * np.pi)) * 360, (sectorangle / (2 * np.pi)) * 360, ', '.join(materialsrequested))) diff --git a/gprMax/cmds_geometry/edge.py b/gprMax/cmds_geometry/edge.py index 1e4ff282..691be9cf 100644 --- a/gprMax/cmds_geometry/edge.py +++ b/gprMax/cmds_geometry/edge.py @@ -1,64 +1,64 @@ -"""Class for edge command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..cython.geometry_primitives import build_edge_x -from ..cython.geometry_primitives import build_edge_y -from ..cython.geometry_primitives import build_edge_z - -from tqdm import tqdm - - -class Edge(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 2 - self.hash = '#edge' - - def create(self, grid, uip): - """Create edge and add it to the grid.""" - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - material_id = self.kwargs['material_id'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires exactly 3 parameters') - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - material = next((x for x in grid.materials if x.ID == material_id), None) - - if not material: - raise CmdInputError('Material with ID {} does not exist'.format(material_id)) - - # Check for valid orientations - # x-orientated wire - if xs != xf: - if ys != yf or zs != zf: - raise CmdInputError(self.__str__() + ' the edge is not specified correctly') - else: - for i in range(xs, xf): - build_edge_x(i, ys, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID) - - # y-orientated wire - elif ys != yf: - if xs != xf or zs != zf: - raise CmdInputError(self.__str__() + ' the edge is not specified correctly') - else: - for j in range(ys, yf): - build_edge_y(xs, j, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID) - - # z-orientated wire - elif zs != zf: - if xs != xf or ys != yf: - raise CmdInputError(self.__str__() + ' the edge is not specified correctly') - else: - for k in range(zs, zf): - build_edge_z(xs, ys, k, material.numID, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - tqdm.write('Edge from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m of material {} created.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, material_id)) +"""Class for edge command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..cython.geometry_primitives import build_edge_x +from ..cython.geometry_primitives import build_edge_y +from ..cython.geometry_primitives import build_edge_z + +from tqdm import tqdm + + +class Edge(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 2 + self.hash = '#edge' + + def create(self, grid, uip): + """Create edge and add it to the grid.""" + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + material_id = self.kwargs['material_id'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires exactly 3 parameters') + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + material = next((x for x in grid.materials if x.ID == material_id), None) + + if not material: + raise CmdInputError('Material with ID {} does not exist'.format(material_id)) + + # Check for valid orientations + # x-orientated wire + if xs != xf: + if ys != yf or zs != zf: + raise CmdInputError(self.__str__() + ' the edge is not specified correctly') + else: + for i in range(xs, xf): + build_edge_x(i, ys, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID) + + # y-orientated wire + elif ys != yf: + if xs != xf or zs != zf: + raise CmdInputError(self.__str__() + ' the edge is not specified correctly') + else: + for j in range(ys, yf): + build_edge_y(xs, j, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID) + + # z-orientated wire + elif zs != zf: + if xs != xf or ys != yf: + raise CmdInputError(self.__str__() + ' the edge is not specified correctly') + else: + for k in range(zs, zf): + build_edge_z(xs, ys, k, material.numID, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + tqdm.write('Edge from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m of material {} created.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, material_id)) diff --git a/gprMax/cmds_geometry/fractal_box.py b/gprMax/cmds_geometry/fractal_box.py index 50ec3b63..c792bb51 100644 --- a/gprMax/cmds_geometry/fractal_box.py +++ b/gprMax/cmds_geometry/fractal_box.py @@ -1,93 +1,93 @@ -"""Class for surface roughness command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..fractals import FractalVolume -import gprMax.config as config - -from tqdm import tqdm -import numpy as np - - -class FractalBox(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 9 - self.hash = '#fractal_box' - - def create(self, grid, uip): - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - frac_dim = self.kwargs['frac_dim'] - weighting = np.array(self.kwargs['weighting']) - n_materials = self.kwargs['n_materials'] - mixing_model_id = self.kwargs['mixing_model_id'] - ID = self.kwargs['id'] - - except KeyError: - raise CmdInputError(self.__str__() + ' Incorrect parameters') - - try: - seed = self.kwargs['seed'] - except KeyError: - seed = None - - # Default is no dielectric smoothing for a fractal box - averagefractalbox = False - - # check averaging - try: - # go with user specified averaging - averagefractalbox = self.kwargs['averaging'] - except KeyError: - # if they havent specfied - go with the grid default - averagefractalbox = False - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - if frac_dim < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal dimension') - if weighting[0] < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the x direction') - if weighting[1] < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the y direction') - if weighting[2] < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the z direction') - if n_materials < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for the number of bins') - - # Find materials to use to build fractal volume, either from mixing models or normal materials - mixingmodel = next((x for x in grid.mixingmodels if x.ID == mixing_model_id), None) - material = next((x for x in grid.materials if x.ID == mixing_model_id), None) - nbins = n_materials - - if mixingmodel: - if nbins == 1: - raise CmdInputError(self.__str__() + ' must be used with more than one material from the mixing model.') - # Create materials from mixing model as number of bins now known from fractal_box command - mixingmodel.calculate_debye_properties(nbins, grid) - elif not material: - raise CmdInputError(self.__str__() + ' mixing model or material with ID {} does not exist'.format(mixing_model_id)) - - volume = FractalVolume(xs, xf, ys, yf, zs, zf, frac_dim) - volume.ID = ID - volume.operatingonID = mixing_model_id - volume.nbins = nbins - volume.seed = seed - volume.weighting = weighting - volume.averaging = averagefractalbox - volume.mixingmodel = mixingmodel - - if config.is_messages(): - if volume.averaging: - dielectricsmoothing = 'on' - else: - dielectricsmoothing = 'off' - tqdm.write('Fractal box {} from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m with {}, fractal dimension {:g}, fractal weightings {:g}, {:g}, {:g}, fractal seeding {}, with {} material(s) created, dielectric smoothing is {}.'.format(volume.ID, xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, volume.operatingonID, volume.dimension, volume.weighting[0], volume.weighting[1], volume.weighting[2], volume.seed, volume.nbins, dielectricsmoothing)) - - grid.fractalvolumes.append(volume) +"""Class for surface roughness command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..fractals import FractalVolume +import gprMax.config as config + +from tqdm import tqdm +import numpy as np + + +class FractalBox(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 9 + self.hash = '#fractal_box' + + def create(self, grid, uip): + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + frac_dim = self.kwargs['frac_dim'] + weighting = np.array(self.kwargs['weighting']) + n_materials = self.kwargs['n_materials'] + mixing_model_id = self.kwargs['mixing_model_id'] + ID = self.kwargs['id'] + + except KeyError: + raise CmdInputError(self.__str__() + ' Incorrect parameters') + + try: + seed = self.kwargs['seed'] + except KeyError: + seed = None + + # Default is no dielectric smoothing for a fractal box + averagefractalbox = False + + # check averaging + try: + # go with user specified averaging + averagefractalbox = self.kwargs['averaging'] + except KeyError: + # if they havent specfied - go with the grid default + averagefractalbox = False + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + if frac_dim < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal dimension') + if weighting[0] < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the x direction') + if weighting[1] < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the y direction') + if weighting[2] < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the fractal weighting in the z direction') + if n_materials < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for the number of bins') + + # Find materials to use to build fractal volume, either from mixing models or normal materials + mixingmodel = next((x for x in grid.mixingmodels if x.ID == mixing_model_id), None) + material = next((x for x in grid.materials if x.ID == mixing_model_id), None) + nbins = n_materials + + if mixingmodel: + if nbins == 1: + raise CmdInputError(self.__str__() + ' must be used with more than one material from the mixing model.') + # Create materials from mixing model as number of bins now known from fractal_box command + mixingmodel.calculate_debye_properties(nbins, grid) + elif not material: + raise CmdInputError(self.__str__() + ' mixing model or material with ID {} does not exist'.format(mixing_model_id)) + + volume = FractalVolume(xs, xf, ys, yf, zs, zf, frac_dim) + volume.ID = ID + volume.operatingonID = mixing_model_id + volume.nbins = nbins + volume.seed = seed + volume.weighting = weighting + volume.averaging = averagefractalbox + volume.mixingmodel = mixingmodel + + if config.is_messages(): + if volume.averaging: + dielectricsmoothing = 'on' + else: + dielectricsmoothing = 'off' + tqdm.write('Fractal box {} from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m with {}, fractal dimension {:g}, fractal weightings {:g}, {:g}, {:g}, fractal seeding {}, with {} material(s) created, dielectric smoothing is {}.'.format(volume.ID, xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, volume.operatingonID, volume.dimension, volume.weighting[0], volume.weighting[1], volume.weighting[2], volume.seed, volume.nbins, dielectricsmoothing)) + + grid.fractalvolumes.append(volume) diff --git a/gprMax/cmds_geometry/plate.py b/gprMax/cmds_geometry/plate.py index 74ebaed0..68a9b270 100644 --- a/gprMax/cmds_geometry/plate.py +++ b/gprMax/cmds_geometry/plate.py @@ -1,112 +1,112 @@ -"""Class for edge command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..cython.geometry_primitives import build_face_yz -from ..cython.geometry_primitives import build_face_xz -from ..cython.geometry_primitives import build_face_xy - -from tqdm import tqdm - - -class Plate(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 3 - self.hash = '#plate' - - def create(self, grid, uip): - - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - - except KeyError: - raise CmdInputError(self.__str__() + ' 2 points must be specified') - - # isotropic - try: - materialsrequested = [self.kwargs['material_id']] - except KeyError: - # Anisotropic case - try: - materialsrequested = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' No materials have been specified') - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - # Check for valid orientations - if xs == xf: - if ys == yf or zs == zf: - raise CmdInputError(self.__str__() + ' the plate is not specified correctly') - - elif ys == yf: - if xs == xf or zs == zf: - raise CmdInputError(self.__str__() + ' the plate is not specified correctly') - - elif zs == zf: - if xs == xf or ys == yf: - raise CmdInputError(self.__str__() + ' the plate is not specified correctly') - - else: - raise CmdInputError(self.__str__() + ' the plate is not specified correctly') - - # Look up requested materials in existing list of material instances - materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] - - if len(materials) != len(materialsrequested): - notfound = [x for x in materialsrequested if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) - - # yz-plane plate - if xs == xf: - # Isotropic case - if len(materials) == 1: - numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 2: - numIDy = materials[0].numID - numIDz = materials[1].numID - - for j in range(ys, yf): - for k in range(zs, zf): - build_face_yz(xs, j, k, numIDy, numIDz, grid.rigidE, grid.rigidH, grid.ID) - - # xz-plane plate - elif ys == yf: - # Isotropic case - if len(materials) == 1: - numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 2: - numIDx = materials[0].numID - numIDz = materials[1].numID - - for i in range(xs, xf): - for k in range(zs, zf): - build_face_xz(i, ys, k, numIDx, numIDz, grid.rigidE, grid.rigidH, grid.ID) - - # xy-plane plate - elif zs == zf: - # Isotropic case - if len(materials) == 1: - numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 2: - numIDx = materials[0].numID - numIDy = materials[1].numID - - for i in range(xs, xf): - for j in range(ys, yf): - build_face_xy(i, j, zs, numIDx, numIDy, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - tqdm.write('Plate from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m of material(s) {} created.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, ', '.join(materialsrequested))) +"""Class for edge command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..cython.geometry_primitives import build_face_yz +from ..cython.geometry_primitives import build_face_xz +from ..cython.geometry_primitives import build_face_xy + +from tqdm import tqdm + + +class Plate(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 3 + self.hash = '#plate' + + def create(self, grid, uip): + + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + + except KeyError: + raise CmdInputError(self.__str__() + ' 2 points must be specified') + + # isotropic + try: + materialsrequested = [self.kwargs['material_id']] + except KeyError: + # Anisotropic case + try: + materialsrequested = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' No materials have been specified') + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + # Check for valid orientations + if xs == xf: + if ys == yf or zs == zf: + raise CmdInputError(self.__str__() + ' the plate is not specified correctly') + + elif ys == yf: + if xs == xf or zs == zf: + raise CmdInputError(self.__str__() + ' the plate is not specified correctly') + + elif zs == zf: + if xs == xf or ys == yf: + raise CmdInputError(self.__str__() + ' the plate is not specified correctly') + + else: + raise CmdInputError(self.__str__() + ' the plate is not specified correctly') + + # Look up requested materials in existing list of material instances + materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] + + if len(materials) != len(materialsrequested): + notfound = [x for x in materialsrequested if x not in materials] + raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + + # yz-plane plate + if xs == xf: + # Isotropic case + if len(materials) == 1: + numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 2: + numIDy = materials[0].numID + numIDz = materials[1].numID + + for j in range(ys, yf): + for k in range(zs, zf): + build_face_yz(xs, j, k, numIDy, numIDz, grid.rigidE, grid.rigidH, grid.ID) + + # xz-plane plate + elif ys == yf: + # Isotropic case + if len(materials) == 1: + numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 2: + numIDx = materials[0].numID + numIDz = materials[1].numID + + for i in range(xs, xf): + for k in range(zs, zf): + build_face_xz(i, ys, k, numIDx, numIDz, grid.rigidE, grid.rigidH, grid.ID) + + # xy-plane plate + elif zs == zf: + # Isotropic case + if len(materials) == 1: + numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 2: + numIDx = materials[0].numID + numIDy = materials[1].numID + + for i in range(xs, xf): + for j in range(ys, yf): + build_face_xy(i, j, zs, numIDx, numIDy, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + tqdm.write('Plate from {:g}m, {:g}m, {:g}m, to {:g}m, {:g}m, {:g}m of material(s) {} created.'.format(xs * grid.dx, ys * grid.dy, zs * grid.dz, xf * grid.dx, yf * grid.dy, zf * grid.dz, ', '.join(materialsrequested))) diff --git a/gprMax/cmds_geometry/sphere.py b/gprMax/cmds_geometry/sphere.py index e38ea409..bea45f99 100644 --- a/gprMax/cmds_geometry/sphere.py +++ b/gprMax/cmds_geometry/sphere.py @@ -1,91 +1,91 @@ -"""Class for sphere command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..materials import Material -from ..cython.geometry_primitives import build_sphere - -from tqdm import tqdm -import numpy as np - - -class Sphere(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 8 - self.hash = '#sphere' - - def create(self, grid, uip): - try: - p1 = self.kwargs['p1'] - r = self.kwargs['r'] - except KeyError: - raise CmdInputError(self.__str__() + ' Please specify a point and a radius.') - - # check averaging - try: - # go with user specified averaging - averagesphere = self.kwargs['averaging'] - except KeyError: - # if they havent specfied - go with the grid default - averagesphere = grid.averagevolumeobjects - - # check materials have been specified - # isotropic case - try: - materialsrequested = [self.kwargs['material_id']] - except KeyError: - # Anisotropic case - try: - materialsrequested = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' No materials have been specified') - - # Centre of sphere - xc, yc, zc = uip.round_to_grid(p1) - - # Look up requested materials in existing list of material instances - materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] - - if len(materials) != len(materialsrequested): - notfound = [x for x in materialsrequested if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) - - # Isotropic case - if len(materials) == 1: - averaging = materials[0].averagable and averagesphere - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - averaging = False - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID - averagedmaterial = [x for x in grid.materials if x.ID == requiredID] - if averagedmaterial: - numID = averagedmaterial.numID - else: - numID = len(grid.materials) - m = Material(numID, requiredID) - m.type = 'dielectric-smoothed' - # Create dielectric-smoothed constituents for material - m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) - m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) - m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - - # Append the new material object to the materials list - grid.materials.append(m) - - build_sphere(xc, yc, zc, r, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - if averaging: - dielectricsmoothing = 'on' - else: - dielectricsmoothing = 'off' - tqdm.write('Sphere with centre {:g}m, {:g}m, {:g}m, radius {:g}m, of material(s) {} created, dielectric smoothing is {}.'.format(xc * grid.dx, yc * grid.dy, zc * grid.dz, r, ', '.join(materialsrequested), dielectricsmoothing)) +"""Class for sphere command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..materials import Material +from ..cython.geometry_primitives import build_sphere + +from tqdm import tqdm +import numpy as np + + +class Sphere(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 8 + self.hash = '#sphere' + + def create(self, grid, uip): + try: + p1 = self.kwargs['p1'] + r = self.kwargs['r'] + except KeyError: + raise CmdInputError(self.__str__() + ' Please specify a point and a radius.') + + # check averaging + try: + # go with user specified averaging + averagesphere = self.kwargs['averaging'] + except KeyError: + # if they havent specfied - go with the grid default + averagesphere = grid.averagevolumeobjects + + # check materials have been specified + # isotropic case + try: + materialsrequested = [self.kwargs['material_id']] + except KeyError: + # Anisotropic case + try: + materialsrequested = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' No materials have been specified') + + # Centre of sphere + xc, yc, zc = uip.round_to_grid(p1) + + # Look up requested materials in existing list of material instances + materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] + + if len(materials) != len(materialsrequested): + notfound = [x for x in materialsrequested if x not in materials] + raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + + # Isotropic case + if len(materials) == 1: + averaging = materials[0].averagable and averagesphere + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + averaging = False + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID + averagedmaterial = [x for x in grid.materials if x.ID == requiredID] + if averagedmaterial: + numID = averagedmaterial.numID + else: + numID = len(grid.materials) + m = Material(numID, requiredID) + m.type = 'dielectric-smoothed' + # Create dielectric-smoothed constituents for material + m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) + m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) + m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + + # Append the new material object to the materials list + grid.materials.append(m) + + build_sphere(xc, yc, zc, r, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + if averaging: + dielectricsmoothing = 'on' + else: + dielectricsmoothing = 'off' + tqdm.write('Sphere with centre {:g}m, {:g}m, {:g}m, radius {:g}m, of material(s) {} created, dielectric smoothing is {}.'.format(xc * grid.dx, yc * grid.dy, zc * grid.dz, r, ', '.join(materialsrequested), dielectricsmoothing)) diff --git a/gprMax/cmds_geometry/triangle.py b/gprMax/cmds_geometry/triangle.py index be2eadd9..89671aa1 100644 --- a/gprMax/cmds_geometry/triangle.py +++ b/gprMax/cmds_geometry/triangle.py @@ -1,132 +1,132 @@ -"""Class for triangle command.""" -from .cmds_geometry import UserObjectGeometry -from ..exceptions import CmdInputError -from ..materials import Material -from ..cython.geometry_primitives import build_triangle - -from tqdm import tqdm -import numpy as np - - -class Triangle(UserObjectGeometry): - """User class for edge command.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 4 - self.hash = '#triangle' - - def create(self, grid, uip): - - try: - up1 = self.kwargs['p1'] - up2 = self.kwargs['p2'] - up3 = self.kwargs['p3'] - thickness = self.kwargs['thickness'] - - except KeyError: - raise CmdInputError(self.params_str() + ' Specify 3 points and a thickness') - - # check averaging - try: - # go with user specified averaging - averagetriangularprism = self.kwargs['averaging'] - except KeyError: - # if they havent specfied - go with the grid default - averagetriangularprism = grid.averagevolumeobjects - - # check materials have been specified - # isotropic case - try: - materialsrequested = [self.kwargs['material_id']] - except KeyError: - # Anisotropic case - try: - materialsrequested = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' No materials have been specified') - - # Check whether points are valid against grid - uip.check_tri_points(up1, up2, up3, object) - # Convert points to metres - x1, y1, z1 = uip.round_to_grid(up1) - x2, y2, z2 = uip.round_to_grid(up2) - x3, y3, z3 = uip.round_to_grid(up3) - - if thickness < 0: - raise CmdInputError(self.__str__() + ' requires a positive value for thickness') - - # Check for valid orientations - # yz-plane triangle - if x1 == x2 and x2 == x3: - normal = 'x' - # xz-plane triangle - elif y1 == y2 and y2 == y3: - normal = 'y' - # xy-plane triangle - elif z1 == z2 and z2 == z3: - normal = 'z' - else: - raise CmdInputError(self.__str__() + ' the triangle is not specified correctly') - - # Look up requested materials in existing list of material instances - materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] - - if len(materials) != len(materialsrequested): - notfound = [x for x in materialsrequested if x not in materials] - raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) - - if thickness > 0: - # Isotropic case - if len(materials) == 1: - averaging = materials[0].averagable and averagetriangularprism - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - averaging = False - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID - averagedmaterial = [x for x in grid.materials if x.ID == requiredID] - if averagedmaterial: - numID = averagedmaterial.numID - else: - numID = len(grid.materials) - m = Material(numID, requiredID) - m.type = 'dielectric-smoothed' - # Create dielectric-smoothed constituents for material - m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) - m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) - m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) - - # Append the new material object to the materials list - grid.materials.append(m) - else: - averaging = False - # Isotropic case - if len(materials) == 1: - numID = numIDx = numIDy = numIDz = materials[0].numID - - # Uniaxial anisotropic case - elif len(materials) == 3: - # numID requires a value but it will not be used - numID = None - numIDx = materials[0].numID - numIDy = materials[1].numID - numIDz = materials[2].numID - - build_triangle(x1, y1, z1, x2, y2, z2, x3, y3, z3, normal, thickness, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) - - if config.is_messages(): - if thickness > 0: - if averaging: - dielectricsmoothing = 'on' - else: - dielectricsmoothing = 'off' - tqdm.write('Triangle with coordinates {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m and thickness {:g}m of material(s) {} created, dielectric smoothing is {}.'.format(x1, y1, z1, x2, y2, z2, x3, y3, z3, thickness, ', '.join(materialsrequested), dielectricsmoothing)) - else: - tqdm.write('Triangle with coordinates {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m of material(s) {} created.'.format(x1, y1, z1, x2, y2, z2, x3, y3, z3, ', '.join(materialsrequested))) +"""Class for triangle command.""" +from .cmds_geometry import UserObjectGeometry +from ..exceptions import CmdInputError +from ..materials import Material +from ..cython.geometry_primitives import build_triangle + +from tqdm import tqdm +import numpy as np + + +class Triangle(UserObjectGeometry): + """User class for edge command.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 4 + self.hash = '#triangle' + + def create(self, grid, uip): + + try: + up1 = self.kwargs['p1'] + up2 = self.kwargs['p2'] + up3 = self.kwargs['p3'] + thickness = self.kwargs['thickness'] + + except KeyError: + raise CmdInputError(self.params_str() + ' Specify 3 points and a thickness') + + # check averaging + try: + # go with user specified averaging + averagetriangularprism = self.kwargs['averaging'] + except KeyError: + # if they havent specfied - go with the grid default + averagetriangularprism = grid.averagevolumeobjects + + # check materials have been specified + # isotropic case + try: + materialsrequested = [self.kwargs['material_id']] + except KeyError: + # Anisotropic case + try: + materialsrequested = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' No materials have been specified') + + # Check whether points are valid against grid + uip.check_tri_points(up1, up2, up3, object) + # Convert points to metres + x1, y1, z1 = uip.round_to_grid(up1) + x2, y2, z2 = uip.round_to_grid(up2) + x3, y3, z3 = uip.round_to_grid(up3) + + if thickness < 0: + raise CmdInputError(self.__str__() + ' requires a positive value for thickness') + + # Check for valid orientations + # yz-plane triangle + if x1 == x2 and x2 == x3: + normal = 'x' + # xz-plane triangle + elif y1 == y2 and y2 == y3: + normal = 'y' + # xy-plane triangle + elif z1 == z2 and z2 == z3: + normal = 'z' + else: + raise CmdInputError(self.__str__() + ' the triangle is not specified correctly') + + # Look up requested materials in existing list of material instances + materials = [y for x in materialsrequested for y in grid.materials if y.ID == x] + + if len(materials) != len(materialsrequested): + notfound = [x for x in materialsrequested if x not in materials] + raise CmdInputError(self.__str__() + ' material(s) {} do not exist'.format(notfound)) + + if thickness > 0: + # Isotropic case + if len(materials) == 1: + averaging = materials[0].averagable and averagetriangularprism + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + averaging = False + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + requiredID = materials[0].ID + '+' + materials[1].ID + '+' + materials[2].ID + averagedmaterial = [x for x in grid.materials if x.ID == requiredID] + if averagedmaterial: + numID = averagedmaterial.numID + else: + numID = len(grid.materials) + m = Material(numID, requiredID) + m.type = 'dielectric-smoothed' + # Create dielectric-smoothed constituents for material + m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0) + m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0) + m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + m.sm = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0) + + # Append the new material object to the materials list + grid.materials.append(m) + else: + averaging = False + # Isotropic case + if len(materials) == 1: + numID = numIDx = numIDy = numIDz = materials[0].numID + + # Uniaxial anisotropic case + elif len(materials) == 3: + # numID requires a value but it will not be used + numID = None + numIDx = materials[0].numID + numIDy = materials[1].numID + numIDz = materials[2].numID + + build_triangle(x1, y1, z1, x2, y2, z2, x3, y3, z3, normal, thickness, grid.dx, grid.dy, grid.dz, numID, numIDx, numIDy, numIDz, averaging, grid.solid, grid.rigidE, grid.rigidH, grid.ID) + + if config.is_messages(): + if thickness > 0: + if averaging: + dielectricsmoothing = 'on' + else: + dielectricsmoothing = 'off' + tqdm.write('Triangle with coordinates {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m and thickness {:g}m of material(s) {} created, dielectric smoothing is {}.'.format(x1, y1, z1, x2, y2, z2, x3, y3, z3, thickness, ', '.join(materialsrequested), dielectricsmoothing)) + else: + tqdm.write('Triangle with coordinates {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m, {:g}m {:g}m {:g}m of material(s) {} created.'.format(x1, y1, z1, x2, y2, z2, x3, y3, z3, ', '.join(materialsrequested))) diff --git a/gprMax/cmds_multiple.py b/gprMax/cmds_multiple.py index 8316c143..15572223 100644 --- a/gprMax/cmds_multiple.py +++ b/gprMax/cmds_multiple.py @@ -1,1042 +1,1042 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -"""Object which can be created multiple times.""" -import gprMax.config as config -from .config import z0 -from .config import dtypes -from .utilities import round_value -from .cmds_geometry.cmds_geometry import UserObjectGeometry -from .waveforms import Waveform as WaveformUser -from .sources import VoltageSource as VoltageSourceUser -from .sources import HertzianDipole as HertzianDipoleUser -from .sources import MagneticDipole as MagneticDipoleUser -from .sources import TransmissionLine as TransmissionLineUser -from .snapshots import Snapshot as SnapshotUser -from .receivers import Rx as RxUser -from .materials import Material as MaterialUser -from .materials import PeplinskiSoil as PeplinskiSoilUser -from .geometry_outputs import GeometryObjects as GeometryObjectsUser -from .pml import CFSParameter -from .pml import CFS -from .subgrids.base import SubGridBase - -from .exceptions import CmdInputError - - -import numpy as np -from tqdm import tqdm - -floattype = dtypes['float_or_double'] - - -class UserObjectMulti: - """Specific multiobject object.""" - - def __init__(self, **kwargs): - """Constructor.""" - self.kwargs = kwargs - self.order = None - self.hash = '#example' - - def __str__(self): - """Readble user string as per hash commands.""" - s = '' - for k, v in self.kwargs.items(): - if isinstance(v, tuple) or isinstance(v, list): - v = ' '.join([str(el) for el in v]) - s += str(v) + ' ' - - return '{}: {}'.format(self.hash, s[:-1]) - - def create(self, grid, uip): - """Create the object and add it to the grid.""" - pass - - def params_str(self): - """Readble string of parameters given to object.""" - return self.hash + ': ' + str(self.kwargs) - - -class Waveform(UserObjectMulti): - """Create Waveform different excitation types.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 0 - self.hash = '#waveform' - - def create(self, grid, uip): - """Create the waveform and add it to the grid.""" - try: - wavetype = self.kwargs['wave_type'].lower() - amp = self.kwargs['amp'] - freq = self.kwargs['freq'] - ID = self.kwargs['id'] - - except KeyError: - raise CmdInputError(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))) - if freq <= 0: - raise CmdInputError(self.__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)) - - w = WaveformUser() - w.ID = ID - w.type = wavetype - 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)) - - grid.waveforms.append(w) - - -class VoltageSource(UserObjectMulti): - """User Object for a voltage source.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 1 - self.hash = '#voltage_source' - - def create(self, grid, uip): - """Create voltage source and add it to the grid.""" - try: - polarisation = self.kwargs['polarisation'].lower() - p1 = self.kwargs['p1'] - resistance = self.kwargs['resistance'] - waveform_id = self.kwargs['waveform_id'] - - except KeyError: - raise CmdInputError(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__())) - 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__())) - 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__())) - 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__())) - - 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__())) - - # 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__())) - - v = VoltageSourceUser() - v.polarisation = polarisation - v.xcoord = xcoord - v.ycoord = ycoord - v.zcoord = zcoord - v.ID = v.__class__.__name__ + '(' + str(v.xcoord) + ',' + str(v.ycoord) + ',' + str(v.zcoord) + ')' - v.resistance = resistance - v.waveformID = waveform_id - - try: - start = self.kwargs['start'] - 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__())) - if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) - if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) - 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) - except KeyError: - v.start = 0 - v.stop = grid.timewindow - startstop = ' ' - - 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)) - - grid.voltagesources.append(v) - - -class HertzianDipole(UserObjectMulti): - """User Object for HertzianDipole.""" - - 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())) - - # Check polarity & position parameters - if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) - 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__())) - 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__())) - 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__())) - - 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__())) - - h = HertzianDipoleUser() - h.polarisation = polarisation - - # Set length of dipole to grid size in polarisation direction - if h.polarisation == 'x': - h.dl = grid.dx - elif h.polarisation == 'y': - h.dl = grid.dy - elif h.polarisation == 'z': - h.dl = grid.dz - - h.xcoord = xcoord - h.ycoord = ycoord - h.zcoord = zcoord - h.xcoordorigin = xcoord - h.ycoordorigin = ycoord - h.zcoordorigin = zcoord - h.ID = h.__class__.__name__ + '(' + str(h.xcoord) + ',' + str(h.ycoord) + ',' + str(h.zcoord) + ')' - h.waveformID = waveform_id - - try: - # Check source start & source remove time parameters - 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__())) - if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) - if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) - 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) - except KeyError: - h.start = 0 - h.stop = grid.timewindow - print(grid.timewindow) - startstop = ' ' - - 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)) - - grid.hertziandipoles.append(h) - - -class MagneticDipole(UserObjectMulti): - """Magnetic Dipole User Object.""" - - 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__())) - - # Check polarity & position parameters - if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) - 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__())) - 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__())) - 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__())) - - 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)) - - m = MagneticDipoleUser() - m.polarisation = polarisation - m.xcoord = xcoord - m.ycoord = ycoord - m.zcoord = zcoord - m.xcoordorigin = xcoord - m.ycoordorigin = ycoord - m.zcoordorigin = zcoord - m.ID = m.__class__.__name__ + '(' + str(m.xcoord) + ',' + str(m.ycoord) + ',' + str(m.zcoord) + ')' - m.waveformID = waveform_id - - try: - # Check source start & source remove time parameters - 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__())) - if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) - if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) - 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) - except KeyError: - m.start = 0 - m.stop = grid.timewindow - startstop = ' ' - - 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)) - - grid.magneticdipoles.append(m) - - -class TransmissionLine(UserObjectMulti): - """Magnetic Dipole User Object.""" - - 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())) - - # 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__())) - - # Check polarity & position parameters - if polarisation not in ('x', 'y', 'z'): - raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) - 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__())) - 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__())) - 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__())) - - 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__())) - - # 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__())) - - t = TransmissionLineUser(grid) - t.polarisation = polarisation - t.xcoord = xcoord - t.ycoord = ycoord - t.zcoord = zcoord - t.ID = t.__class__.__name__ + '(' + str(t.xcoord) + ',' + str(t.ycoord) + ',' + str(t.zcoord) + ')' - t.resistance = resistance - t.waveformID = waveform_id - - try: - # Check source start & source remove time parameters - 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__())) - if stop < 0: - raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) - if stop - start <= 0: - raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) - 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) - except KeyError: - t.start = 0 - t.stop = grid.timewindow - startstop = ' ' - - 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)) - - grid.transmissionlines.append(t) - - -class Rx(UserObjectMulti): - """Magnetic Dipole User Object.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 5 - self.hash = '#rx' - self.constructor = RxUser - - def create(self, grid, uip): - try: - p1 = self.kwargs['p1'] - except KeyError: - raise CmdInputError("'{}' has an incorrect number of parameters".format(self.__str__())) - - p = uip.check_src_rx_point(p1, self.__str__()) - - r = self.constructor() - r.xcoord, r.ycoord, r.zcoord = p - r.xcoordorigin, r.ycoordorigin, r.zcoordorigin = p - - try: - r.ID = self.kwargs['id'] - outputs = self.kwargs['outputs'] - # Get allowable outputs - if grid.gpu is not None: - allowableoutputs = RxUser.gpu_allowableoutputs - else: - allowableoutputs = RxUser.allowableoutputs - # Check and add field output names - for field in outputs: - if field in allowableoutputs: - r.outputs[field] = np.zeros(grid.iterations, dtype=floattype) - else: - raise CmdInputError("{} contains an output type that is not allowable. Allowable outputs in current context are {}".format(self.__str__(), 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))) - - grid.rxs.append(r) - - return r - -class RxArray(UserObjectMulti): - """Receiver Array User Object.""" - - 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__())) - - 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__())) - if dx < 0 or dy < 0 or dz < 0: - raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) - 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__())) - 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__())) - 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__())) - - 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)) - - for x in range(xs, xf + 1, dx): - for y in range(ys, yf + 1, dy): - for z in range(zs, zf + 1, dz): - r = RxUser() - r.xcoord = x - r.ycoord = y - r.zcoord = z - r.xcoordorigin = x - r.ycoordorigin = y - 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))) - grid.rxs.append(r) - - -class Snapshot(UserObjectMulti): - """Snapshot User Object.""" - - 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__())) - 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__())) - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - dx, dy, dz = uip.discretise_point(dl) - - # If number of iterations given - try: - iterations = self.kwargs['iterations'] - # If real floating point value given - except KeyError: - try: - time = self.kwargs['time'] - except KeyError: - raise CmdInputError("'{}' requires exactly 5 parameters".format(self.__str__())) - if time > 0: - iterations = round_value((time / grid.dt)) + 1 - else: - raise CmdInputError("'{}' time value must be greater than zero".format(self.__str__())) - - if dx < 0 or dy < 0 or dz < 0: - raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) - 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__())) - if iterations <= 0 or iterations > grid.iterations: - raise CmdInputError("'{}' time value is not valid".format(self.__str__())) - - # Replace with old style snapshots if there are subgrids - if grid.subgrids: - from .snapshot_subgrid import Snapshot as SnapshotSub - s = SnapshotSub(xs, ys, zs, xf, yf, zf, dx, dy, dz, iterations, filename) - 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)) - - grid.snapshots.append(s) - -class Material(UserObjectMulti): - """Material User Object.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 8 - self.hash = '#material' - - def create(self, grid, uip): - try: - er = self.kwargs['er'] - se = self.kwargs['se'] - mr = self.kwargs['mr'] - sm = self.kwargs['sm'] - material_id = self.kwargs['id'] - except KeyError: - raise CmdInputError('{} requires exactly five parameters'.format(self.params_str())) - - if er < 1: - raise CmdInputError('{} requires a positive value of one or greater for static (DC) permittivity'.format(self.__str__())) - if se != 'inf': - se = float(se) - if se < 0: - raise CmdInputError('{} requires a positive value for conductivity'.format(self.__str__())) - else: - se = float('inf') - if mr < 1: - raise CmdInputError('{} requires a positive value of one or greater for permeability'.format(self.__str__())) - if sm < 0: - raise CmdInputError('{} requires a positive value for magnetic conductivity'.format(self.__str__())) - if any(x.ID == material_id for x in grid.materials): - raise CmdInputError('{} with ID {} already exists'.format(material_id).format(self.__str__())) - - # Create a new instance of the Material class material (start index after pec & free_space) - m = MaterialUser(len(grid.materials), material_id) - m.er = er - m.se = se - m.mr = mr - m.sm = sm - - # Set material averaging to False if infinite conductivity, i.e. pec - 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)) - - # Append the new material object to the materials list - grid.materials.append(m) - - -class AddDebyeDispersion(UserObjectMulti): - """Material User Object.""" - - 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') - - if poles < 0: - raise CmdInputError(self.__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)) - - for material in materials: - material.type = 'debye' - material.poles = poles - material.averagable = False - for i in range(0, poles): - # N.B Not checking if relaxation times are greater than time-step - if tau[i] > 0: - material.deltaer.append(er_delta[i]) - material.tau.append(tau[i]) - else: - raise CmdInputError(self.__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))) - - -class AddLorentzDispersion(UserObjectMulti): - """Material User Object.""" - - 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'] - tau = self.kwargs['omega'] - alpha = self.kwargs['delta'] - material_ids = self.kwargs['material_ids'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires at least five parameters') - - if poles < 0: - raise CmdInputError(self.__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)) - - for material in materials: - material.type = 'lorentz' - material.poles = poles - material.averagable = False - for i in range(0, poles): - if er_delta[i] > 0 and tau[i] > grid.dt and alpha[i] > grid.dt: - material.deltaer.append(er_delta[i]) - 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.') - 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))) - - -class AddDrudeDispersion(UserObjectMulti): - """Material User Object.""" - - 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') - if poles < 0: - raise CmdInputError(self.__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)) - - for material in materials: - material.type = 'drude' - material.poles = poles - material.averagable = False - for i in range(0, poles): - if tau[i] > 0 and alpha[i] > grid.dt: - 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.') - 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))) - - -class SoilPeplinski(UserObjectMulti): - """Material User Object.""" - - 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'] - bulk_density = self.kwargs['bulk_density'] - sand_density = self.kwargs['sand_density'] - 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') - - if sand_fraction < 0: - raise CmdInputError(self.__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') - if bulk_density < 0: - raise CmdInputError(self.__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') - if water_fraction_lower < 0: - raise CmdInputError(self.__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') - if any(x.ID == ID for x in grid.mixingmodels): - raise CmdInputError(self.__str__() + ' with ID {} already exists'.format(ID)) - - # 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])) - - # Append the new material object to the materials list - grid.mixingmodels.append(s) - - -class GeometryView(UserObjectMulti): - """Material User Object.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 18 - self.hash = '#geometry_view' - self.multi_grid = False - - def geometry_view_constructor(self, grid, output_type): - # Check if user want geometry output for all grids - try: - 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__())) - if output_type == 'n': - raise CmdInputError("'{}' Voxel output type (n) is not supported for multigrid output :(".format(self.__str__())) - # Change constructor to the multi grid output - from .geometry_outputs import GeometryViewFineMultiGrid as GeometryViewUser - self.multi_grid = True - except KeyError: - self.multi_grid = False - from .geometry_outputs import GeometryView as GeometryViewUser - - return GeometryViewUser - - def create(self, grid, uip): - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - dl = self.kwargs['dl'] - output_type = self.kwargs['output_type'].lower() - filename = self.kwargs['filename'] - except KeyError: - raise CmdInputError("'{}' requires exactly eleven parameters".format(self.__str__())) - - GeometryViewUser = self.geometry_view_constructor(grid, output_type) - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - xs, ys, zs = p1 - xf, yf, zf = p2 - - 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__())) - 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__())) - 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__())) - if output_type != 'n' and output_type != 'f': - raise CmdInputError("'{}' requires type to be either n (normal) or f (fine)".format(self.__str__())) - 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__())) - - # Set type of geometry file - if output_type == 'n': - fileext = '.vti' - else: - fileext = '.vtp' - - 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)) - - # Append the new GeometryView object to the geometry views list - grid.geometryviews.append(g) - - -class GeometryObjectsWrite(UserObjectMulti): - """Material User Object.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.order = 14 - self.hash = '#geometry_objects_write' - - def create(self, grid, uip): - try: - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - filename = self.kwargs['filename'] - except KeyError: - raise CmdInputError("'{}' requires exactly seven parameters".format(self.__str__())) - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - x0, y0, z0 = p1 - x1, y1, z1 = p2 - - 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)) - - # Append the new GeometryView object to the geometry objects to write list - grid.geometryobjectswrite.append(g) - - -class PMLCFS(UserObjectMulti): - """Material User Object.""" - - 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') - - def create(self, grid, uip): - - try: - alphascalingprofile = self.kwargs['alphascalingprofile'] - alphascalingdirection = self.kwargs['alphascalingdirection'] - alphamin = self.kwargs['alphamin'] - alphamax = self.kwargs['alphamax'] - kappascalingprofile = self.kwargs['kappascalingprofile'] - kappascalingdirection = self.kwargs['kappascalingdirection'] - kappamin = self.kwargs['kappamin'] - kappamax = self.kwargs['kappamax'] - sigmascalingprofile = self.kwargs['sigmascalingprofile'] - sigmascalingdirection = self.kwargs['sigmascalingdirection'] - sigmamin = self.kwargs['sigmamin'] - sigmamax = self.kwargs['sigmamax'] - - except KeyError: - raise CmdInputError(self.__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()))) - 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))) - 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') - if float(kappamin) < 1: - raise CmdInputError(self.__str__() + ' minimum scaling value for kappa must be greater than or equal to one') - - cfsalpha = CFSParameter() - cfsalpha.ID = 'alpha' - cfsalpha.scalingprofile = alphascalingprofile - cfsalpha.scalingdirection = alphascalingdirection - cfsalpha.min = float(alphamin) - cfsalpha.max = float(alphamax) - cfskappa = CFSParameter() - cfskappa.ID = 'kappa' - cfskappa.scalingprofile = kappascalingprofile - cfskappa.scalingdirection = kappascalingdirection - cfskappa.min = float(kappamin) - cfskappa.max = float(kappamax) - cfssigma = CFSParameter() - cfssigma.ID = 'sigma' - cfssigma.scalingprofile = sigmascalingprofile - cfssigma.scalingdirection = sigmascalingdirection - cfssigma.min = float(sigmamin) - if sigmamax == 'None': - cfssigma.max = None - else: - cfssigma.max = float(sigmamax) - cfs = CFS() - cfs.alpha = cfsalpha - 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)) - - grid.cfs.append(cfs) - -class Subgrid(UserObjectMulti): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.children_multiple = [] - self.children_geometry = [] - - def add(self, node): - if isinstance(node, UserObjectMulti): - self.children_multiple.append(node) - elif isinstance(node, UserObjectGeometry): - self.children_geometry.append(node) - else: - raise Exception('This Object is Unknown to gprMax') - -class SubgridHSG(UserObjectMulti): - - def __init__(self, **kwargs): - super().__init__(**kwargs) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +"""Object which can be created multiple times.""" +import gprMax.config as config +from .config import z0 +from .config import dtypes +from .utilities import round_value +from .cmds_geometry.cmds_geometry import UserObjectGeometry +from .waveforms import Waveform as WaveformUser +from .sources import VoltageSource as VoltageSourceUser +from .sources import HertzianDipole as HertzianDipoleUser +from .sources import MagneticDipole as MagneticDipoleUser +from .sources import TransmissionLine as TransmissionLineUser +from .snapshots import Snapshot as SnapshotUser +from .receivers import Rx as RxUser +from .materials import Material as MaterialUser +from .materials import PeplinskiSoil as PeplinskiSoilUser +from .geometry_outputs import GeometryObjects as GeometryObjectsUser +from .pml import CFSParameter +from .pml import CFS +from .subgrids.base import SubGridBase + +from .exceptions import CmdInputError + + +import numpy as np +from tqdm import tqdm + +floattype = dtypes['float_or_double'] + + +class UserObjectMulti: + """Specific multiobject object.""" + + def __init__(self, **kwargs): + """Constructor.""" + self.kwargs = kwargs + self.order = None + self.hash = '#example' + + def __str__(self): + """Readble user string as per hash commands.""" + s = '' + for k, v in self.kwargs.items(): + if isinstance(v, tuple) or isinstance(v, list): + v = ' '.join([str(el) for el in v]) + s += str(v) + ' ' + + return '{}: {}'.format(self.hash, s[:-1]) + + def create(self, grid, uip): + """Create the object and add it to the grid.""" + pass + + def params_str(self): + """Readble string of parameters given to object.""" + return self.hash + ': ' + str(self.kwargs) + + +class Waveform(UserObjectMulti): + """Create Waveform different excitation types.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 0 + self.hash = '#waveform' + + def create(self, grid, uip): + """Create the waveform and add it to the grid.""" + try: + wavetype = self.kwargs['wave_type'].lower() + amp = self.kwargs['amp'] + freq = self.kwargs['freq'] + ID = self.kwargs['id'] + + except KeyError: + raise CmdInputError(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))) + if freq <= 0: + raise CmdInputError(self.__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)) + + w = WaveformUser() + w.ID = ID + w.type = wavetype + 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)) + + grid.waveforms.append(w) + + +class VoltageSource(UserObjectMulti): + """User Object for a voltage source.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 1 + self.hash = '#voltage_source' + + def create(self, grid, uip): + """Create voltage source and add it to the grid.""" + try: + polarisation = self.kwargs['polarisation'].lower() + p1 = self.kwargs['p1'] + resistance = self.kwargs['resistance'] + waveform_id = self.kwargs['waveform_id'] + + except KeyError: + raise CmdInputError(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__())) + 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__())) + 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__())) + 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__())) + + 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__())) + + # 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__())) + + v = VoltageSourceUser() + v.polarisation = polarisation + v.xcoord = xcoord + v.ycoord = ycoord + v.zcoord = zcoord + v.ID = v.__class__.__name__ + '(' + str(v.xcoord) + ',' + str(v.ycoord) + ',' + str(v.zcoord) + ')' + v.resistance = resistance + v.waveformID = waveform_id + + try: + start = self.kwargs['start'] + 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__())) + if stop < 0: + raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + if stop - start <= 0: + raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + 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) + except KeyError: + v.start = 0 + v.stop = grid.timewindow + startstop = ' ' + + 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)) + + grid.voltagesources.append(v) + + +class HertzianDipole(UserObjectMulti): + """User Object for HertzianDipole.""" + + 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())) + + # Check polarity & position parameters + if polarisation not in ('x', 'y', 'z'): + raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + 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__())) + 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__())) + 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__())) + + 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__())) + + h = HertzianDipoleUser() + h.polarisation = polarisation + + # Set length of dipole to grid size in polarisation direction + if h.polarisation == 'x': + h.dl = grid.dx + elif h.polarisation == 'y': + h.dl = grid.dy + elif h.polarisation == 'z': + h.dl = grid.dz + + h.xcoord = xcoord + h.ycoord = ycoord + h.zcoord = zcoord + h.xcoordorigin = xcoord + h.ycoordorigin = ycoord + h.zcoordorigin = zcoord + h.ID = h.__class__.__name__ + '(' + str(h.xcoord) + ',' + str(h.ycoord) + ',' + str(h.zcoord) + ')' + h.waveformID = waveform_id + + try: + # Check source start & source remove time parameters + 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__())) + if stop < 0: + raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + if stop - start <= 0: + raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + 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) + except KeyError: + h.start = 0 + h.stop = grid.timewindow + print(grid.timewindow) + startstop = ' ' + + 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)) + + grid.hertziandipoles.append(h) + + +class MagneticDipole(UserObjectMulti): + """Magnetic Dipole User Object.""" + + 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__())) + + # Check polarity & position parameters + if polarisation not in ('x', 'y', 'z'): + raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + 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__())) + 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__())) + 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__())) + + 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)) + + m = MagneticDipoleUser() + m.polarisation = polarisation + m.xcoord = xcoord + m.ycoord = ycoord + m.zcoord = zcoord + m.xcoordorigin = xcoord + m.ycoordorigin = ycoord + m.zcoordorigin = zcoord + m.ID = m.__class__.__name__ + '(' + str(m.xcoord) + ',' + str(m.ycoord) + ',' + str(m.zcoord) + ')' + m.waveformID = waveform_id + + try: + # Check source start & source remove time parameters + 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__())) + if stop < 0: + raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + if stop - start <= 0: + raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + 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) + except KeyError: + m.start = 0 + m.stop = grid.timewindow + startstop = ' ' + + 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)) + + grid.magneticdipoles.append(m) + + +class TransmissionLine(UserObjectMulti): + """Magnetic Dipole User Object.""" + + 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())) + + # 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__())) + + # Check polarity & position parameters + if polarisation not in ('x', 'y', 'z'): + raise CmdInputError("'{}' polarisation must be x, y, or z".format(self.__str__())) + 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__())) + 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__())) + 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__())) + + 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__())) + + # 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__())) + + t = TransmissionLineUser(grid) + t.polarisation = polarisation + t.xcoord = xcoord + t.ycoord = ycoord + t.zcoord = zcoord + t.ID = t.__class__.__name__ + '(' + str(t.xcoord) + ',' + str(t.ycoord) + ',' + str(t.zcoord) + ')' + t.resistance = resistance + t.waveformID = waveform_id + + try: + # Check source start & source remove time parameters + 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__())) + if stop < 0: + raise CmdInputError("'{}' time to remove the source should not be less than zero".format(self.__str__())) + if stop - start <= 0: + raise CmdInputError("'{}' duration of the source should not be zero or less".format(self.__str__())) + 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) + except KeyError: + t.start = 0 + t.stop = grid.timewindow + startstop = ' ' + + 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)) + + grid.transmissionlines.append(t) + + +class Rx(UserObjectMulti): + """Magnetic Dipole User Object.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 5 + self.hash = '#rx' + self.constructor = RxUser + + def create(self, grid, uip): + try: + p1 = self.kwargs['p1'] + except KeyError: + raise CmdInputError("'{}' has an incorrect number of parameters".format(self.__str__())) + + p = uip.check_src_rx_point(p1, self.__str__()) + + r = self.constructor() + r.xcoord, r.ycoord, r.zcoord = p + r.xcoordorigin, r.ycoordorigin, r.zcoordorigin = p + + try: + r.ID = self.kwargs['id'] + outputs = self.kwargs['outputs'] + # Get allowable outputs + if grid.gpu is not None: + allowableoutputs = RxUser.gpu_allowableoutputs + else: + allowableoutputs = RxUser.allowableoutputs + # Check and add field output names + for field in outputs: + if field in allowableoutputs: + r.outputs[field] = np.zeros(grid.iterations, dtype=floattype) + else: + raise CmdInputError("{} contains an output type that is not allowable. Allowable outputs in current context are {}".format(self.__str__(), 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))) + + grid.rxs.append(r) + + return r + +class RxArray(UserObjectMulti): + """Receiver Array User Object.""" + + 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__())) + + 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__())) + if dx < 0 or dy < 0 or dz < 0: + raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) + 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__())) + 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__())) + 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__())) + + 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)) + + for x in range(xs, xf + 1, dx): + for y in range(ys, yf + 1, dy): + for z in range(zs, zf + 1, dz): + r = RxUser() + r.xcoord = x + r.ycoord = y + r.zcoord = z + r.xcoordorigin = x + r.ycoordorigin = y + 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))) + grid.rxs.append(r) + + +class Snapshot(UserObjectMulti): + """Snapshot User Object.""" + + 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__())) + 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__())) + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + dx, dy, dz = uip.discretise_point(dl) + + # If number of iterations given + try: + iterations = self.kwargs['iterations'] + # If real floating point value given + except KeyError: + try: + time = self.kwargs['time'] + except KeyError: + raise CmdInputError("'{}' requires exactly 5 parameters".format(self.__str__())) + if time > 0: + iterations = round_value((time / grid.dt)) + 1 + else: + raise CmdInputError("'{}' time value must be greater than zero".format(self.__str__())) + + if dx < 0 or dy < 0 or dz < 0: + raise CmdInputError("'{}' the step size should not be less than zero".format(self.__str__())) + 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__())) + if iterations <= 0 or iterations > grid.iterations: + raise CmdInputError("'{}' time value is not valid".format(self.__str__())) + + # Replace with old style snapshots if there are subgrids + if grid.subgrids: + from .snapshot_subgrid import Snapshot as SnapshotSub + s = SnapshotSub(xs, ys, zs, xf, yf, zf, dx, dy, dz, iterations, filename) + 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)) + + grid.snapshots.append(s) + +class Material(UserObjectMulti): + """Material User Object.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 8 + self.hash = '#material' + + def create(self, grid, uip): + try: + er = self.kwargs['er'] + se = self.kwargs['se'] + mr = self.kwargs['mr'] + sm = self.kwargs['sm'] + material_id = self.kwargs['id'] + except KeyError: + raise CmdInputError('{} requires exactly five parameters'.format(self.params_str())) + + if er < 1: + raise CmdInputError('{} requires a positive value of one or greater for static (DC) permittivity'.format(self.__str__())) + if se != 'inf': + se = float(se) + if se < 0: + raise CmdInputError('{} requires a positive value for conductivity'.format(self.__str__())) + else: + se = float('inf') + if mr < 1: + raise CmdInputError('{} requires a positive value of one or greater for permeability'.format(self.__str__())) + if sm < 0: + raise CmdInputError('{} requires a positive value for magnetic conductivity'.format(self.__str__())) + if any(x.ID == material_id for x in grid.materials): + raise CmdInputError('{} with ID {} already exists'.format(material_id).format(self.__str__())) + + # Create a new instance of the Material class material (start index after pec & free_space) + m = MaterialUser(len(grid.materials), material_id) + m.er = er + m.se = se + m.mr = mr + m.sm = sm + + # Set material averaging to False if infinite conductivity, i.e. pec + 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)) + + # Append the new material object to the materials list + grid.materials.append(m) + + +class AddDebyeDispersion(UserObjectMulti): + """Material User Object.""" + + 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') + + if poles < 0: + raise CmdInputError(self.__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)) + + for material in materials: + material.type = 'debye' + material.poles = poles + material.averagable = False + for i in range(0, poles): + # N.B Not checking if relaxation times are greater than time-step + if tau[i] > 0: + material.deltaer.append(er_delta[i]) + material.tau.append(tau[i]) + else: + raise CmdInputError(self.__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))) + + +class AddLorentzDispersion(UserObjectMulti): + """Material User Object.""" + + 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'] + tau = self.kwargs['omega'] + alpha = self.kwargs['delta'] + material_ids = self.kwargs['material_ids'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires at least five parameters') + + if poles < 0: + raise CmdInputError(self.__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)) + + for material in materials: + material.type = 'lorentz' + material.poles = poles + material.averagable = False + for i in range(0, poles): + if er_delta[i] > 0 and tau[i] > grid.dt and alpha[i] > grid.dt: + material.deltaer.append(er_delta[i]) + 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.') + 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))) + + +class AddDrudeDispersion(UserObjectMulti): + """Material User Object.""" + + 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') + if poles < 0: + raise CmdInputError(self.__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)) + + for material in materials: + material.type = 'drude' + material.poles = poles + material.averagable = False + for i in range(0, poles): + if tau[i] > 0 and alpha[i] > grid.dt: + 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.') + 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))) + + +class SoilPeplinski(UserObjectMulti): + """Material User Object.""" + + 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'] + bulk_density = self.kwargs['bulk_density'] + sand_density = self.kwargs['sand_density'] + 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') + + if sand_fraction < 0: + raise CmdInputError(self.__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') + if bulk_density < 0: + raise CmdInputError(self.__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') + if water_fraction_lower < 0: + raise CmdInputError(self.__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') + if any(x.ID == ID for x in grid.mixingmodels): + raise CmdInputError(self.__str__() + ' with ID {} already exists'.format(ID)) + + # 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])) + + # Append the new material object to the materials list + grid.mixingmodels.append(s) + + +class GeometryView(UserObjectMulti): + """Material User Object.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 18 + self.hash = '#geometry_view' + self.multi_grid = False + + def geometry_view_constructor(self, grid, output_type): + # Check if user want geometry output for all grids + try: + 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__())) + if output_type == 'n': + raise CmdInputError("'{}' Voxel output type (n) is not supported for multigrid output :(".format(self.__str__())) + # Change constructor to the multi grid output + from .geometry_outputs import GeometryViewFineMultiGrid as GeometryViewUser + self.multi_grid = True + except KeyError: + self.multi_grid = False + from .geometry_outputs import GeometryView as GeometryViewUser + + return GeometryViewUser + + def create(self, grid, uip): + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + dl = self.kwargs['dl'] + output_type = self.kwargs['output_type'].lower() + filename = self.kwargs['filename'] + except KeyError: + raise CmdInputError("'{}' requires exactly eleven parameters".format(self.__str__())) + + GeometryViewUser = self.geometry_view_constructor(grid, output_type) + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + xs, ys, zs = p1 + xf, yf, zf = p2 + + 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__())) + 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__())) + 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__())) + if output_type != 'n' and output_type != 'f': + raise CmdInputError("'{}' requires type to be either n (normal) or f (fine)".format(self.__str__())) + 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__())) + + # Set type of geometry file + if output_type == 'n': + fileext = '.vti' + else: + fileext = '.vtp' + + 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)) + + # Append the new GeometryView object to the geometry views list + grid.geometryviews.append(g) + + +class GeometryObjectsWrite(UserObjectMulti): + """Material User Object.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.order = 14 + self.hash = '#geometry_objects_write' + + def create(self, grid, uip): + try: + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + filename = self.kwargs['filename'] + except KeyError: + raise CmdInputError("'{}' requires exactly seven parameters".format(self.__str__())) + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + x0, y0, z0 = p1 + x1, y1, z1 = p2 + + 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)) + + # Append the new GeometryView object to the geometry objects to write list + grid.geometryobjectswrite.append(g) + + +class PMLCFS(UserObjectMulti): + """Material User Object.""" + + 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') + + def create(self, grid, uip): + + try: + alphascalingprofile = self.kwargs['alphascalingprofile'] + alphascalingdirection = self.kwargs['alphascalingdirection'] + alphamin = self.kwargs['alphamin'] + alphamax = self.kwargs['alphamax'] + kappascalingprofile = self.kwargs['kappascalingprofile'] + kappascalingdirection = self.kwargs['kappascalingdirection'] + kappamin = self.kwargs['kappamin'] + kappamax = self.kwargs['kappamax'] + sigmascalingprofile = self.kwargs['sigmascalingprofile'] + sigmascalingdirection = self.kwargs['sigmascalingdirection'] + sigmamin = self.kwargs['sigmamin'] + sigmamax = self.kwargs['sigmamax'] + + except KeyError: + raise CmdInputError(self.__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()))) + 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))) + 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') + if float(kappamin) < 1: + raise CmdInputError(self.__str__() + ' minimum scaling value for kappa must be greater than or equal to one') + + cfsalpha = CFSParameter() + cfsalpha.ID = 'alpha' + cfsalpha.scalingprofile = alphascalingprofile + cfsalpha.scalingdirection = alphascalingdirection + cfsalpha.min = float(alphamin) + cfsalpha.max = float(alphamax) + cfskappa = CFSParameter() + cfskappa.ID = 'kappa' + cfskappa.scalingprofile = kappascalingprofile + cfskappa.scalingdirection = kappascalingdirection + cfskappa.min = float(kappamin) + cfskappa.max = float(kappamax) + cfssigma = CFSParameter() + cfssigma.ID = 'sigma' + cfssigma.scalingprofile = sigmascalingprofile + cfssigma.scalingdirection = sigmascalingdirection + cfssigma.min = float(sigmamin) + if sigmamax == 'None': + cfssigma.max = None + else: + cfssigma.max = float(sigmamax) + cfs = CFS() + cfs.alpha = cfsalpha + 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)) + + grid.cfs.append(cfs) + +class Subgrid(UserObjectMulti): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.children_multiple = [] + self.children_geometry = [] + + def add(self, node): + if isinstance(node, UserObjectMulti): + self.children_multiple.append(node) + elif isinstance(node, UserObjectGeometry): + self.children_geometry.append(node) + else: + raise Exception('This Object is Unknown to gprMax') + +class SubgridHSG(UserObjectMulti): + + def __init__(self, **kwargs): + super().__init__(**kwargs) diff --git a/gprMax/cmds_single_use.py b/gprMax/cmds_single_use.py index fe1aa759..634f01a6 100644 --- a/gprMax/cmds_single_use.py +++ b/gprMax/cmds_single_use.py @@ -1,526 +1,526 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -import inspect -import os -import sys -import decimal as d - -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 - -import numpy as np -from scipy import interpolate -from colorama import init -from colorama import Fore -from colorama import Style -init() - -floattype = dtypes['float_or_double'] - -class UserObjectSingle: - - """ - Specific geometry object - """ - - def __init__(self, **kwargs): - # each single command has an order to specify the order in which - # the commands are constructed. IE. discretisation must be - # created before the domain - self.order = None - self.kwargs = kwargs - - def __str__(self): - pass - - def create(self, grid, uip): - pass - - -class DomainSingle(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 2 - - def __str__(self): - try: - s = '#domain: {} {} {}'.format(self.kwargs['p1'][0], - self.kwargs['p1'][1], - self.kwargs['p1'][2]) - except KeyError: - print('error message') - - return s - - def create(self, G, uip): - - # code to create the gprMax domain as per input_cmds_singleuse.py - try: - G.nx, G.ny, G.nz = uip.discretise_point(self.kwargs['p1']) - except KeyError: - raise CmdInputError(self.__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))) - - # Time step CFL limit (either 2D or 3D); 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.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.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.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.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. - # 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)) - - # Number of threads (OpenMP) to use - if sys.platform == 'darwin': - os.environ['OMP_WAIT_POLICY'] = 'ACTIVE' # Should waiting threads consume CPU power (can drastically effect performance) - os.environ['OMP_DYNAMIC'] = 'FALSE' # Number of threads may be adjusted by the run time environment to best utilize system resources - os.environ['OMP_PLACES'] = 'cores' # Each place corresponds to a single core (having one or more hardware threads) - os.environ['OMP_PROC_BIND'] = 'TRUE' # Bind threads to physical cores - # 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']: - os.environ['KMP_AFFINITY'] = 'disabled' - del os.environ['OMP_PLACES'] - del os.environ['OMP_PROC_BIND'] - - if os.environ.get('OMP_NUM_THREADS'): - G.nthreads = int(os.environ.get('OMP_NUM_THREADS')) - else: - # Set number of threads to number of physical CPU cores - G.nthreads = 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) - - - -class Domain: - - """Restrict user object so there can only be one instance - https://python-3-patterns-idioms-test.readthedocs.io/en/latest/index.html - """ - - instance = None - - def __new__(cls, **kwargs): # __new__ always a classmethod - if not Domain.instance: - Domain.instance = DomainSingle(**kwargs) - return Domain.instance - - -class Discretisation(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 1 - - def __str__(self): - try: - s = '#dx_dy_dz: {} {} {}'.format(self.kwargs['p1'][0], - self.kwargs['p1'][1], - self.kwargs['p1'][2]) - except KeyError: - print('error message') - - return s - - def create(self, G, uip): - - try: - G.dl = np.array(self.kwargs['p1']) - G.dx, G.dy, G.dz = self.kwargs['p1'] - - except KeyError: - raise CmdInputError('Discretisation requires a point') - - if G.dl[0] <= 0: - raise CmdInputError('Discretisation requires the x-direction spatial step to be greater than zero') - if G.dl[1] <= 0: - raise CmdInputError(' Discretisation requires the y-direction spatial step to be greater than zero') - if G.dl[2] <= 0: - 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)) - - -class TimeWindow(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 4 - - def __str__(self): - try: - s = '#time_window: {}'.format(self.kwargs['time']) - except KeyError: - try: - s = '#time_window: {}'.format(self.kwargs['iterations']) - except KeyError: - print('time window error') - - return s - - def create(self, G, uip): - # If number of iterations given - # The +/- 1 used in calculating the number of iterations is to account for - # the fact that the solver (iterations) loop runs from 0 to < G.iterations - try: - iterations = int(self.kwargs['iterations']) - G.timewindow = (iterations - 1) * G.dt - G.iterations = iterations - - except KeyError: - pass - - try: - tmp = float(self.kwargs['time']) - if tmp > 0: - G.timewindow = tmp - G.iterations = int(np.ceil(tmp / G.dt)) + 1 - else: - raise CmdInputError(self.__str__() + ' must have a value greater than zero') - - except KeyError: - pass - - if not G.timewindow: - 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)) - - -class Messages(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 0 - - def __str__(self): - try: - s = '#messages: {}'.format(self.kwargs['yn']) - except KeyError: - print('messages problem') - - def create(self, G, uip): - try: - yn = self.kwargs['yn'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires exactly one parameter') - - if yn.lower() == 'y': - config.general['messages'] = True - elif yn.lower() == 'n': - config.general['messages'] = False - else: - raise CmdInputError(self.__str__() + ' requires input values of either y or n') - - -class Title(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 5 - - def create(self, G, uip): - # Title - try: - title = self.kwargs['name'] - G.title = title - except KeyError: - pass - - if config.is_messages(): - print('Model title: {}'.format(G.title)) - -class NumThreads(UserObjectSingle): - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 6 - - def __str__(self): - try: - return '#n_threads: {}'.format(self.kwargs['n']) - except KeyError: - return '#n_threads:' - - def create(self, G, uip): - # Get information about host machine - - try: - n = self.kwargs['n'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires exactly one parameter to specify the number of threads to use') - - if n < 1: - raise CmdInputError(self.__str__() + ' requires the value to be an integer not less than one') - - G.nthreads = n - 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) - - # 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)) - - -# Time step stability factor -class TimeStepStabilityFactor(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 7 - - def __str__(self): - - try: - return '#time_step_stability_factor: {}'.format(self.kwargs['f']) - except KeyError: - return '#time_step_stability_factor:' - - def create(self, G, uip): - - try: - f = self.kwargs['f'] - except KeyError: - raise CmdInputError(self.__str__() + ' requires exactly one parameter') - - if f <= 0 or f > 1: - 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)) - - -class PMLCells(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 8 - - def create(self, G, uip): - - try: - thickness = self.kwargs['thickness'] - - for key in G.pmlthickness.keys(): - G.pmlthickness[key] = int(thickness) - - except KeyError: - try: - G.pmlthickness['x0'] = int(self.kwargs['x0']) - G.pmlthickness['y0'] = int(self.kwargs['y0']) - G.pmlthickness['z0'] = int(self.kwargs['z0']) - G.pmlthickness['xmax'] = int(self.kwargs['xmax']) - G.pmlthickness['ymax'] = int(self.kwargs['ymax']) - G.pmlthickness['zmax'] = int(self.kwargs['zmax']) - except KeyError: - raise CmdInputError('#pml_cells: requires either one or six parameter(s)') - - if (2 * G.pmlthickness['x0'] >= G.nx or - 2 * G.pmlthickness['y0'] >= G.ny or - 2 * G.pmlthickness['z0'] >= G.nz or - 2 * G.pmlthickness['xmax'] >= G.nx or - 2 * G.pmlthickness['ymax'] >= G.ny or - 2 * G.pmlthickness['zmax'] >= G.nz): - raise CmdInputError('#pml_thickness: has too many cells for the domain size') - - -class SrcSteps(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 9 - - def create(self, G, uip): - try: - G.srcsteps = uip.discretise_point(self.kwargs['p1']) - except KeyError: - raise CmdInputError('#src_steps: requires exactly three parameters') - # src_steps - 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)) - - -class RxSteps(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 10 - - def create(self, G, uip): - try: - G.rxsteps = uip.discretise_point(self.kwargs['p1']) - except KeyError: - 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)) - - -class ExcitationFile(UserObjectSingle): - - def create(self, G, uip): - # Excitation file for user-defined source waveforms - try: - kwargs = dict() - excitationfile = self.kwargs['filepath'] - kwargs['kind'] = self.kwargs['kind'] - kwargs['fill_value'] = self.kwargs['fill_value'] - - except KeyError: - try: - excitationfile = self.kwargs['filepath'] - args, varargs, keywords, defaults = inspect.getargspec(interpolate.interp1d) - kwargs = dict(zip(reversed(args), reversed(defaults))) - except KeyError: - raise CmdInputError('#excitation_file: requires either one or three parameter(s)') - - # See if file exists at specified path and if not try input file directory - if not os.path.isfile(excitationfile): - excitationfile = os.path.abspath(os.path.join(G.inputdirectory, excitationfile)) - - if config.is_messages(): - print('\nExcitation file: {}'.format(excitationfile)) - - # Get waveform names - with open(excitationfile, 'r') as f: - waveformIDs = f.readline().split() - - # Read all waveform values into an array - waveformvalues = np.loadtxt(excitationfile, skiprows=1, dtype=floattype) - - # Time array (if specified) for interpolation, otherwise use simulation time - if waveformIDs[0].lower() == 'time': - waveformIDs = waveformIDs[1:] - waveformtime = waveformvalues[:, 0] - waveformvalues = waveformvalues[:, 1:] - timestr = 'user-defined time array' - else: - waveformtime = np.arange(0, G.timewindow + G.dt, G.dt) - timestr = 'simulation time array' - - for waveform in range(len(waveformIDs)): - if any(x.ID == waveformIDs[waveform] for x in G.waveforms): - raise CmdInputError('Waveform with ID {} already exists'.format(waveformIDs[waveform])) - w = Waveform() - w.ID = waveformIDs[waveform] - w.type = 'user' - - # Select correct column of waveform values depending on array shape - singlewaveformvalues = waveformvalues[:] if len(waveformvalues.shape) == 1 else waveformvalues[:, waveform] - - # Truncate waveform array if it is longer than time array - if len(singlewaveformvalues) > len(waveformtime): - singlewaveformvalues = singlewaveformvalues[:len(waveformtime)] - # Zero-pad end of waveform array if it is shorter than time array - elif len(singlewaveformvalues) < len(waveformtime): - singlewaveformvalues = np.lib.pad(singlewaveformvalues, (0, len(singlewaveformvalues) - len(waveformvalues)), 'constant', constant_values=0) - - # Interpolate waveform values - 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'])) - - G.waveforms.append(w) - - -class OutputDir(UserObjectSingle): - - def __init__(self, **kwargs): - # dont need to define parameters in advance. Just catch errors - # when they occur - super().__init__(**kwargs) - self.order = 11 - - def create(self, grid, uip): - grid.outputdirectory = self.kwargs['dir'] - - -class NumberOfModelRuns(UserObjectSingle): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 12 - - def create(self, grid, uip): - try: - grid.numberofmodelruns = self.kwargs['n'] - except KeyError: - raise CmdInputError('#numberofmodelruns: requires exactly one parameter') +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import inspect +import os +import sys +import decimal as d + +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 + +import numpy as np +from scipy import interpolate +from colorama import init +from colorama import Fore +from colorama import Style +init() + +floattype = dtypes['float_or_double'] + +class UserObjectSingle: + + """ + Specific geometry object + """ + + def __init__(self, **kwargs): + # each single command has an order to specify the order in which + # the commands are constructed. IE. discretisation must be + # created before the domain + self.order = None + self.kwargs = kwargs + + def __str__(self): + pass + + def create(self, grid, uip): + pass + + +class DomainSingle(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 2 + + def __str__(self): + try: + s = '#domain: {} {} {}'.format(self.kwargs['p1'][0], + self.kwargs['p1'][1], + self.kwargs['p1'][2]) + except KeyError: + print('error message') + + return s + + def create(self, G, uip): + + # code to create the gprMax domain as per input_cmds_singleuse.py + try: + G.nx, G.ny, G.nz = uip.discretise_point(self.kwargs['p1']) + except KeyError: + raise CmdInputError(self.__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))) + + # Time step CFL limit (either 2D or 3D); 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.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.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.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.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. + # 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)) + + # Number of threads (OpenMP) to use + if sys.platform == 'darwin': + os.environ['OMP_WAIT_POLICY'] = 'ACTIVE' # Should waiting threads consume CPU power (can drastically effect performance) + os.environ['OMP_DYNAMIC'] = 'FALSE' # Number of threads may be adjusted by the run time environment to best utilize system resources + os.environ['OMP_PLACES'] = 'cores' # Each place corresponds to a single core (having one or more hardware threads) + os.environ['OMP_PROC_BIND'] = 'TRUE' # Bind threads to physical cores + # 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']: + os.environ['KMP_AFFINITY'] = 'disabled' + del os.environ['OMP_PLACES'] + del os.environ['OMP_PROC_BIND'] + + if os.environ.get('OMP_NUM_THREADS'): + G.nthreads = int(os.environ.get('OMP_NUM_THREADS')) + else: + # Set number of threads to number of physical CPU cores + G.nthreads = 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) + + + +class Domain: + + """Restrict user object so there can only be one instance + https://python-3-patterns-idioms-test.readthedocs.io/en/latest/index.html + """ + + instance = None + + def __new__(cls, **kwargs): # __new__ always a classmethod + if not Domain.instance: + Domain.instance = DomainSingle(**kwargs) + return Domain.instance + + +class Discretisation(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 1 + + def __str__(self): + try: + s = '#dx_dy_dz: {} {} {}'.format(self.kwargs['p1'][0], + self.kwargs['p1'][1], + self.kwargs['p1'][2]) + except KeyError: + print('error message') + + return s + + def create(self, G, uip): + + try: + G.dl = np.array(self.kwargs['p1']) + G.dx, G.dy, G.dz = self.kwargs['p1'] + + except KeyError: + raise CmdInputError('Discretisation requires a point') + + if G.dl[0] <= 0: + raise CmdInputError('Discretisation requires the x-direction spatial step to be greater than zero') + if G.dl[1] <= 0: + raise CmdInputError(' Discretisation requires the y-direction spatial step to be greater than zero') + if G.dl[2] <= 0: + 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)) + + +class TimeWindow(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 4 + + def __str__(self): + try: + s = '#time_window: {}'.format(self.kwargs['time']) + except KeyError: + try: + s = '#time_window: {}'.format(self.kwargs['iterations']) + except KeyError: + print('time window error') + + return s + + def create(self, G, uip): + # If number of iterations given + # The +/- 1 used in calculating the number of iterations is to account for + # the fact that the solver (iterations) loop runs from 0 to < G.iterations + try: + iterations = int(self.kwargs['iterations']) + G.timewindow = (iterations - 1) * G.dt + G.iterations = iterations + + except KeyError: + pass + + try: + tmp = float(self.kwargs['time']) + if tmp > 0: + G.timewindow = tmp + G.iterations = int(np.ceil(tmp / G.dt)) + 1 + else: + raise CmdInputError(self.__str__() + ' must have a value greater than zero') + + except KeyError: + pass + + if not G.timewindow: + 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)) + + +class Messages(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 0 + + def __str__(self): + try: + s = '#messages: {}'.format(self.kwargs['yn']) + except KeyError: + print('messages problem') + + def create(self, G, uip): + try: + yn = self.kwargs['yn'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires exactly one parameter') + + if yn.lower() == 'y': + config.general['messages'] = True + elif yn.lower() == 'n': + config.general['messages'] = False + else: + raise CmdInputError(self.__str__() + ' requires input values of either y or n') + + +class Title(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 5 + + def create(self, G, uip): + # Title + try: + title = self.kwargs['name'] + G.title = title + except KeyError: + pass + + if config.is_messages(): + print('Model title: {}'.format(G.title)) + +class NumThreads(UserObjectSingle): + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 6 + + def __str__(self): + try: + return '#n_threads: {}'.format(self.kwargs['n']) + except KeyError: + return '#n_threads:' + + def create(self, G, uip): + # Get information about host machine + + try: + n = self.kwargs['n'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires exactly one parameter to specify the number of threads to use') + + if n < 1: + raise CmdInputError(self.__str__() + ' requires the value to be an integer not less than one') + + G.nthreads = n + 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) + + # 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)) + + +# Time step stability factor +class TimeStepStabilityFactor(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 7 + + def __str__(self): + + try: + return '#time_step_stability_factor: {}'.format(self.kwargs['f']) + except KeyError: + return '#time_step_stability_factor:' + + def create(self, G, uip): + + try: + f = self.kwargs['f'] + except KeyError: + raise CmdInputError(self.__str__() + ' requires exactly one parameter') + + if f <= 0 or f > 1: + 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)) + + +class PMLCells(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 8 + + def create(self, G, uip): + + try: + thickness = self.kwargs['thickness'] + + for key in G.pmlthickness.keys(): + G.pmlthickness[key] = int(thickness) + + except KeyError: + try: + G.pmlthickness['x0'] = int(self.kwargs['x0']) + G.pmlthickness['y0'] = int(self.kwargs['y0']) + G.pmlthickness['z0'] = int(self.kwargs['z0']) + G.pmlthickness['xmax'] = int(self.kwargs['xmax']) + G.pmlthickness['ymax'] = int(self.kwargs['ymax']) + G.pmlthickness['zmax'] = int(self.kwargs['zmax']) + except KeyError: + raise CmdInputError('#pml_cells: requires either one or six parameter(s)') + + if (2 * G.pmlthickness['x0'] >= G.nx or + 2 * G.pmlthickness['y0'] >= G.ny or + 2 * G.pmlthickness['z0'] >= G.nz or + 2 * G.pmlthickness['xmax'] >= G.nx or + 2 * G.pmlthickness['ymax'] >= G.ny or + 2 * G.pmlthickness['zmax'] >= G.nz): + raise CmdInputError('#pml_thickness: has too many cells for the domain size') + + +class SrcSteps(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 9 + + def create(self, G, uip): + try: + G.srcsteps = uip.discretise_point(self.kwargs['p1']) + except KeyError: + raise CmdInputError('#src_steps: requires exactly three parameters') + # src_steps + 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)) + + +class RxSteps(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 10 + + def create(self, G, uip): + try: + G.rxsteps = uip.discretise_point(self.kwargs['p1']) + except KeyError: + 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)) + + +class ExcitationFile(UserObjectSingle): + + def create(self, G, uip): + # Excitation file for user-defined source waveforms + try: + kwargs = dict() + excitationfile = self.kwargs['filepath'] + kwargs['kind'] = self.kwargs['kind'] + kwargs['fill_value'] = self.kwargs['fill_value'] + + except KeyError: + try: + excitationfile = self.kwargs['filepath'] + args, varargs, keywords, defaults = inspect.getargspec(interpolate.interp1d) + kwargs = dict(zip(reversed(args), reversed(defaults))) + except KeyError: + raise CmdInputError('#excitation_file: requires either one or three parameter(s)') + + # See if file exists at specified path and if not try input file directory + if not os.path.isfile(excitationfile): + excitationfile = os.path.abspath(os.path.join(G.inputdirectory, excitationfile)) + + if config.is_messages(): + print('\nExcitation file: {}'.format(excitationfile)) + + # Get waveform names + with open(excitationfile, 'r') as f: + waveformIDs = f.readline().split() + + # Read all waveform values into an array + waveformvalues = np.loadtxt(excitationfile, skiprows=1, dtype=floattype) + + # Time array (if specified) for interpolation, otherwise use simulation time + if waveformIDs[0].lower() == 'time': + waveformIDs = waveformIDs[1:] + waveformtime = waveformvalues[:, 0] + waveformvalues = waveformvalues[:, 1:] + timestr = 'user-defined time array' + else: + waveformtime = np.arange(0, G.timewindow + G.dt, G.dt) + timestr = 'simulation time array' + + for waveform in range(len(waveformIDs)): + if any(x.ID == waveformIDs[waveform] for x in G.waveforms): + raise CmdInputError('Waveform with ID {} already exists'.format(waveformIDs[waveform])) + w = Waveform() + w.ID = waveformIDs[waveform] + w.type = 'user' + + # Select correct column of waveform values depending on array shape + singlewaveformvalues = waveformvalues[:] if len(waveformvalues.shape) == 1 else waveformvalues[:, waveform] + + # Truncate waveform array if it is longer than time array + if len(singlewaveformvalues) > len(waveformtime): + singlewaveformvalues = singlewaveformvalues[:len(waveformtime)] + # Zero-pad end of waveform array if it is shorter than time array + elif len(singlewaveformvalues) < len(waveformtime): + singlewaveformvalues = np.lib.pad(singlewaveformvalues, (0, len(singlewaveformvalues) - len(waveformvalues)), 'constant', constant_values=0) + + # Interpolate waveform values + 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'])) + + G.waveforms.append(w) + + +class OutputDir(UserObjectSingle): + + def __init__(self, **kwargs): + # dont need to define parameters in advance. Just catch errors + # when they occur + super().__init__(**kwargs) + self.order = 11 + + def create(self, grid, uip): + grid.outputdirectory = self.kwargs['dir'] + + +class NumberOfModelRuns(UserObjectSingle): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.order = 12 + + def create(self, grid, uip): + try: + grid.numberofmodelruns = self.kwargs['n'] + except KeyError: + raise CmdInputError('#numberofmodelruns: requires exactly one parameter') diff --git a/gprMax/config.py b/gprMax/config.py index 1e6879bf..ad5f54b9 100644 --- a/gprMax/config.py +++ b/gprMax/config.py @@ -1,267 +1,267 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -import os - -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 -import sys - -from .utilities import get_terminal_width -from .utilities import get_host_info -from pathlib import Path - -from colorama import init -from colorama import Fore -from colorama import Style -init() - - -# Impedance of free space (Ohms) -z0 = np.sqrt(m0 / e0) - -# General setting 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 -# 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} - - -def is_messages(): - """Function to return messages.""" - return general['messages'] - - -# Store information about host machine -hostinfo = get_host_info() - -# Store information for CUDA solver type -# gpus: information about any GPUs as a list of GPU objects -# snapsgpu2cpu: copy snapshot data from GPU to CPU during simulation -# N.B. This will happen if the requested snapshots are too large to fit -# 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 -# 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' - -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'} - - -class ModelConfig(): - - def __init__(self, sim_config, i): - self.sim_config = sim_config - self.reuse_geometry = False - - # current model number (indexed from 0) - self.i = i - - parts = self.sim_config.output_file_path.parts - - if not sim_config.single_model: - # 1 indexed - self.appendmodelnumber = str(self.i + 1) - else: - self.appendmodelnumber = '' - - # outputfilepath for specific model - self.output_file_path = Path(*parts[:-2], parts[-1] + self.appendmodelnumber) - self.output_file_path_ext = self.output_file_path.with_suffix('.out') - - # make a snapshot directory - stem = parts[-1] + '_snaps' + self.appendmodelnumber - self.snapshot_dir = Path(*parts[:-2], stem) - - inputfilestr_f = '\n--- Model {}/{}, input file: {}' - self.inputfilestr = inputfilestr_f.format(self.i + 1, self.sim_config.model_end, self.sim_config.input_file_path) - # string to print at start of each model run - self.next_model = Fore.GREEN + '{} {}\n'.format(self.inputfilestr, '-' * (get_terminal_width() - 1 - len(self.inputfilestr))) + Style.RESET_ALL - - # Add the current model run to namespace that can be accessed by - # user in any Python code blocks in input file - #self.usernamespace['current_model_run'] = self.i + 1 - - 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, - 'number_model_runs': self.sim_config.model_end + 1, - 'current_model_run': self.i + 1, - 'inputfile': self.sim_config.input_file_path.resolve()} - - -class SimulationConfig: - - def __init__(self, args): - """Adapter for args into Simulation level configuration""" - - # adapt the arg properties to link smoothly with MPIRunner(), CPURunner() etc.. - - # args.inputfile - # args.n - # args.task - # args.restart - # args.mpi - # args.mpi_no_spawn - # args.mpicomm - # args.gpu - # args.benchmark - # args.geometry_only - # args.geometry_fixed - # args.write_processed - - 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 - - # subgrid parameter may not exist if user uses CLI api - try: - self.subgrid = args.subgrid - except AttributeError: - # this must be CLI user. No subgrids are available - self.subgrid = False - - # scenes parameter may not exist if user uses CLI api - try: - self.scenes = args.scenes - except AttributeError: - self.scenes = [] - - # set more complex parameters - self.set_input_file_path() - self.set_output_file_path() - self.set_model_start_end() - self.set_single_model() - - def set_single_model(self): - if self.model_start == 0 and self.model_end == 1: - self.single_model = True - else: - self.single_model = False - - # for example - def set_model_start_end(self): - - # 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 - modelend = self.args.task - elif self.args.restart: - modelstart = self.args.restart - 1 - modelend = modelstart + self.args.n - 1 - else: - modelstart = 0 - modelend = modelstart + self.args.n - - self.model_start = modelstart - self.model_end = modelend - - def set_precision(self): - pass - - def set_input_file_path(self): - """Function to set to inputfile path""" - - # 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 can be provided by the user. if they havent provided None - # use the inputfilefile path instead - try: - self.output_file_path = Path(self.args.outputfile) - except AttributeError: - self.output_file_path = Path(self.args.inputfile) - - -class SimulationConfigMPI(SimulationConfig): - - def __init__(self, args): - super().__init__(args) - - def set_model_start_end(self): - # Set range for number of models to run - self.model_start = self.args.restart if self.args.restart else 1 - self.model_end = self.modelstart + self.args.n - - -def create_simulation_config(args): - - if not args.mpi and not args.mpi_no_spawn: - sc = SimulationConfig(args) - elif args.mpi: - sc = SimulationConfigMPI(args) - return sc - - -def create_model_config(sim_config, i): - mc = ModelConfig(sim_config, i) - return mc +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import os + +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 +import sys + +from .utilities import get_terminal_width +from .utilities import get_host_info +from pathlib import Path + +from colorama import init +from colorama import Fore +from colorama import Style +init() + + +# Impedance of free space (Ohms) +z0 = np.sqrt(m0 / e0) + +# General setting 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 +# 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} + + +def is_messages(): + """Function to return messages.""" + return general['messages'] + + +# Store information about host machine +hostinfo = get_host_info() + +# Store information for CUDA solver type +# gpus: information about any GPUs as a list of GPU objects +# snapsgpu2cpu: copy snapshot data from GPU to CPU during simulation +# N.B. This will happen if the requested snapshots are too large to fit +# 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 +# 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' + +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'} + + +class ModelConfig(): + + def __init__(self, sim_config, i): + self.sim_config = sim_config + self.reuse_geometry = False + + # current model number (indexed from 0) + self.i = i + + parts = self.sim_config.output_file_path.parts + + if not sim_config.single_model: + # 1 indexed + self.appendmodelnumber = str(self.i + 1) + else: + self.appendmodelnumber = '' + + # outputfilepath for specific model + self.output_file_path = Path(*parts[:-2], parts[-1] + self.appendmodelnumber) + self.output_file_path_ext = self.output_file_path.with_suffix('.out') + + # make a snapshot directory + stem = parts[-1] + '_snaps' + self.appendmodelnumber + self.snapshot_dir = Path(*parts[:-2], stem) + + inputfilestr_f = '\n--- Model {}/{}, input file: {}' + self.inputfilestr = inputfilestr_f.format(self.i + 1, self.sim_config.model_end, self.sim_config.input_file_path) + # string to print at start of each model run + self.next_model = Fore.GREEN + '{} {}\n'.format(self.inputfilestr, '-' * (get_terminal_width() - 1 - len(self.inputfilestr))) + Style.RESET_ALL + + # Add the current model run to namespace that can be accessed by + # user in any Python code blocks in input file + #self.usernamespace['current_model_run'] = self.i + 1 + + 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, + 'number_model_runs': self.sim_config.model_end + 1, + 'current_model_run': self.i + 1, + 'inputfile': self.sim_config.input_file_path.resolve()} + + +class SimulationConfig: + + def __init__(self, args): + """Adapter for args into Simulation level configuration""" + + # adapt the arg properties to link smoothly with MPIRunner(), CPURunner() etc.. + + # args.inputfile + # args.n + # args.task + # args.restart + # args.mpi + # args.mpi_no_spawn + # args.mpicomm + # args.gpu + # args.benchmark + # args.geometry_only + # args.geometry_fixed + # args.write_processed + + 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 + + # subgrid parameter may not exist if user uses CLI api + try: + self.subgrid = args.subgrid + except AttributeError: + # this must be CLI user. No subgrids are available + self.subgrid = False + + # scenes parameter may not exist if user uses CLI api + try: + self.scenes = args.scenes + except AttributeError: + self.scenes = [] + + # set more complex parameters + self.set_input_file_path() + self.set_output_file_path() + self.set_model_start_end() + self.set_single_model() + + def set_single_model(self): + if self.model_start == 0 and self.model_end == 1: + self.single_model = True + else: + self.single_model = False + + # for example + def set_model_start_end(self): + + # 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 + modelend = self.args.task + elif self.args.restart: + modelstart = self.args.restart - 1 + modelend = modelstart + self.args.n - 1 + else: + modelstart = 0 + modelend = modelstart + self.args.n + + self.model_start = modelstart + self.model_end = modelend + + def set_precision(self): + pass + + def set_input_file_path(self): + """Function to set to inputfile path""" + + # 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 can be provided by the user. if they havent provided None + # use the inputfilefile path instead + try: + self.output_file_path = Path(self.args.outputfile) + except AttributeError: + self.output_file_path = Path(self.args.inputfile) + + +class SimulationConfigMPI(SimulationConfig): + + def __init__(self, args): + super().__init__(args) + + def set_model_start_end(self): + # Set range for number of models to run + self.model_start = self.args.restart if self.args.restart else 1 + self.model_end = self.modelstart + self.args.n + + +def create_simulation_config(args): + + if not args.mpi and not args.mpi_no_spawn: + sc = SimulationConfig(args) + elif args.mpi: + sc = SimulationConfigMPI(args) + return sc + + +def create_model_config(sim_config, i): + mc = ModelConfig(sim_config, i) + return mc diff --git a/gprMax/contexts.py b/gprMax/contexts.py index ca480800..04842070 100644 --- a/gprMax/contexts.py +++ b/gprMax/contexts.py @@ -1,119 +1,119 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -from .utilities import get_terminal_width -from .utilities import timer -from .model_build_run import ModelBuildRun -import datetime -from .config import create_model_config -from .solvers import create_solver -from .solvers import create_G - - -class Context(): - - def __init__(self, sim_config): - """Context for the model to run in. Sub-class this with contexts - i.e. an MPI context. - - Args: - sim_config (SimConfig): Simulation level configuration object. - solver (Solver): FDTD general solver object. - """ - self.sim_config = sim_config - self.model_range = range(sim_config.model_start, - sim_config.model_end) - self.tsimend = 0 - self.tsimstart = 1 - - def run(self): - """Function to run the simulation in the correct context.""" - self.tsimstart = timer() - self._run() - self.tsimend = timer() - - def print_time_report(self): - """Function to print the total simulation time based on context.""" - s = self.make_time_report(sim_time) - print(s) - - def make_time_report(self): - """Function to generate a string for the total simulation time bas""" - pass - - -class NoMPIContext(Context): - - def _run(self): - """Specialise how the models are farmed out.""" - - for i in self.model_range: - model_config = create_model_config(self.sim_config, i) - - # always create a solver for the first model - # the next model to run only gets a new solver if the - # geometry is not re used. - if i != 0 and self.sim_config.geometry_fixed: - model_config.reuse_geometry = True - else: - G = create_G(self.sim_config) - - model = ModelBuildRun(G, self.sim_config, model_config) - model.build() - - solver = create_solver(G, self.sim_config) - - if not self.sim_config.geometry_only: - model.run_model(solver) - - def make_time_report(self): - """Function to specialise the time reporting for the standard Simulation - 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))) - - -class MPIContext(Context): - - def _run(self): - pass - - def make_time_report(self): - pass - - -class MPINoSpawnContext(Context): - - def _run(self): - pass - - def make_time_report(self): - pass - - -def create_context(sim_config): - """Create a context in which to run the simulation. i.e MPI.""" - if sim_config.mpi_no_spawn: - context = MPIContext(sim_config) - elif sim_config.mpi: - context = MPINoSpawnContext(sim_config) - else: - context = NoMPIContext(sim_config) - - return context +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +from .utilities import get_terminal_width +from .utilities import timer +from .model_build_run import ModelBuildRun +import datetime +from .config import create_model_config +from .solvers import create_solver +from .solvers import create_G + + +class Context(): + + def __init__(self, sim_config): + """Context for the model to run in. Sub-class this with contexts + i.e. an MPI context. + + Args: + sim_config (SimConfig): Simulation level configuration object. + solver (Solver): FDTD general solver object. + """ + self.sim_config = sim_config + self.model_range = range(sim_config.model_start, + sim_config.model_end) + self.tsimend = 0 + self.tsimstart = 1 + + def run(self): + """Function to run the simulation in the correct context.""" + self.tsimstart = timer() + self._run() + self.tsimend = timer() + + def print_time_report(self): + """Function to print the total simulation time based on context.""" + s = self.make_time_report(sim_time) + print(s) + + def make_time_report(self): + """Function to generate a string for the total simulation time bas""" + pass + + +class NoMPIContext(Context): + + def _run(self): + """Specialise how the models are farmed out.""" + + for i in self.model_range: + model_config = create_model_config(self.sim_config, i) + + # always create a solver for the first model + # the next model to run only gets a new solver if the + # geometry is not re used. + if i != 0 and self.sim_config.geometry_fixed: + model_config.reuse_geometry = True + else: + G = create_G(self.sim_config) + + model = ModelBuildRun(G, self.sim_config, model_config) + model.build() + + solver = create_solver(G, self.sim_config) + + if not self.sim_config.geometry_only: + model.run_model(solver) + + def make_time_report(self): + """Function to specialise the time reporting for the standard Simulation + 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))) + + +class MPIContext(Context): + + def _run(self): + pass + + def make_time_report(self): + pass + + +class MPINoSpawnContext(Context): + + def _run(self): + pass + + def make_time_report(self): + pass + + +def create_context(sim_config): + """Create a context in which to run the simulation. i.e MPI.""" + if sim_config.mpi_no_spawn: + context = MPIContext(sim_config) + elif sim_config.mpi: + context = MPINoSpawnContext(sim_config) + else: + context = NoMPIContext(sim_config) + + return context diff --git a/gprMax/cython/fields_updates_hsg.pyx b/gprMax/cython/fields_updates_hsg.pyx index 7c56b42a..ea0379ff 100644 --- a/gprMax/cython/fields_updates_hsg.pyx +++ b/gprMax/cython/fields_updates_hsg.pyx @@ -1,281 +1,281 @@ -# Copyright (C) 2015-2017: 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -import 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, - int face, - int l_l, - int l_u, - int m_l, - int m_u, - size_t n_l, - size_t n_u, - int nwn, - size_t lookup_id, - np.float64_t[:, :, :] field, - np.float64_t[:, :, :] inc_field, - size_t co, - int sign_n, - int sign_f, - int mid, - int r, - int s, - int nb, - int nthreads -): - """ - Args: - - subgrid: (Subgrid) - n: (String) the normal to the face to update - nwn: (Int) number of working cell in the normal direction - to the face - lookup_id: (Int) id of the H component we wish to update at - each node - field: (Numpy array) main grid field to be updated - inc_field: (Numpy array) incident sub_grid field - co: (Int) Coefficient used by gprMax update equations which - is specific to the field component being updated. - sign_n: (Int) 1 or -1 sign of the incident field on the near face. - sign_f: (Int) 1 or -1 sign of the incident field on the far face. - mid: (Bool) is the H node midway along the lower edge? - r = self.ratio - s = self.is_os_sep - nb = self.n_boundary_cells - """ - # Comments here as as per left and right face - - cdef Py_ssize_t l, m, l_s, m_s, n_s_l, n_s_r, material_e_l, material_e_r, i0, j0, k0, i1, j1, k1, i2, j2, k2, i3, j3, k3 - cdef int os - cdef double inc_n, inc_f - - # surface normal index for the subgrid near face h nodes (left i index) - n_s_l = nb - s * r - r + r // 2 - # surface normal index for the subgrid far face h nodes (right i index) - n_s_r = nb + nwn + s * r + r // 2 - # OS at the left face - os = nb - r * s - - # Iterate over a slice of the main grid using dummy indices - for l in prange(l_l, l_u, nogil=True, schedule='static', num_threads=nthreads): - - # Calculate the subgrid j component of the H nodes - # i.e. Hz node of the left or right face - if mid == 1: - l_s = os + (l - l_l) * r + r // 2 - # i.e. the Hy node of the left or right face - else: - l_s = os + (l - l_l) * r - - for m in range(m_l, m_u): - - # Calculate the subgrid k component of the H nodes - if mid == 1: - m_s = os + (m - m_l) * r - else: - m_s = os + (m - m_l) * r + r // 2 - - # left and right - if face == 2: - # main grid index - i0, j0, k0 = n_l, l, m - # equivalent subgrid index - i1, j1, k1 = n_s_l, l_s, m_s - i2, j2, k2 = n_u, l, m - i3, j3, k3 = n_s_r, l_s, m_s - # front and back - if face == 3: - i0, j0, k0 = l, n_l, m - i1, j1, k1 = l_s, n_s_l, m_s - i2, j2, k2 = l, n_u, m - i3, j3, k3 = l_s, n_s_r, m_s - # top bottom - if face == 1: - i0, j0, k0 = l, m, n_l - i1, j1, k1 = l_s, m_s, n_s_l - i2, j2, k2 = l, m, n_u - i3, j3, k3 = l_s, m_s, n_s_r - # Update the left face - - # Get the material at main grid index - material_e_l = ID[lookup_id, i0, j0, k0] - # Get the associated indident field from the subgrid - inc_n = inc_field[i1, j1, k1] * sign_n - # Update the main grid E field with the corrected H field - field[i0, j0, k0] += updatecoeffsE[material_e_l, co] * inc_n - - # Update the right face - material_e_r = ID[lookup_id, i2, j2, k2] - inc_f = inc_field[i3, j3, k3] * sign_f - field[i2, j2, k2] += updatecoeffsE[material_e_r, co] * inc_f - -cpdef void cython_update_magnetic_os( - np.float64_t[:, :] updatecoeffsH, - np.uint32_t[:, :, :, :] ID, - int face, - int l_l, - int l_u, - int m_l, - int m_u, - size_t n_l, - size_t n_u, - int nwn, - size_t lookup_id, - np.float64_t[:, :, :] field, - np.float64_t[:, :, :] inc_field, - size_t co, - int sign_n, - int sign_f, - int mid, - int r, - int s, - int nb, - int nthreads -): - """ - int r ratio, - int s is_os_sep, - int nb n_boundary_cells - """ - - cdef Py_ssize_t l, m, l_s, m_s, n_s_l, n_s_r, material_e_l, material_e_r, i0, j0, k0, i1, j1, k1, i2, j2, k2, i3, j3, k3 - cdef int os - cdef double inc_n, inc_f - - # i index (normal to os) for the subgrid near face e node - n_s_l = nb - r * s - # Normal index for the subgrid far face e node - n_s_r = nb + nwn + s * r - - # os inner index for the sub grid - os = nb - r * s - - for l in prange(l_l, l_u, nogil=True, schedule='static', num_threads=nthreads): - - # y coord of the Ex field component - if mid == 1: - l_s = os + (l - l_l) * r + r // 2 - # y coord of the Ez field component - else: - l_s = os + (l - l_l) * r - - for m in range(m_l, m_u): - - # z coordinate of the Ex node in the subgrid - if mid == 1: - m_s = os + (m - m_l) * r - else: - m_s = os + (m - m_l) * r + r // 2 - - # associate the given indices with their i, j, k values - - # left and right - if face == 2: - # main grid index - i0, j0, k0 = n_l, l, m - # equivalent subgrid index - i1, j1, k1 = n_s_l, l_s, m_s - i2, j2, k2 = n_u, l, m - i3, j3, k3 = n_s_r, l_s, m_s - # front and back - if face == 3: - i0, j0, k0 = l, n_l, m - i1, j1, k1 = l_s, n_s_l, m_s - i2, j2, k2 = l, n_u, m - i3, j3, k3 = l_s, n_s_r, m_s - # top bottom - if face == 1: - i0, j0, k0 = l, m, n_l - i1, j1, k1 = l_s, m_s, n_s_l - i2, j2, k2 = l, m, n_u - i3, j3, k3 = l_s, m_s, n_s_r - - material_e_l = ID[lookup_id, i0, j0, k0] - inc_n = inc_field[i1, j1, k1] * sign_n - - # make sure these are the correct grid - field[i0, j0, k0] += updatecoeffsH[material_e_l, co] * inc_n - - # Far face - material_e_r = ID[lookup_id, i2, j2, k2] - inc_f = inc_field[i3, j3, k3] * sign_f - field[i2, j2, k2] += updatecoeffsH[material_e_r, co] * inc_f - -cpdef void cython_update_is( - int nwx, - int nwy, - int nwz, - np.float64_t[:, :] updatecoeffsE, - np.uint32_t[:, :, :, :] ID, - int n, - int offset, - int nwl, - int nwm, - int nwn, - int face, - np.float64_t[:, :, :] field, - np.float64_t[:, :] inc_field_l, - np.float64_t[:, :] inc_field_u, - Py_ssize_t lookup_id, - int sign_l, - int sign_u, - Py_ssize_t co, - int nthreads - ): - - cdef Py_ssize_t l, m, i1, j1, k1, i2, j2, k2, field_material_l, field_material_u, inc_i, inc_j - cdef double inc_l, inc_u, f_l, f_u - # for inner faces H nodes are 1 cell before n boundary cells - cdef int n_o = n + offset - - for l in prange(n, nwl + n, nogil=True, schedule='static', num_threads=nthreads): - for m in range(n, nwm + n): - - # bottom and top - if face == 1: - i1, j1, k1 = l, m, n_o - i2, j2, k2 = l, m, n + nwz - # left and right - if face == 2: - i1, j1, k1 = n_o, l, m - i2, j2, k2 = n + nwx, l, m - # front and back - if face == 3: - i1, j1, k1 = l, n_o, m - i2, j2, k2 = l, n + nwy, m - - inc_i = l - n - inc_j = m - n - - field_material_l = ID[lookup_id, i1, j1, k1] - inc_l = inc_field_l[inc_i, inc_j] - # Additional field at i, j, k - f_l = updatecoeffsE[field_material_l, co] * inc_l * sign_l - # Set the new value - field[i1, j1, k1] += f_l - - field_material_u = ID[lookup_id, i2, j2, k2] - inc_u = inc_field_u[inc_i, inc_j] - # Additional field at i, j, k - f_u = updatecoeffsE[field_material_u, co] * inc_u * sign_u - # Set the new value - field[i2, j2, k2] += f_u +# Copyright (C) 2015-2017: 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +import 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, + int face, + int l_l, + int l_u, + int m_l, + int m_u, + size_t n_l, + size_t n_u, + int nwn, + size_t lookup_id, + np.float64_t[:, :, :] field, + np.float64_t[:, :, :] inc_field, + size_t co, + int sign_n, + int sign_f, + int mid, + int r, + int s, + int nb, + int nthreads +): + """ + Args: + + subgrid: (Subgrid) + n: (String) the normal to the face to update + nwn: (Int) number of working cell in the normal direction + to the face + lookup_id: (Int) id of the H component we wish to update at + each node + field: (Numpy array) main grid field to be updated + inc_field: (Numpy array) incident sub_grid field + co: (Int) Coefficient used by gprMax update equations which + is specific to the field component being updated. + sign_n: (Int) 1 or -1 sign of the incident field on the near face. + sign_f: (Int) 1 or -1 sign of the incident field on the far face. + mid: (Bool) is the H node midway along the lower edge? + r = self.ratio + s = self.is_os_sep + nb = self.n_boundary_cells + """ + # Comments here as as per left and right face + + cdef Py_ssize_t l, m, l_s, m_s, n_s_l, n_s_r, material_e_l, material_e_r, i0, j0, k0, i1, j1, k1, i2, j2, k2, i3, j3, k3 + cdef int os + cdef double inc_n, inc_f + + # surface normal index for the subgrid near face h nodes (left i index) + n_s_l = nb - s * r - r + r // 2 + # surface normal index for the subgrid far face h nodes (right i index) + n_s_r = nb + nwn + s * r + r // 2 + # OS at the left face + os = nb - r * s + + # Iterate over a slice of the main grid using dummy indices + for l in prange(l_l, l_u, nogil=True, schedule='static', num_threads=nthreads): + + # Calculate the subgrid j component of the H nodes + # i.e. Hz node of the left or right face + if mid == 1: + l_s = os + (l - l_l) * r + r // 2 + # i.e. the Hy node of the left or right face + else: + l_s = os + (l - l_l) * r + + for m in range(m_l, m_u): + + # Calculate the subgrid k component of the H nodes + if mid == 1: + m_s = os + (m - m_l) * r + else: + m_s = os + (m - m_l) * r + r // 2 + + # left and right + if face == 2: + # main grid index + i0, j0, k0 = n_l, l, m + # equivalent subgrid index + i1, j1, k1 = n_s_l, l_s, m_s + i2, j2, k2 = n_u, l, m + i3, j3, k3 = n_s_r, l_s, m_s + # front and back + if face == 3: + i0, j0, k0 = l, n_l, m + i1, j1, k1 = l_s, n_s_l, m_s + i2, j2, k2 = l, n_u, m + i3, j3, k3 = l_s, n_s_r, m_s + # top bottom + if face == 1: + i0, j0, k0 = l, m, n_l + i1, j1, k1 = l_s, m_s, n_s_l + i2, j2, k2 = l, m, n_u + i3, j3, k3 = l_s, m_s, n_s_r + # Update the left face + + # Get the material at main grid index + material_e_l = ID[lookup_id, i0, j0, k0] + # Get the associated indident field from the subgrid + inc_n = inc_field[i1, j1, k1] * sign_n + # Update the main grid E field with the corrected H field + field[i0, j0, k0] += updatecoeffsE[material_e_l, co] * inc_n + + # Update the right face + material_e_r = ID[lookup_id, i2, j2, k2] + inc_f = inc_field[i3, j3, k3] * sign_f + field[i2, j2, k2] += updatecoeffsE[material_e_r, co] * inc_f + +cpdef void cython_update_magnetic_os( + np.float64_t[:, :] updatecoeffsH, + np.uint32_t[:, :, :, :] ID, + int face, + int l_l, + int l_u, + int m_l, + int m_u, + size_t n_l, + size_t n_u, + int nwn, + size_t lookup_id, + np.float64_t[:, :, :] field, + np.float64_t[:, :, :] inc_field, + size_t co, + int sign_n, + int sign_f, + int mid, + int r, + int s, + int nb, + int nthreads +): + """ + int r ratio, + int s is_os_sep, + int nb n_boundary_cells + """ + + cdef Py_ssize_t l, m, l_s, m_s, n_s_l, n_s_r, material_e_l, material_e_r, i0, j0, k0, i1, j1, k1, i2, j2, k2, i3, j3, k3 + cdef int os + cdef double inc_n, inc_f + + # i index (normal to os) for the subgrid near face e node + n_s_l = nb - r * s + # Normal index for the subgrid far face e node + n_s_r = nb + nwn + s * r + + # os inner index for the sub grid + os = nb - r * s + + for l in prange(l_l, l_u, nogil=True, schedule='static', num_threads=nthreads): + + # y coord of the Ex field component + if mid == 1: + l_s = os + (l - l_l) * r + r // 2 + # y coord of the Ez field component + else: + l_s = os + (l - l_l) * r + + for m in range(m_l, m_u): + + # z coordinate of the Ex node in the subgrid + if mid == 1: + m_s = os + (m - m_l) * r + else: + m_s = os + (m - m_l) * r + r // 2 + + # associate the given indices with their i, j, k values + + # left and right + if face == 2: + # main grid index + i0, j0, k0 = n_l, l, m + # equivalent subgrid index + i1, j1, k1 = n_s_l, l_s, m_s + i2, j2, k2 = n_u, l, m + i3, j3, k3 = n_s_r, l_s, m_s + # front and back + if face == 3: + i0, j0, k0 = l, n_l, m + i1, j1, k1 = l_s, n_s_l, m_s + i2, j2, k2 = l, n_u, m + i3, j3, k3 = l_s, n_s_r, m_s + # top bottom + if face == 1: + i0, j0, k0 = l, m, n_l + i1, j1, k1 = l_s, m_s, n_s_l + i2, j2, k2 = l, m, n_u + i3, j3, k3 = l_s, m_s, n_s_r + + material_e_l = ID[lookup_id, i0, j0, k0] + inc_n = inc_field[i1, j1, k1] * sign_n + + # make sure these are the correct grid + field[i0, j0, k0] += updatecoeffsH[material_e_l, co] * inc_n + + # Far face + material_e_r = ID[lookup_id, i2, j2, k2] + inc_f = inc_field[i3, j3, k3] * sign_f + field[i2, j2, k2] += updatecoeffsH[material_e_r, co] * inc_f + +cpdef void cython_update_is( + int nwx, + int nwy, + int nwz, + np.float64_t[:, :] updatecoeffsE, + np.uint32_t[:, :, :, :] ID, + int n, + int offset, + int nwl, + int nwm, + int nwn, + int face, + np.float64_t[:, :, :] field, + np.float64_t[:, :] inc_field_l, + np.float64_t[:, :] inc_field_u, + Py_ssize_t lookup_id, + int sign_l, + int sign_u, + Py_ssize_t co, + int nthreads + ): + + cdef Py_ssize_t l, m, i1, j1, k1, i2, j2, k2, field_material_l, field_material_u, inc_i, inc_j + cdef double inc_l, inc_u, f_l, f_u + # for inner faces H nodes are 1 cell before n boundary cells + cdef int n_o = n + offset + + for l in prange(n, nwl + n, nogil=True, schedule='static', num_threads=nthreads): + for m in range(n, nwm + n): + + # bottom and top + if face == 1: + i1, j1, k1 = l, m, n_o + i2, j2, k2 = l, m, n + nwz + # left and right + if face == 2: + i1, j1, k1 = n_o, l, m + i2, j2, k2 = n + nwx, l, m + # front and back + if face == 3: + i1, j1, k1 = l, n_o, m + i2, j2, k2 = l, n + nwy, m + + inc_i = l - n + inc_j = m - n + + field_material_l = ID[lookup_id, i1, j1, k1] + inc_l = inc_field_l[inc_i, inc_j] + # Additional field at i, j, k + f_l = updatecoeffsE[field_material_l, co] * inc_l * sign_l + # Set the new value + field[i1, j1, k1] += f_l + + field_material_u = ID[lookup_id, i2, j2, k2] + inc_u = inc_field_u[inc_i, inc_j] + # Additional field at i, j, k + f_u = updatecoeffsE[field_material_u, co] * inc_u * sign_u + # Set the new value + field[i2, j2, k2] += f_u diff --git a/gprMax/exceptions.py b/gprMax/exceptions.py index 4ec57e02..1beab97b 100644 --- a/gprMax/exceptions.py +++ b/gprMax/exceptions.py @@ -1,48 +1,48 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -import sys - -from colorama import init -from colorama import Fore - -init() - -sys.tracebacklimit = None - - -class GeneralError(ValueError): - """Handles general errors. Subclasses the ValueError class.""" - - def __init__(self, message, *args): - - self.message = message - super(GeneralError, self).__init__(message, *args) - print(Fore.RED) - - -class CmdInputError(ValueError): - """Handles errors in user specified commands. Subclasses the ValueError - class. - """ - - def __init__(self, message, *args): - - self.message = message - super(CmdInputError, self).__init__(message, *args) - print(Fore.RED) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import sys + +from colorama import init +from colorama import Fore + +init() + +sys.tracebacklimit = None + + +class GeneralError(ValueError): + """Handles general errors. Subclasses the ValueError class.""" + + def __init__(self, message, *args): + + self.message = message + super(GeneralError, self).__init__(message, *args) + print(Fore.RED) + + +class CmdInputError(ValueError): + """Handles errors in user specified commands. Subclasses the ValueError + class. + """ + + def __init__(self, message, *args): + + self.message = message + super(CmdInputError, self).__init__(message, *args) + print(Fore.RED) diff --git a/gprMax/fields_outputs.py b/gprMax/fields_outputs.py index de618e41..26a5212f 100644 --- a/gprMax/fields_outputs.py +++ b/gprMax/fields_outputs.py @@ -1,202 +1,202 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -from string import Template -from pathlib import Path - -import h5py - -from ._version import __version__ - - -def store_outputs(G): - """Stores field component values for every receiver and transmission line. - - Args: - iteration (int): Current iteration number. - Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic - field values. - G (class): Grid class instance - holds essential parameters describing - the model. - """ - - iteration = G.iteration - #import pdb; pdb.set_trace() - - Ex, Ey, Ez, Hx, Hy, Hz = G.Ex, G.Ey, G.Ez, G.Hx, G.Hy, G.Hz - for rx in G.rxs: - for output in rx.outputs: - # Store electric or magnetic field components - if 'I' not in output: - field = locals()[output] - rx.outputs[output][iteration] = field[rx.xcoord, rx.ycoord, rx.zcoord] - # Store current component - else: - func = globals()[output] - rx.outputs[output][iteration] = func(rx.xcoord, rx.ycoord, rx.zcoord, - Hx, Hy, Hz, G) - - for tl in G.transmissionlines: - tl.Vtotal[iteration] = tl.voltage[tl.antpos] - tl.Itotal[iteration] = tl.current[tl.antpos] - - -kernel_template_store_outputs = Template(""" - -// Macros for converting subscripts to linear index: -#define INDEX2D_RXCOORDS(m, n) (m)*($NY_RXCOORDS)+(n) -#define INDEX3D_RXS(i, j, k) (i)*($NY_RXS)*($NZ_RXS)+(j)*($NZ_RXS)+(k) -#define INDEX3D_FIELDS(i, j, k) (i)*($NY_FIELDS)*($NZ_FIELDS)+(j)*($NZ_FIELDS)+(k) - -////////////////////////////////////////////////////// -// Stores field component values for every receiver // -////////////////////////////////////////////////////// - -__global__ void store_outputs(int NRX, int iteration, const int* __restrict__ rxcoords, $REAL *rxs, const $REAL* __restrict__ Ex, const $REAL* __restrict__ Ey, const $REAL* __restrict__ Ez, const $REAL* __restrict__ Hx, const $REAL* __restrict__ Hy, const $REAL* __restrict__ Hz) { - - // This function stores field component values for every receiver in the model. - // - // Args: - // NRX: Total number of receivers in the model - // rxs: Array to store field components for receivers - rows are field components; columns are iterations; pages are receivers - // E, H: Access to field component arrays - - // Obtain the linear index corresponding to the current thread and use for each receiver - int rx = blockIdx.x * blockDim.x + threadIdx.x; - - int i, j, k; - - if (rx < NRX) { - i = rxcoords[INDEX2D_RXCOORDS(rx,0)]; - j = rxcoords[INDEX2D_RXCOORDS(rx,1)]; - k = rxcoords[INDEX2D_RXCOORDS(rx,2)]; - rxs[INDEX3D_RXS(0,iteration,rx)] = Ex[INDEX3D_FIELDS(i,j,k)]; - rxs[INDEX3D_RXS(1,iteration,rx)] = Ey[INDEX3D_FIELDS(i,j,k)]; - rxs[INDEX3D_RXS(2,iteration,rx)] = Ez[INDEX3D_FIELDS(i,j,k)]; - rxs[INDEX3D_RXS(3,iteration,rx)] = Hx[INDEX3D_FIELDS(i,j,k)]; - rxs[INDEX3D_RXS(4,iteration,rx)] = Hy[INDEX3D_FIELDS(i,j,k)]; - rxs[INDEX3D_RXS(5,iteration,rx)] = Hz[INDEX3D_FIELDS(i,j,k)]; - } -} - -""") - - -def write_hdf5_outputfile(outputfile, G): - write_hdf5_main_grid_outputfile(outputfile, G) - write_hdf5_sub_grid_outputfile(outputfile, G) - - -def write_hdf5_main_grid_outputfile(outputfile, G): - """Write an output file in HDF5 format. - - Args: - outputfile (str): Name of the output file. - Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - write_data(outputfile, G) - - -def write_hdf5_sub_grid_outputfile(outputfile, G): - """Write an output file in HDF5 format. - - Args: - outputfile (str): Name of the output file. - Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - stem = outputfile.stem - suffix = outputfile.suffix - parent = outputfile.parent - - for sg in G.subgrids: - - # create an outputfile for each subgrid - fp = stem + '_' + sg.name + suffix - fp = parent / Path(fp) - - f = write_data(fp, sg) - - # write some additional meta data about the subgrid - f.attrs['is_os_sep'] = sg.is_os_sep - f.attrs['pml_separation'] = sg.pml_separation - f.attrs['subgrid_pml_thickness'] = sg.pmlthickness['x0'] - f.attrs['filter'] = sg.filter - f.attrs['ratio'] = sg.ratio - f.attrs['interpolation'] = sg.interpolation - - -def write_data(outputfile, G): - """Write an output file in HDF5 format. - - Args: - outputfile (str): Name of the output file. - Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic - field values. - G (class): Grid class instance - holds essential parameters describing - the model. - """ - - f = h5py.File(outputfile, 'w') - f.attrs['gprMax'] = __version__ - f.attrs['Title'] = G.title - f.attrs['Iterations'] = G.iterations - f.attrs['nx_ny_nz'] = (G.nx, G.ny, G.nz) - f.attrs['dx_dy_dz'] = (G.dx, G.dy, G.dz) - f.attrs['dt'] = G.dt - nsrc = len(G.voltagesources + G.hertziandipoles + G.magneticdipoles + G.transmissionlines) - f.attrs['nsrc'] = nsrc - f.attrs['nrx'] = len(G.rxs) - f.attrs['srcsteps'] = G.srcsteps - f.attrs['rxsteps'] = G.rxsteps - - # Create group for sources (except transmission lines); add type and positional data attributes - srclist = G.voltagesources + G.hertziandipoles + G.magneticdipoles - for srcindex, src in enumerate(srclist): - grp = f.create_group('/srcs/src' + str(srcindex + 1)) - grp.attrs['Type'] = type(src).__name__ - grp.attrs['Position'] = (src.xcoord * G.dx, src.ycoord * G.dy, src.zcoord * G.dz) - - # Create group for transmission lines; add positional data, line resistance and - # line discretisation attributes; write arrays for line voltages and currents - for tlindex, tl in enumerate(G.transmissionlines): - grp = f.create_group('/tls/tl' + str(tlindex + 1)) - grp.attrs['Position'] = (tl.xcoord * G.dx, tl.ycoord * G.dy, tl.zcoord * G.dz) - grp.attrs['Resistance'] = tl.resistance - grp.attrs['dl'] = tl.dl - # Save incident voltage and current - grp['Vinc'] = tl.Vinc - grp['Iinc'] = tl.Iinc - # Save total voltage and current - f['/tls/tl' + str(tlindex + 1) + '/Vtotal'] = tl.Vtotal - f['/tls/tl' + str(tlindex + 1) + '/Itotal'] = tl.Itotal - - # Create group, add positional data and write field component arrays for receivers - for rxindex, rx in enumerate(G.rxs): - grp = f.create_group('/rxs/rx' + str(rxindex + 1)) - if rx.ID: - grp.attrs['Name'] = rx.ID - grp.attrs['Position'] = (rx.xcoord * G.dx, rx.ycoord * G.dy, rx.zcoord * G.dz) - - for output in rx.outputs: - f['/rxs/rx' + str(rxindex + 1) + '/' + output] = rx.outputs[output] - - return f +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +from string import Template +from pathlib import Path + +import h5py + +from ._version import __version__ + + +def store_outputs(G): + """Stores field component values for every receiver and transmission line. + + Args: + iteration (int): Current iteration number. + Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic + field values. + G (class): Grid class instance - holds essential parameters describing + the model. + """ + + iteration = G.iteration + #import pdb; pdb.set_trace() + + Ex, Ey, Ez, Hx, Hy, Hz = G.Ex, G.Ey, G.Ez, G.Hx, G.Hy, G.Hz + for rx in G.rxs: + for output in rx.outputs: + # Store electric or magnetic field components + if 'I' not in output: + field = locals()[output] + rx.outputs[output][iteration] = field[rx.xcoord, rx.ycoord, rx.zcoord] + # Store current component + else: + func = globals()[output] + rx.outputs[output][iteration] = func(rx.xcoord, rx.ycoord, rx.zcoord, + Hx, Hy, Hz, G) + + for tl in G.transmissionlines: + tl.Vtotal[iteration] = tl.voltage[tl.antpos] + tl.Itotal[iteration] = tl.current[tl.antpos] + + +kernel_template_store_outputs = Template(""" + +// Macros for converting subscripts to linear index: +#define INDEX2D_RXCOORDS(m, n) (m)*($NY_RXCOORDS)+(n) +#define INDEX3D_RXS(i, j, k) (i)*($NY_RXS)*($NZ_RXS)+(j)*($NZ_RXS)+(k) +#define INDEX3D_FIELDS(i, j, k) (i)*($NY_FIELDS)*($NZ_FIELDS)+(j)*($NZ_FIELDS)+(k) + +////////////////////////////////////////////////////// +// Stores field component values for every receiver // +////////////////////////////////////////////////////// + +__global__ void store_outputs(int NRX, int iteration, const int* __restrict__ rxcoords, $REAL *rxs, const $REAL* __restrict__ Ex, const $REAL* __restrict__ Ey, const $REAL* __restrict__ Ez, const $REAL* __restrict__ Hx, const $REAL* __restrict__ Hy, const $REAL* __restrict__ Hz) { + + // This function stores field component values for every receiver in the model. + // + // Args: + // NRX: Total number of receivers in the model + // rxs: Array to store field components for receivers - rows are field components; columns are iterations; pages are receivers + // E, H: Access to field component arrays + + // Obtain the linear index corresponding to the current thread and use for each receiver + int rx = blockIdx.x * blockDim.x + threadIdx.x; + + int i, j, k; + + if (rx < NRX) { + i = rxcoords[INDEX2D_RXCOORDS(rx,0)]; + j = rxcoords[INDEX2D_RXCOORDS(rx,1)]; + k = rxcoords[INDEX2D_RXCOORDS(rx,2)]; + rxs[INDEX3D_RXS(0,iteration,rx)] = Ex[INDEX3D_FIELDS(i,j,k)]; + rxs[INDEX3D_RXS(1,iteration,rx)] = Ey[INDEX3D_FIELDS(i,j,k)]; + rxs[INDEX3D_RXS(2,iteration,rx)] = Ez[INDEX3D_FIELDS(i,j,k)]; + rxs[INDEX3D_RXS(3,iteration,rx)] = Hx[INDEX3D_FIELDS(i,j,k)]; + rxs[INDEX3D_RXS(4,iteration,rx)] = Hy[INDEX3D_FIELDS(i,j,k)]; + rxs[INDEX3D_RXS(5,iteration,rx)] = Hz[INDEX3D_FIELDS(i,j,k)]; + } +} + +""") + + +def write_hdf5_outputfile(outputfile, G): + write_hdf5_main_grid_outputfile(outputfile, G) + write_hdf5_sub_grid_outputfile(outputfile, G) + + +def write_hdf5_main_grid_outputfile(outputfile, G): + """Write an output file in HDF5 format. + + Args: + outputfile (str): Name of the output file. + Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + write_data(outputfile, G) + + +def write_hdf5_sub_grid_outputfile(outputfile, G): + """Write an output file in HDF5 format. + + Args: + outputfile (str): Name of the output file. + Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + stem = outputfile.stem + suffix = outputfile.suffix + parent = outputfile.parent + + for sg in G.subgrids: + + # create an outputfile for each subgrid + fp = stem + '_' + sg.name + suffix + fp = parent / Path(fp) + + f = write_data(fp, sg) + + # write some additional meta data about the subgrid + f.attrs['is_os_sep'] = sg.is_os_sep + f.attrs['pml_separation'] = sg.pml_separation + f.attrs['subgrid_pml_thickness'] = sg.pmlthickness['x0'] + f.attrs['filter'] = sg.filter + f.attrs['ratio'] = sg.ratio + f.attrs['interpolation'] = sg.interpolation + + +def write_data(outputfile, G): + """Write an output file in HDF5 format. + + Args: + outputfile (str): Name of the output file. + Ex, Ey, Ez, Hx, Hy, Hz (memory view): Current electric and magnetic + field values. + G (class): Grid class instance - holds essential parameters describing + the model. + """ + + f = h5py.File(outputfile, 'w') + f.attrs['gprMax'] = __version__ + f.attrs['Title'] = G.title + f.attrs['Iterations'] = G.iterations + f.attrs['nx_ny_nz'] = (G.nx, G.ny, G.nz) + f.attrs['dx_dy_dz'] = (G.dx, G.dy, G.dz) + f.attrs['dt'] = G.dt + nsrc = len(G.voltagesources + G.hertziandipoles + G.magneticdipoles + G.transmissionlines) + f.attrs['nsrc'] = nsrc + f.attrs['nrx'] = len(G.rxs) + f.attrs['srcsteps'] = G.srcsteps + f.attrs['rxsteps'] = G.rxsteps + + # Create group for sources (except transmission lines); add type and positional data attributes + srclist = G.voltagesources + G.hertziandipoles + G.magneticdipoles + for srcindex, src in enumerate(srclist): + grp = f.create_group('/srcs/src' + str(srcindex + 1)) + grp.attrs['Type'] = type(src).__name__ + grp.attrs['Position'] = (src.xcoord * G.dx, src.ycoord * G.dy, src.zcoord * G.dz) + + # Create group for transmission lines; add positional data, line resistance and + # line discretisation attributes; write arrays for line voltages and currents + for tlindex, tl in enumerate(G.transmissionlines): + grp = f.create_group('/tls/tl' + str(tlindex + 1)) + grp.attrs['Position'] = (tl.xcoord * G.dx, tl.ycoord * G.dy, tl.zcoord * G.dz) + grp.attrs['Resistance'] = tl.resistance + grp.attrs['dl'] = tl.dl + # Save incident voltage and current + grp['Vinc'] = tl.Vinc + grp['Iinc'] = tl.Iinc + # Save total voltage and current + f['/tls/tl' + str(tlindex + 1) + '/Vtotal'] = tl.Vtotal + f['/tls/tl' + str(tlindex + 1) + '/Itotal'] = tl.Itotal + + # Create group, add positional data and write field component arrays for receivers + for rxindex, rx in enumerate(G.rxs): + grp = f.create_group('/rxs/rx' + str(rxindex + 1)) + if rx.ID: + grp.attrs['Name'] = rx.ID + grp.attrs['Position'] = (rx.xcoord * G.dx, rx.ycoord * G.dy, rx.zcoord * G.dz) + + for output in rx.outputs: + f['/rxs/rx' + str(rxindex + 1) + '/' + output] = rx.outputs[output] + + return f diff --git a/gprMax/gprMax.py b/gprMax/gprMax.py index 3830171b..50f12d7a 100644 --- a/gprMax/gprMax.py +++ b/gprMax/gprMax.py @@ -1,94 +1,94 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -from .config import create_simulation_config -from .contexts import create_context -from .solvers import create_solver - -import argparse - -def api( - scenes=None, - id=None, - inputfile=None, - outputfile=None, - n=1, - task=None, - restart=None, - mpi=False, - mpi_no_spawn=False, - mpicomm=None, - gpu=None, - subgrid=None, - benchmark=False, - geometry_only=False, - geometry_fixed=False, - write_processed=False, -): - """If installed as a module this is the entry point.""" - - class ImportArguments: - pass - - args = ImportArguments() - - args.scenes = scenes - args.inputfile = inputfile - args.outputfile = outputfile - args.n = n - args.task = task - args.restart = restart - args.mpi = mpi - args.mpi_no_spawn = mpi_no_spawn - args.mpicomm = mpicomm - args.gpu = gpu - args.subgrid=subgrid - args.benchmark = benchmark - args.geometry_only = geometry_only - args.geometry_fixed = geometry_fixed - args.write_processed = write_processed - - run_main(args) - - -def main(): - """This is the main function for gprMax.""" - - # Parse command line arguments - parser = argparse.ArgumentParser(prog='gprMax', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('inputfile', help='path to, and name of inputfile or file object') - parser.add_argument('-n', default=1, type=int, help='number of times to run the input file, e.g. to create a B-scan') - parser.add_argument('-task', type=int, help='task identifier (model number) for job array on Open Grid Scheduler/Grid Engine (http://gridscheduler.sourceforge.net/index.html)') - parser.add_argument('-restart', type=int, help='model number to restart from, e.g. when creating B-scan') - parser.add_argument('-mpi', type=int, help='number of MPI tasks, i.e. master + workers') - parser.add_argument('--mpi-no-spawn', action='store_true', default=False, help='flag to use MPI without spawn mechanism') - parser.add_argument('--mpi-worker', action='store_true', default=False, help=argparse.SUPPRESS) - parser.add_argument('-gpu', type=int, action='append', nargs='*', help='flag to use Nvidia GPU or option to give list of device ID(s)') - parser.add_argument('-benchmark', action='store_true', default=False, help='flag to switch on benchmarking mode') - parser.add_argument('--geometry-only', action='store_true', default=False, help='flag to only build model and produce geometry file(s)') - parser.add_argument('--geometry-fixed', action='store_true', default=False, help='flag to not reprocess model geometry, e.g. for B-scans where the geometry is fixed') - parser.add_argument('--write-processed', action='store_true', default=False, help='flag to write an input file after any Python code and include commands in the original input file have been processed') - args = parser.parse_args() - - run_main(args) - -def run_main(args): - - sim_config = create_simulation_config(args) - context = create_context(sim_config) - context.run() +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +from .config import create_simulation_config +from .contexts import create_context +from .solvers import create_solver + +import argparse + +def api( + scenes=None, + id=None, + inputfile=None, + outputfile=None, + n=1, + task=None, + restart=None, + mpi=False, + mpi_no_spawn=False, + mpicomm=None, + gpu=None, + subgrid=None, + benchmark=False, + geometry_only=False, + geometry_fixed=False, + write_processed=False, +): + """If installed as a module this is the entry point.""" + + class ImportArguments: + pass + + args = ImportArguments() + + args.scenes = scenes + args.inputfile = inputfile + args.outputfile = outputfile + args.n = n + args.task = task + args.restart = restart + args.mpi = mpi + args.mpi_no_spawn = mpi_no_spawn + args.mpicomm = mpicomm + args.gpu = gpu + args.subgrid=subgrid + args.benchmark = benchmark + args.geometry_only = geometry_only + args.geometry_fixed = geometry_fixed + args.write_processed = write_processed + + run_main(args) + + +def main(): + """This is the main function for gprMax.""" + + # Parse command line arguments + parser = argparse.ArgumentParser(prog='gprMax', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('inputfile', help='path to, and name of inputfile or file object') + parser.add_argument('-n', default=1, type=int, help='number of times to run the input file, e.g. to create a B-scan') + parser.add_argument('-task', type=int, help='task identifier (model number) for job array on Open Grid Scheduler/Grid Engine (http://gridscheduler.sourceforge.net/index.html)') + parser.add_argument('-restart', type=int, help='model number to restart from, e.g. when creating B-scan') + parser.add_argument('-mpi', type=int, help='number of MPI tasks, i.e. master + workers') + parser.add_argument('--mpi-no-spawn', action='store_true', default=False, help='flag to use MPI without spawn mechanism') + parser.add_argument('--mpi-worker', action='store_true', default=False, help=argparse.SUPPRESS) + parser.add_argument('-gpu', type=int, action='append', nargs='*', help='flag to use Nvidia GPU or option to give list of device ID(s)') + parser.add_argument('-benchmark', action='store_true', default=False, help='flag to switch on benchmarking mode') + parser.add_argument('--geometry-only', action='store_true', default=False, help='flag to only build model and produce geometry file(s)') + parser.add_argument('--geometry-fixed', action='store_true', default=False, help='flag to not reprocess model geometry, e.g. for B-scans where the geometry is fixed') + parser.add_argument('--write-processed', action='store_true', default=False, help='flag to write an input file after any Python code and include commands in the original input file have been processed') + args = parser.parse_args() + + run_main(args) + +def run_main(args): + + sim_config = create_simulation_config(args) + context = create_context(sim_config) + context.run() diff --git a/gprMax/grid.py b/gprMax/grid.py index 8f04240e..4fc295c5 100644 --- a/gprMax/grid.py +++ b/gprMax/grid.py @@ -1,501 +1,501 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -from collections import OrderedDict - -from colorama import init -from colorama import Fore -from colorama import Style -init() -import numpy as np -np.seterr(invalid='raise') - -import gprMax.config as config -from .exceptions import GeneralError -from .pml import PML -from .pml import CFS -from .utilities import fft_power -from .utilities import human_size -from .utilities import round_value - -import decimal as d -from scipy.constants import c - - - -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 the entire grid. A convenient - way for accessing regularly used parameters. - """ - - def __init__(self): - self.title = '' - self.memoryusage = 0 - - self.nx = 0 - self.ny = 0 - self.nz = 0 - self.dx = 0 - self.dy = 0 - self.dz = 0 - self.dt = 0 - self.iterations = 0 - self.timewindow = 0 - - # Ordered dictionary required so that PMLs are always updated in the - # same order. The order itself does not matter, however, if must be the - # same from model to model otherwise the numerical precision from adding - # the PML corrections will be different. - self.pmlthickness = OrderedDict((key, 10) for key in PML.boundaryIDs) - self.cfs = [CFS()] - self.pmls = [] - self.pmlformulation = 'HORIPML' - - self.materials = [] - self.mixingmodels = [] - self.averagevolumeobjects = True - self.fractalvolumes = [] - self.geometryviews = [] - self.geometryobjectswrite = [] - self.waveforms = [] - self.voltagesources = [] - self.hertziandipoles = [] - self.magneticdipoles = [] - self.transmissionlines = [] - self.rxs = [] - self.srcsteps = [0, 0, 0] - self.rxsteps = [0, 0, 0] - self.snapshots = [] - self.subgrids = [] - self.gpu = None - self.name = 'Main' - self.outputdirectory = '' - self.iteration = 0 - - def initialise_geometry_arrays(self): - """ - Initialise an array for volumetric material IDs (solid); - boolean arrays for specifying whether materials can have dielectric smoothing (rigid); - and an array for cell edge IDs (ID). - Solid and ID arrays are initialised to free_space (one); - rigid arrays to allow dielectric smoothing (zero). - """ - self.solid = np.ones((self.nx, self.ny, self.nz), dtype=np.uint32) - self.rigidE = np.zeros((12, self.nx, self.ny, self.nz), dtype=np.int8) - self.rigidH = np.zeros((6, self.nx, self.ny, self.nz), dtype=np.int8) - self.ID = np.ones((6, self.nx + 1, self.ny + 1, self.nz + 1), dtype=np.uint32) - self.IDlookup = {'Ex': 0, 'Ey': 1, 'Ez': 2, 'Hx': 3, 'Hy': 4, 'Hz': 5} - - def initialise_field_arrays(self): - """Initialise arrays for the electric and magnetic field components.""" - self.Ex = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) - self.Ey = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) - self.Ez = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) - self.Hx = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) - 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.""" - for g in [self] + self.subgrids: - g.initialise_geometry_arrays() - g.initialise_field_arrays() - - def initialise_std_update_coeff_arrays(self): - """Initialise arrays for storing update coefficients.""" - self.updatecoeffsE = np.zeros((len(self.materials), 5), dtype=config.dtypes['float_or_double']) - self.updatecoeffsH = np.zeros((len(self.materials), 5), dtype=config.dtypes['float_or_double']) - - def initialise_dispersive_arrays(self, dtype): - """Initialise arrays for storing coefficients when there are dispersive materials present.""" - self.Tx = np.zeros((config.materials['maxpoles'], self.nx + 1, self.ny + 1, self.nz + 1), dtype=dtype) - self.Ty = np.zeros((config.materials['maxpoles'], self.nx + 1, self.ny + 1, self.nz + 1), dtype=dtype) - self.Tz = np.zeros((config.materials['maxpoles'], self.nx + 1, self.ny + 1, self.nz + 1), dtype=dtype) - self.updatecoeffsdispersive = np.zeros((len(self.materials), 3 * config.materials['maxpoles']), dtype=dtype) - - def memory_estimate_basic(self): - """Estimate the amount of memory (RAM) required to run a model.""" - - stdoverhead = 50e6 - - solidarray = self.nx * self.ny * self.nz * np.dtype(np.uint32).itemsize - - # 12 x rigidE array components + 6 x rigidH array components - rigidarrays = (12 + 6) * self.nx * self.ny * self.nz * np.dtype(np.int8).itemsize - - # 6 x field arrays + 6 x ID arrays - fieldarrays = (6 + 6) * (self.nx + 1) * (self.ny + 1) * (self.nz + 1) * np.dtype(config.dtypes['float_or_double']).itemsize - - # PML arrays - pmlarrays = 0 - for (k, v) in self.pmlthickness.items(): - if v > 0: - if 'x' in k: - pmlarrays += ((v + 1) * self.ny * (self.nz + 1)) - pmlarrays += ((v + 1) * (self.ny + 1) * self.nz) - pmlarrays += (v * self.ny * (self.nz + 1)) - pmlarrays += (v * (self.ny + 1) * self.nz) - elif 'y' in k: - pmlarrays += (self.nx * (v + 1) * (self.nz + 1)) - pmlarrays += ((self.nx + 1) * (v + 1) * self.nz) - pmlarrays += ((self.nx + 1) * v * self.nz) - pmlarrays += (self.nx * v * (self.nz + 1)) - elif 'z' in k: - pmlarrays += (self.nx * (self.ny + 1) * (v + 1)) - pmlarrays += ((self.nx + 1) * self.ny * (v + 1)) - pmlarrays += ((self.nx + 1) * self.ny * v) - pmlarrays += (self.nx * (self.ny + 1) * v) - - 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 - """ - - # 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))) - - # 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)) - - # 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 gpu_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.""" - - import pycuda.gpuarray as gpuarray - - self.ID_gpu = gpuarray.to_gpu(self.ID) - self.Ex_gpu = gpuarray.to_gpu(self.Ex) - self.Ey_gpu = gpuarray.to_gpu(self.Ey) - self.Ez_gpu = gpuarray.to_gpu(self.Ez) - self.Hx_gpu = gpuarray.to_gpu(self.Hx) - self.Hy_gpu = gpuarray.to_gpu(self.Hy) - self.Hz_gpu = gpuarray.to_gpu(self.Hz) - - def gpu_initialise_dispersive_arrays(self): - """Initialise dispersive material coefficient arrays on GPU.""" - - import pycuda.gpuarray as gpuarray - - self.Tx_gpu = gpuarray.to_gpu(self.Tx) - self.Ty_gpu = gpuarray.to_gpu(self.Ty) - 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 tmy(): - # Ex & Ez components - G.ID[0, :, 0, :] = 0 - G.ID[0, :, 1, :] = 0 - G.ID[2, :, 0, :] = 0 - G.ID[2, :, 1, :] = 0 - - def tmz(): - # Ex & Ey components - G.ID[0, :, :, 0] = 0 - G.ID[0, :, :, 1] = 0 - G.ID[1, :, :, 0] = 0 - G.ID[1, :, :, 1] = 0 - - - 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) - - - -def dispersion_analysis(G): - """ - Analysis of numerical dispersion (Taflove et al, 2005, p112) - - worse case of maximum frequency and minimum wavelength - - Args: - G (class): Grid class instance - holds essential parameters describing the model. - - Returns: - results (dict): Results from dispersion analysis - """ - - # Physical phase velocity error (percentage); grid sampling density; - # material with maximum permittivity; maximum significant frequency; error message - results = {'deltavp': False, 'N': False, 'material': False, 'maxfreq': [], 'error': ''} - - # Find maximum significant frequency - if G.waveforms: - for waveform in G.waveforms: - if waveform.type == 'sine' or waveform.type == 'contsine': - results['maxfreq'].append(4 * waveform.freq) - - elif waveform.type == 'impulse': - results['error'] = 'impulse waveform used.' - - else: - # User-defined waveform - if waveform.type == 'user': - iterations = G.iterations - - # Built-in waveform - else: - # Time to analyse waveform - 4*pulse_width as using entire - # time window can result in demanding FFT - waveform.calculate_coefficients() - iterations = round_value(4 * waveform.chi / G.dt) - if iterations > G.iterations: - iterations = G.iterations - - waveformvalues = np.zeros(G.iterations) - for iteration in range(G.iterations): - waveformvalues[iteration] = waveform.calculate_value(iteration * G.dt, G.dt) - - # Ensure source waveform is not being overly truncated before attempting any FFT - if np.abs(waveformvalues[-1]) < np.abs(np.amax(waveformvalues)) / 100: - # FFT - freqs, power = fft_power(waveformvalues, G.dt) - # Get frequency for max power - freqmaxpower = np.where(np.isclose(power, 0))[0][0] - - # Set maximum frequency to a threshold drop from maximum power, ignoring DC value - try: - freqthres = np.where(power[freqmaxpower:] < -config.numdispersion['highestfreqthres'])[0][0] + freqmaxpower - results['maxfreq'].append(freqs[freqthres]) - except ValueError: - results['error'] = 'unable to calculate maximum power from waveform, most likely due to undersampling.' - - # Ignore case where someone is using a waveform with zero amplitude, i.e. on a receiver - elif waveform.amp == 0: - pass - - # If waveform is truncated don't do any further analysis - else: - results['error'] = 'waveform does not fit within specified time window and is therefore being truncated.' - else: - results['error'] = 'no waveform detected.' - - if results['maxfreq']: - results['maxfreq'] = max(results['maxfreq']) - - # Find minimum wavelength (material with maximum permittivity) - maxer = 0 - matmaxer = '' - for x in G.materials: - if x.se != float('inf'): - er = x.er - # If there are dispersive materials calculate the complex relative permittivity - # at maximum frequency and take the real part - if x.__class__.__name__ is 'DispersiveMaterial': - er = x.calculate_er(results['maxfreq']) - er = er.real - if er > maxer: - maxer = er - matmaxer = x.ID - results['material'] = next(x for x in G.materials if x.ID == matmaxer) - - # Minimum velocity - minvelocity = config.c / np.sqrt(maxer) - - # Minimum wavelength - minwavelength = minvelocity / results['maxfreq'] - - # Maximum spatial step - if '3D' in config.general['mode']: - delta = max(G.dx, G.dy, G.dz) - elif '2D' in config.general['mode']: - if G.nx == 1: - delta = max(G.dy, G.dz) - elif G.ny == 1: - delta = max(G.dx, G.dz) - elif G.nz == 1: - delta = max(G.dx, G.dy) - - # Courant stability factor - S = (config.c * G.dt) / delta - - # Grid sampling density - results['N'] = minwavelength / delta - - # Check grid sampling will result in physical wave propagation - if int(np.floor(results['N'])) >= config.numdispersion['mingridsampling']: - # Numerical phase velocity - vp = np.pi / (results['N'] * np.arcsin((1 / S) * np.sin((np.pi * S) / results['N']))) - - # Physical phase velocity error (percentage) - results['deltavp'] = (((vp * config.c) - config.c) / config.c) * 100 - - # Store rounded down value of grid sampling density - results['N'] = int(np.floor(results['N'])) - - return results - - -def Ix(x, y, z, Hx, Hy, Hz, G): - """Calculates the x-component of current at a grid position. - - Args: - x, y, z (float): Coordinates of position in grid. - Hx, Hy, Hz (memory view): numpy array of magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if y == 0 or z == 0: - Ix = 0 - - else: - Ix = G.dy * (Hy[x, y, z - 1] - Hy[x, y, z]) + G.dz * (Hz[x, y, z] - Hz[x, y - 1, z]) - - return Ix - - -def Iy(x, y, z, Hx, Hy, Hz, G): - """Calculates the y-component of current at a grid position. - - Args: - x, y, z (float): Coordinates of position in grid. - Hx, Hy, Hz (memory view): numpy array of magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if x == 0 or z == 0: - Iy = 0 - - else: - Iy = G.dx * (Hx[x, y, z] - Hx[x, y, z - 1]) + G.dz * (Hz[x - 1, y, z] - Hz[x, y, z]) - - return Iy - - -def Iz(x, y, z, Hx, Hy, Hz, G): - """Calculates the z-component of current at a grid position. - - Args: - x, y, z (float): Coordinates of position in grid. - Hx, Hy, Hz (memory view): numpy array of magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if x == 0 or y == 0: - Iz = 0 - - else: - 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(Grid): - pass +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +from collections import OrderedDict + +from colorama import init +from colorama import Fore +from colorama import Style +init() +import numpy as np +np.seterr(invalid='raise') + +import gprMax.config as config +from .exceptions import GeneralError +from .pml import PML +from .pml import CFS +from .utilities import fft_power +from .utilities import human_size +from .utilities import round_value + +import decimal as d +from scipy.constants import c + + + +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 the entire grid. A convenient + way for accessing regularly used parameters. + """ + + def __init__(self): + self.title = '' + self.memoryusage = 0 + + self.nx = 0 + self.ny = 0 + self.nz = 0 + self.dx = 0 + self.dy = 0 + self.dz = 0 + self.dt = 0 + self.iterations = 0 + self.timewindow = 0 + + # Ordered dictionary required so that PMLs are always updated in the + # same order. The order itself does not matter, however, if must be the + # same from model to model otherwise the numerical precision from adding + # the PML corrections will be different. + self.pmlthickness = OrderedDict((key, 10) for key in PML.boundaryIDs) + self.cfs = [CFS()] + self.pmls = [] + self.pmlformulation = 'HORIPML' + + self.materials = [] + self.mixingmodels = [] + self.averagevolumeobjects = True + self.fractalvolumes = [] + self.geometryviews = [] + self.geometryobjectswrite = [] + self.waveforms = [] + self.voltagesources = [] + self.hertziandipoles = [] + self.magneticdipoles = [] + self.transmissionlines = [] + self.rxs = [] + self.srcsteps = [0, 0, 0] + self.rxsteps = [0, 0, 0] + self.snapshots = [] + self.subgrids = [] + self.gpu = None + self.name = 'Main' + self.outputdirectory = '' + self.iteration = 0 + + def initialise_geometry_arrays(self): + """ + Initialise an array for volumetric material IDs (solid); + boolean arrays for specifying whether materials can have dielectric smoothing (rigid); + and an array for cell edge IDs (ID). + Solid and ID arrays are initialised to free_space (one); + rigid arrays to allow dielectric smoothing (zero). + """ + self.solid = np.ones((self.nx, self.ny, self.nz), dtype=np.uint32) + self.rigidE = np.zeros((12, self.nx, self.ny, self.nz), dtype=np.int8) + self.rigidH = np.zeros((6, self.nx, self.ny, self.nz), dtype=np.int8) + self.ID = np.ones((6, self.nx + 1, self.ny + 1, self.nz + 1), dtype=np.uint32) + self.IDlookup = {'Ex': 0, 'Ey': 1, 'Ez': 2, 'Hx': 3, 'Hy': 4, 'Hz': 5} + + def initialise_field_arrays(self): + """Initialise arrays for the electric and magnetic field components.""" + self.Ex = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) + self.Ey = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) + self.Ez = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) + self.Hx = np.zeros((self.nx + 1, self.ny + 1, self.nz + 1), dtype=config.dtypes['float_or_double']) + 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.""" + for g in [self] + self.subgrids: + g.initialise_geometry_arrays() + g.initialise_field_arrays() + + def initialise_std_update_coeff_arrays(self): + """Initialise arrays for storing update coefficients.""" + self.updatecoeffsE = np.zeros((len(self.materials), 5), dtype=config.dtypes['float_or_double']) + self.updatecoeffsH = np.zeros((len(self.materials), 5), dtype=config.dtypes['float_or_double']) + + def initialise_dispersive_arrays(self, dtype): + """Initialise arrays for storing coefficients when there are dispersive materials present.""" + self.Tx = np.zeros((config.materials['maxpoles'], self.nx + 1, self.ny + 1, self.nz + 1), dtype=dtype) + self.Ty = np.zeros((config.materials['maxpoles'], self.nx + 1, self.ny + 1, self.nz + 1), dtype=dtype) + self.Tz = np.zeros((config.materials['maxpoles'], self.nx + 1, self.ny + 1, self.nz + 1), dtype=dtype) + self.updatecoeffsdispersive = np.zeros((len(self.materials), 3 * config.materials['maxpoles']), dtype=dtype) + + def memory_estimate_basic(self): + """Estimate the amount of memory (RAM) required to run a model.""" + + stdoverhead = 50e6 + + solidarray = self.nx * self.ny * self.nz * np.dtype(np.uint32).itemsize + + # 12 x rigidE array components + 6 x rigidH array components + rigidarrays = (12 + 6) * self.nx * self.ny * self.nz * np.dtype(np.int8).itemsize + + # 6 x field arrays + 6 x ID arrays + fieldarrays = (6 + 6) * (self.nx + 1) * (self.ny + 1) * (self.nz + 1) * np.dtype(config.dtypes['float_or_double']).itemsize + + # PML arrays + pmlarrays = 0 + for (k, v) in self.pmlthickness.items(): + if v > 0: + if 'x' in k: + pmlarrays += ((v + 1) * self.ny * (self.nz + 1)) + pmlarrays += ((v + 1) * (self.ny + 1) * self.nz) + pmlarrays += (v * self.ny * (self.nz + 1)) + pmlarrays += (v * (self.ny + 1) * self.nz) + elif 'y' in k: + pmlarrays += (self.nx * (v + 1) * (self.nz + 1)) + pmlarrays += ((self.nx + 1) * (v + 1) * self.nz) + pmlarrays += ((self.nx + 1) * v * self.nz) + pmlarrays += (self.nx * v * (self.nz + 1)) + elif 'z' in k: + pmlarrays += (self.nx * (self.ny + 1) * (v + 1)) + pmlarrays += ((self.nx + 1) * self.ny * (v + 1)) + pmlarrays += ((self.nx + 1) * self.ny * v) + pmlarrays += (self.nx * (self.ny + 1) * v) + + 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 + """ + + # 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))) + + # 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)) + + # 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 gpu_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.""" + + import pycuda.gpuarray as gpuarray + + self.ID_gpu = gpuarray.to_gpu(self.ID) + self.Ex_gpu = gpuarray.to_gpu(self.Ex) + self.Ey_gpu = gpuarray.to_gpu(self.Ey) + self.Ez_gpu = gpuarray.to_gpu(self.Ez) + self.Hx_gpu = gpuarray.to_gpu(self.Hx) + self.Hy_gpu = gpuarray.to_gpu(self.Hy) + self.Hz_gpu = gpuarray.to_gpu(self.Hz) + + def gpu_initialise_dispersive_arrays(self): + """Initialise dispersive material coefficient arrays on GPU.""" + + import pycuda.gpuarray as gpuarray + + self.Tx_gpu = gpuarray.to_gpu(self.Tx) + self.Ty_gpu = gpuarray.to_gpu(self.Ty) + 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 tmy(): + # Ex & Ez components + G.ID[0, :, 0, :] = 0 + G.ID[0, :, 1, :] = 0 + G.ID[2, :, 0, :] = 0 + G.ID[2, :, 1, :] = 0 + + def tmz(): + # Ex & Ey components + G.ID[0, :, :, 0] = 0 + G.ID[0, :, :, 1] = 0 + G.ID[1, :, :, 0] = 0 + G.ID[1, :, :, 1] = 0 + + + 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) + + + +def dispersion_analysis(G): + """ + Analysis of numerical dispersion (Taflove et al, 2005, p112) - + worse case of maximum frequency and minimum wavelength + + Args: + G (class): Grid class instance - holds essential parameters describing the model. + + Returns: + results (dict): Results from dispersion analysis + """ + + # Physical phase velocity error (percentage); grid sampling density; + # material with maximum permittivity; maximum significant frequency; error message + results = {'deltavp': False, 'N': False, 'material': False, 'maxfreq': [], 'error': ''} + + # Find maximum significant frequency + if G.waveforms: + for waveform in G.waveforms: + if waveform.type == 'sine' or waveform.type == 'contsine': + results['maxfreq'].append(4 * waveform.freq) + + elif waveform.type == 'impulse': + results['error'] = 'impulse waveform used.' + + else: + # User-defined waveform + if waveform.type == 'user': + iterations = G.iterations + + # Built-in waveform + else: + # Time to analyse waveform - 4*pulse_width as using entire + # time window can result in demanding FFT + waveform.calculate_coefficients() + iterations = round_value(4 * waveform.chi / G.dt) + if iterations > G.iterations: + iterations = G.iterations + + waveformvalues = np.zeros(G.iterations) + for iteration in range(G.iterations): + waveformvalues[iteration] = waveform.calculate_value(iteration * G.dt, G.dt) + + # Ensure source waveform is not being overly truncated before attempting any FFT + if np.abs(waveformvalues[-1]) < np.abs(np.amax(waveformvalues)) / 100: + # FFT + freqs, power = fft_power(waveformvalues, G.dt) + # Get frequency for max power + freqmaxpower = np.where(np.isclose(power, 0))[0][0] + + # Set maximum frequency to a threshold drop from maximum power, ignoring DC value + try: + freqthres = np.where(power[freqmaxpower:] < -config.numdispersion['highestfreqthres'])[0][0] + freqmaxpower + results['maxfreq'].append(freqs[freqthres]) + except ValueError: + results['error'] = 'unable to calculate maximum power from waveform, most likely due to undersampling.' + + # Ignore case where someone is using a waveform with zero amplitude, i.e. on a receiver + elif waveform.amp == 0: + pass + + # If waveform is truncated don't do any further analysis + else: + results['error'] = 'waveform does not fit within specified time window and is therefore being truncated.' + else: + results['error'] = 'no waveform detected.' + + if results['maxfreq']: + results['maxfreq'] = max(results['maxfreq']) + + # Find minimum wavelength (material with maximum permittivity) + maxer = 0 + matmaxer = '' + for x in G.materials: + if x.se != float('inf'): + er = x.er + # If there are dispersive materials calculate the complex relative permittivity + # at maximum frequency and take the real part + if x.__class__.__name__ is 'DispersiveMaterial': + er = x.calculate_er(results['maxfreq']) + er = er.real + if er > maxer: + maxer = er + matmaxer = x.ID + results['material'] = next(x for x in G.materials if x.ID == matmaxer) + + # Minimum velocity + minvelocity = config.c / np.sqrt(maxer) + + # Minimum wavelength + minwavelength = minvelocity / results['maxfreq'] + + # Maximum spatial step + if '3D' in config.general['mode']: + delta = max(G.dx, G.dy, G.dz) + elif '2D' in config.general['mode']: + if G.nx == 1: + delta = max(G.dy, G.dz) + elif G.ny == 1: + delta = max(G.dx, G.dz) + elif G.nz == 1: + delta = max(G.dx, G.dy) + + # Courant stability factor + S = (config.c * G.dt) / delta + + # Grid sampling density + results['N'] = minwavelength / delta + + # Check grid sampling will result in physical wave propagation + if int(np.floor(results['N'])) >= config.numdispersion['mingridsampling']: + # Numerical phase velocity + vp = np.pi / (results['N'] * np.arcsin((1 / S) * np.sin((np.pi * S) / results['N']))) + + # Physical phase velocity error (percentage) + results['deltavp'] = (((vp * config.c) - config.c) / config.c) * 100 + + # Store rounded down value of grid sampling density + results['N'] = int(np.floor(results['N'])) + + return results + + +def Ix(x, y, z, Hx, Hy, Hz, G): + """Calculates the x-component of current at a grid position. + + Args: + x, y, z (float): Coordinates of position in grid. + Hx, Hy, Hz (memory view): numpy array of magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if y == 0 or z == 0: + Ix = 0 + + else: + Ix = G.dy * (Hy[x, y, z - 1] - Hy[x, y, z]) + G.dz * (Hz[x, y, z] - Hz[x, y - 1, z]) + + return Ix + + +def Iy(x, y, z, Hx, Hy, Hz, G): + """Calculates the y-component of current at a grid position. + + Args: + x, y, z (float): Coordinates of position in grid. + Hx, Hy, Hz (memory view): numpy array of magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if x == 0 or z == 0: + Iy = 0 + + else: + Iy = G.dx * (Hx[x, y, z] - Hx[x, y, z - 1]) + G.dz * (Hz[x - 1, y, z] - Hz[x, y, z]) + + return Iy + + +def Iz(x, y, z, Hx, Hy, Hz, G): + """Calculates the z-component of current at a grid position. + + Args: + x, y, z (float): Coordinates of position in grid. + Hx, Hy, Hz (memory view): numpy array of magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if x == 0 or y == 0: + Iz = 0 + + else: + 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(Grid): + pass diff --git a/gprMax/input_cmds_geometry.py b/gprMax/input_cmds_geometry.py index a25f0ec0..a3793082 100644 --- a/gprMax/input_cmds_geometry.py +++ b/gprMax/input_cmds_geometry.py @@ -1,318 +1,318 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -import sys - -from .utilities import get_terminal_width - -from .cmds_geometry.geometry_objects_read import GeometryObjectsRead -from .cmds_geometry.edge import Edge -from .cmds_geometry.plate import Plate -from .cmds_geometry.triangle import Triangle -from .cmds_geometry.box import Box -from .cmds_geometry.cylinder import Cylinder -from .cmds_geometry.cylindrical_sector import CylindricalSector -from .cmds_geometry.fractal_box import FractalBox -from .cmds_geometry.sphere import Sphere -from .cmds_geometry.add_surface_roughness import AddSurfaceRoughness -from .cmds_geometry.add_surface_water import AddSurfaceWater -from .cmds_geometry.add_grass import AddGrass - -from .utilities import round_value - - -from tqdm import tqdm -import numpy as np - - -def process_geometrycmds(geometry): - """ - This function checks the validity of command parameters, creates instances - of classes of parameters, and calls functions to directly set arrays - solid, rigid and ID. - - Args: - geometry (list): Geometry commands in the model - """ - - scene_objects = [] - - # Disable progress bar if on Windows as it does not update properly - # when messages are printed - #if sys.platform == 'win32': -# tqdmdisable = True - #else: - # tqdmdisable = G.tqdmdisable - - tqdmdisable = False - - for object in tqdm(geometry, desc='Processing geometry related cmds', unit='cmds', ncols=get_terminal_width() - 1, file=sys.stdout, disable=tqdmdisable): - tmp = object.split() - - if tmp[0] == '#geometry_objects_read:': - if len(tmp) != 6: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires exactly five parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - - gor = GeometryObjectsRead(p1=p1, geofile=tmp[4], matfile=tmp[5]) - scene_objects.append(gor) - - elif tmp[0] == '#edge:': - if len(tmp) != 8: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires exactly seven parameters') - - edge = Edge(p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), - p2=(float(tmp[4]), float(tmp[5]), float(tmp[6])), - material_id=tmp[7]) - - scene_objects.append(edge) - - elif tmp[0] == '#plate:': - if len(tmp) < 8: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least seven parameters') - - # Isotropic case - if len(tmp) == 8: - plate = Plate(p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), - p2=(float(tmp[4]), float(tmp[5]), float(tmp[6])), - material_id=tmp[7]) - - # Anisotropic case - elif len(tmp) == 9: - plate = Plate(p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), - p2=(float(tmp[4]), float(tmp[5]), float(tmp[6])), - material_ids=tmp[7:]) - - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(plate) - - elif tmp[0] == '#triangle:': - if len(tmp) < 12: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least eleven parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - p3 = (float(tmp[7]), float(tmp[8]), float(tmp[9])) - thickness = float(tmp[10]) - - # Isotropic case with no user specified averaging - if len(tmp) == 12: - triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11]) - - # Isotropic case with user specified averaging - elif len(tmp) == 13: - triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11], averaging=tmp[12].lower()) - - # Uniaxial anisotropic case - elif len(tmp) == 14: - triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_ids=tmp[11:]) - - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(triangle) - - elif tmp[0] == '#box:': - if len(tmp) < 8: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least seven parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - - # Isotropic case with no user specified averaging - if len(tmp) == 8: - box = Box(p1=p1, p2=p2, material_id=tmp[7]) - - # Isotropic case with user specified averaging - elif len(tmp) == 9: - box = Box(p1=p1, p2=p2, material_id=tmp[7], averaging=tmp[8]) - - # Uniaxial anisotropic case - elif len(tmp) == 10: - box = Box(p1=p1, p2=p2, material_ids=tmp[7:]) - - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(box) - - elif tmp[0] == '#cylinder:': - if len(tmp) < 9: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least eight parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - r = float(tmp[7]) - - # Isotropic case with no user specified averaging - if len(tmp) == 9: - cylinder = Cylinder(p1=p1, p2=p2, r=r, material_id=tmp[8]) - - # Isotropic case with user specified averaging - elif len(tmp) == 10: - cylinder = Cylinder(p1=p1, p2=p2, r=r, material_id=tmp[8], averaging=tmp[9]) - - # Uniaxial anisotropic case - elif len(tmp) == 11: - cylinder = Cylinder(p1=p1, p2=p2, r=r, material_ids=tmp[8:]) - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(cylinder) - - elif tmp[0] == '#cylindrical_sector:': - if len(tmp) < 10: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least nine parameters') - - normal = tmp[1].lower() - ctr1 = float(tmp[2]) - ctr2 = float(tmp[3]) - extent1 = float(tmp[4]) - extent2 = float(tmp[5]) - r = float(tmp[6]) - start = float(tmp[7]) - end = float(tmp[8]) - - # Isotropic case with no user specified averaging - if len(tmp) == 10: - CylindricalSector(normal=normal, ctl1=ctl1, ctl2=ctl2, extent1=extent1, extent2=extent2, r=r, start=start, end=end, msterial_id=tmp[9]) - - # Isotropic case with user specified averaging - elif len(tmp) == 11: - CylindricalSector(normal=normal, ctl1=ctl1, ctl2=ctl2, extent1=extent1, extent2=extent2, r=r, start=start, end=end, averaging=tmp[10], material_id=tmp[9]) - - # Uniaxial anisotropic case - elif len(tmp) == 12: - CylindricalSector(normal=normal, ctl1=ctl1, ctl2=ctl2, extent1=extent1, extent2=extent2, r=r, start=start, end=end, material_ids=tmp[9:]) - - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(cylindrical_sector) - - elif tmp[0] == '#sphere:': - if len(tmp) < 6: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least five parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - r = float(tmp[4]) - - # Isotropic case with no user specified averaging - if len(tmp) == 6: - sphere = Sphere(p1=p1, r=r, material_id=tmp[5]) - - # Isotropic case with user specified averaging - elif len(tmp) == 7: - sphere = Sphere(p1=p1, r=r, material_id=tmp[5], averaging=tmp[6]) - - # Uniaxial anisotropic case - elif len(tmp) == 8: - sphere = Sphere(p1=p1, r=r, material_id=tmp[5:]) - - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(sphere) - - elif tmp[0] == '#fractal_box:': - # Default is no dielectric smoothing for a fractal box - - if len(tmp) < 14: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least thirteen parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - frac_dim = float(tmp[7]) - weighting = np.array([float(tmp[8]), float(tmp[9]), float(tmp[10])]) - n_materials = round_value(tmp[11]) - mixing_model_id = tmp[12] - ID = tmp[13] - # without seed - if len(tmp) == 14: - fb = FractalBox(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, mixing_model_id=mixing_model_id, id=ID, n_materials=n_materials) - # with seed - elif len(tmp) == 15: - fb = FractalBox(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, mixing_model_id=mixing_model_id, id=ID, n_materials=n_materials, seed=tmp[14]) - # user specified averaging - elif len(tmp) == 16: - fb = FractalBox(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, mixing_model_id=mixing_model_id, id=ID, n_materials=n_materials, seed=tmp[14], averaging=tmp[15].lower()) - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(fb) - - # Search and process any modifiers for the fractal box - for object in geometry: - tmp = object.split() - - if tmp[0] == '#add_surface_roughness:': - if len(tmp) < 13: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least twelve parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - frac_dim = float(tmp[7]) - weighting = np.array([float(tmp[8]), float(tmp[9])]) - limits = [float(tmp[10]), float(tmp[11])] - fractal_box_id = tmp[12] - - # No seed - if len(tmp) == 13: - asr = AddSurfaceRoughness(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, limits=limits, fractal_box_id=fractal_box_id) - elif len(tmp) == 14: - asr = AddSurfaceRoughness(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, limits=limits, fractal_box_id=fractal_box_id, seed=int(tmp[13])) - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - - scene_objects.append(asr) - - if tmp[0] == '#add_surface_water:': - if len(tmp) != 9: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires exactly eight parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - depth = float(tmp[7]) - fractal_box_id = tmp[8] - - asf = AddSurfaceWater(p1=p1, p2=p2, depth=depth, fractal_box_id=fractal_box_id) - scene_objects.append(asf) - - if tmp[0] == '#add_grass:': - if len(tmp) < 12: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least eleven parameters') - - p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) - p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) - frac_dim = float(tmp[7]) - limits = [float(tmp[8]), float(tmp[9])] - n_blades = int(tmp[10]) - fractal_box_id = tmp[11] - - # no seed - if len(tmp) == 12: - grass = AddGrass(p1=p1, p2=p2, frac_dim=frac_dim, limits=limits, n_blades=n_blades, fractal_box_id=fractal_box_id) - elif len(tmp) == 13: - grass = AddGrass(p1=p1, p2=p2, frac_dim=frac_dim, limits=limits, n_blades=n_blades, fractal_box_id=fractal_box_id, seed=int(tmp[12])) - else: - raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') - scene_objects.append(grass) - - return scene_objects +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +import sys + +from .utilities import get_terminal_width + +from .cmds_geometry.geometry_objects_read import GeometryObjectsRead +from .cmds_geometry.edge import Edge +from .cmds_geometry.plate import Plate +from .cmds_geometry.triangle import Triangle +from .cmds_geometry.box import Box +from .cmds_geometry.cylinder import Cylinder +from .cmds_geometry.cylindrical_sector import CylindricalSector +from .cmds_geometry.fractal_box import FractalBox +from .cmds_geometry.sphere import Sphere +from .cmds_geometry.add_surface_roughness import AddSurfaceRoughness +from .cmds_geometry.add_surface_water import AddSurfaceWater +from .cmds_geometry.add_grass import AddGrass + +from .utilities import round_value + + +from tqdm import tqdm +import numpy as np + + +def process_geometrycmds(geometry): + """ + This function checks the validity of command parameters, creates instances + of classes of parameters, and calls functions to directly set arrays + solid, rigid and ID. + + Args: + geometry (list): Geometry commands in the model + """ + + scene_objects = [] + + # Disable progress bar if on Windows as it does not update properly + # when messages are printed + #if sys.platform == 'win32': +# tqdmdisable = True + #else: + # tqdmdisable = G.tqdmdisable + + tqdmdisable = False + + for object in tqdm(geometry, desc='Processing geometry related cmds', unit='cmds', ncols=get_terminal_width() - 1, file=sys.stdout, disable=tqdmdisable): + tmp = object.split() + + if tmp[0] == '#geometry_objects_read:': + if len(tmp) != 6: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires exactly five parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + + gor = GeometryObjectsRead(p1=p1, geofile=tmp[4], matfile=tmp[5]) + scene_objects.append(gor) + + elif tmp[0] == '#edge:': + if len(tmp) != 8: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires exactly seven parameters') + + edge = Edge(p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), + p2=(float(tmp[4]), float(tmp[5]), float(tmp[6])), + material_id=tmp[7]) + + scene_objects.append(edge) + + elif tmp[0] == '#plate:': + if len(tmp) < 8: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least seven parameters') + + # Isotropic case + if len(tmp) == 8: + plate = Plate(p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), + p2=(float(tmp[4]), float(tmp[5]), float(tmp[6])), + material_id=tmp[7]) + + # Anisotropic case + elif len(tmp) == 9: + plate = Plate(p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), + p2=(float(tmp[4]), float(tmp[5]), float(tmp[6])), + material_ids=tmp[7:]) + + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(plate) + + elif tmp[0] == '#triangle:': + if len(tmp) < 12: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least eleven parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + p3 = (float(tmp[7]), float(tmp[8]), float(tmp[9])) + thickness = float(tmp[10]) + + # Isotropic case with no user specified averaging + if len(tmp) == 12: + triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11]) + + # Isotropic case with user specified averaging + elif len(tmp) == 13: + triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11], averaging=tmp[12].lower()) + + # Uniaxial anisotropic case + elif len(tmp) == 14: + triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_ids=tmp[11:]) + + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(triangle) + + elif tmp[0] == '#box:': + if len(tmp) < 8: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least seven parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + + # Isotropic case with no user specified averaging + if len(tmp) == 8: + box = Box(p1=p1, p2=p2, material_id=tmp[7]) + + # Isotropic case with user specified averaging + elif len(tmp) == 9: + box = Box(p1=p1, p2=p2, material_id=tmp[7], averaging=tmp[8]) + + # Uniaxial anisotropic case + elif len(tmp) == 10: + box = Box(p1=p1, p2=p2, material_ids=tmp[7:]) + + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(box) + + elif tmp[0] == '#cylinder:': + if len(tmp) < 9: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least eight parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + r = float(tmp[7]) + + # Isotropic case with no user specified averaging + if len(tmp) == 9: + cylinder = Cylinder(p1=p1, p2=p2, r=r, material_id=tmp[8]) + + # Isotropic case with user specified averaging + elif len(tmp) == 10: + cylinder = Cylinder(p1=p1, p2=p2, r=r, material_id=tmp[8], averaging=tmp[9]) + + # Uniaxial anisotropic case + elif len(tmp) == 11: + cylinder = Cylinder(p1=p1, p2=p2, r=r, material_ids=tmp[8:]) + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(cylinder) + + elif tmp[0] == '#cylindrical_sector:': + if len(tmp) < 10: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least nine parameters') + + normal = tmp[1].lower() + ctr1 = float(tmp[2]) + ctr2 = float(tmp[3]) + extent1 = float(tmp[4]) + extent2 = float(tmp[5]) + r = float(tmp[6]) + start = float(tmp[7]) + end = float(tmp[8]) + + # Isotropic case with no user specified averaging + if len(tmp) == 10: + CylindricalSector(normal=normal, ctl1=ctl1, ctl2=ctl2, extent1=extent1, extent2=extent2, r=r, start=start, end=end, msterial_id=tmp[9]) + + # Isotropic case with user specified averaging + elif len(tmp) == 11: + CylindricalSector(normal=normal, ctl1=ctl1, ctl2=ctl2, extent1=extent1, extent2=extent2, r=r, start=start, end=end, averaging=tmp[10], material_id=tmp[9]) + + # Uniaxial anisotropic case + elif len(tmp) == 12: + CylindricalSector(normal=normal, ctl1=ctl1, ctl2=ctl2, extent1=extent1, extent2=extent2, r=r, start=start, end=end, material_ids=tmp[9:]) + + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(cylindrical_sector) + + elif tmp[0] == '#sphere:': + if len(tmp) < 6: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least five parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + r = float(tmp[4]) + + # Isotropic case with no user specified averaging + if len(tmp) == 6: + sphere = Sphere(p1=p1, r=r, material_id=tmp[5]) + + # Isotropic case with user specified averaging + elif len(tmp) == 7: + sphere = Sphere(p1=p1, r=r, material_id=tmp[5], averaging=tmp[6]) + + # Uniaxial anisotropic case + elif len(tmp) == 8: + sphere = Sphere(p1=p1, r=r, material_id=tmp[5:]) + + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(sphere) + + elif tmp[0] == '#fractal_box:': + # Default is no dielectric smoothing for a fractal box + + if len(tmp) < 14: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least thirteen parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + frac_dim = float(tmp[7]) + weighting = np.array([float(tmp[8]), float(tmp[9]), float(tmp[10])]) + n_materials = round_value(tmp[11]) + mixing_model_id = tmp[12] + ID = tmp[13] + # without seed + if len(tmp) == 14: + fb = FractalBox(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, mixing_model_id=mixing_model_id, id=ID, n_materials=n_materials) + # with seed + elif len(tmp) == 15: + fb = FractalBox(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, mixing_model_id=mixing_model_id, id=ID, n_materials=n_materials, seed=tmp[14]) + # user specified averaging + elif len(tmp) == 16: + fb = FractalBox(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, mixing_model_id=mixing_model_id, id=ID, n_materials=n_materials, seed=tmp[14], averaging=tmp[15].lower()) + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(fb) + + # Search and process any modifiers for the fractal box + for object in geometry: + tmp = object.split() + + if tmp[0] == '#add_surface_roughness:': + if len(tmp) < 13: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least twelve parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + frac_dim = float(tmp[7]) + weighting = np.array([float(tmp[8]), float(tmp[9])]) + limits = [float(tmp[10]), float(tmp[11])] + fractal_box_id = tmp[12] + + # No seed + if len(tmp) == 13: + asr = AddSurfaceRoughness(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, limits=limits, fractal_box_id=fractal_box_id) + elif len(tmp) == 14: + asr = AddSurfaceRoughness(p1=p1, p2=p2, frac_dim=frac_dim, weighting=weighting, limits=limits, fractal_box_id=fractal_box_id, seed=int(tmp[13])) + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + + scene_objects.append(asr) + + if tmp[0] == '#add_surface_water:': + if len(tmp) != 9: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires exactly eight parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + depth = float(tmp[7]) + fractal_box_id = tmp[8] + + asf = AddSurfaceWater(p1=p1, p2=p2, depth=depth, fractal_box_id=fractal_box_id) + scene_objects.append(asf) + + if tmp[0] == '#add_grass:': + if len(tmp) < 12: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' requires at least eleven parameters') + + p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) + p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6])) + frac_dim = float(tmp[7]) + limits = [float(tmp[8]), float(tmp[9])] + n_blades = int(tmp[10]) + fractal_box_id = tmp[11] + + # no seed + if len(tmp) == 12: + grass = AddGrass(p1=p1, p2=p2, frac_dim=frac_dim, limits=limits, n_blades=n_blades, fractal_box_id=fractal_box_id) + elif len(tmp) == 13: + grass = AddGrass(p1=p1, p2=p2, frac_dim=frac_dim, limits=limits, n_blades=n_blades, fractal_box_id=fractal_box_id, seed=int(tmp[12])) + else: + raise CmdInputError("'" + ' '.join(tmp) + "'" + ' too many parameters have been given') + scene_objects.append(grass) + + return scene_objects diff --git a/gprMax/input_cmds_multiuse.py b/gprMax/input_cmds_multiuse.py index 5ead64de..20922e70 100644 --- a/gprMax/input_cmds_multiuse.py +++ b/gprMax/input_cmds_multiuse.py @@ -1,338 +1,338 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -# Copyright (C) 2015-2018: 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -from .exceptions import CmdInputError -from .cmds_multiple import Waveform -from .cmds_multiple import VoltageSource -from .cmds_multiple import HertzianDipole -from .cmds_multiple import MagneticDipole -from .cmds_multiple import TransmissionLine -from .cmds_multiple import Material -from .cmds_multiple import Snapshot -from .cmds_multiple import AddDebyeDispersion -from .cmds_multiple import AddLorentzDispersion -from .cmds_multiple import AddDrudeDispersion -from .cmds_multiple import SoilPeplinski -from .cmds_multiple import GeometryView -from .cmds_multiple import GeometryObjectsWrite -from .cmds_multiple import PMLCFS -from .cmds_multiple import Rx - - -def process_multicmds(multicmds): - """ - Checks the validity of command parameters and creates instances of - classes of parameters. - - Args: - multicmds (dict): Commands that can have multiple instances in the model. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - scene_objects = [] - - # Waveform definitions - cmdname = '#waveform' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 4: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly four parameters') - - waveform = Waveform(wave_type=tmp[0], amp=float(tmp[1]), freq=float(tmp[2]), id=tmp[3]) - scene_objects.append(waveform) - - # Voltage source - cmdname = '#voltage_source' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) == 6: - voltage_source = VoltageSource(polarisation=tmp[0].lower(), p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5]) - elif len(tmp) == 8: - voltage_source = VoltageSource(polarisation=tmp[0].lower(), p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5], start=float(tmp[6]), end=float(tmp[7])) - else: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least six parameters') - - scene_objects.append(voltage_source) - - # Hertzian dipole - cmdname = '#hertzian_dipole' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) < 5: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') - if len(tmp) == 5: - hertzian_dipole = HertzianDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4]) - elif len(tmp) == 7: - hertzian_dipole = HertzianDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4], start=float(tmp[5]), end=float(tmp[6])) - else: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' too many parameters') - - scene_objects.append(hertzian_dipole) - - # Magnetic dipole - cmdname = '#magnetic_dipole' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) < 5: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') - if len(tmp) == 5: - magnetic_dipole = MagneticDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4]) - elif len(tmp) == 7: - magnetic_dipole = MagneticDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4], start=float(tmp[5]), end=float(tmp[6])) - else: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' too many parameters') - - scene_objects.append(magnetic_dipole) - - # Transmission line - cmdname = '#transmission_line' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) < 6: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least six parameters') - - if len(tmp) == 6: - tl = TransmissionLine(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5]) - elif len(tmp) == 8: - tl = TransmissionLine(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5], start=tmp[6], end=tmp[7]) - else: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' too many parameters') - - scene_objects.append(tl) - - # Receiver - cmdname = '#rx' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 3 and len(tmp) < 5: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' has an incorrect number of parameters') - if len(tmp) == 3: - rx = Rx(p1=(float(tmp[0]), float(tmp[1]), float(tmp[2]))) - else: - rx = Rx(p1=(float(tmp[0]), float(tmp[1]), float(tmp[2])), id=tmp[3], outputs=tmp[4:]) - scene_objects.append(rx) - - # Receiver array - cmdname = '#rx_array' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 9: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly nine parameters') - - p1 = (float(tmp[0], float(tmp[1]), float[tmp[2]])) - p2 = (float(tmp[3], float(tmp[4]), float[tmp[5]])) - dl = (float(tmp[6], float(tmp[7]), float[tmp[8]])) - - rx_array = RxArray(p1=p1, p2=p2, dl=dl) - scene_objects.append(rx_array) - - # Snapshot - cmdname = '#snapshot' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 11: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly eleven parameters') - - p1 = (float(tmp[0], float(tmp[1]), float[tmp[2]])) - p2 = (float(tmp[3], float(tmp[4]), float[tmp[5]])) - dl = (float(tmp[6], float(tmp[7]), float[tmp[8]])) - filename = tmp[10] - - try: - iterations = int(tmp[9]) - snapshot = Snapshot(p1=p1, p2=p2, dl=dl, iterations=iterations, filename=filename) - - except ValueError: - time = float(tmp[9]) - snapshot = Snapshot(p1=p1, p2=p2, dl=dl, time=time, filename=filename) - - scene_objects.append(snapshot) - - # Materials - cmdname = '#material' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 5: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly five parameters') - - material = Material(er=float(tmp[0]), se=float(tmp[1]), mr=float(tmp[2]), sm=float(tmp[3]), id=tmp[4]) - scene_objects.append(material) - - cmdname = '#add_dispersion_debye' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - - if len(tmp) < 4: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least four parameters') - - poles = int(tmp[0]) - er_delta = [] - tau = [] - material_ids = tmp[(2 * poles) + 1:len(tmp)] - - for pole in range(1, 2 * poles, 2): - er_delta.append(float(tmp[pole])) - tau.append(float(tmp[pole + 1])) - - debye_dispersion = AddDebyeDispersion(pole=poles, er_delta=er_delta, tau=tau, material_ids=material_ids) - scene_objects.append(debye_dispersion) - - cmdname = '#add_dispersion_lorentz' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - - if len(tmp) < 5: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') - - poles = int(tmp[0]) - material_ids = tmp[(3 * poles) + 1:len(tmp)] - er_delta = [] - tau = [] - alpha = [] - - for pole in range(1, 3 * poles, 3): - er_delta.append(float(tmp[pole])) - tau.append(float(tmp[pole + 1])) - alpha.append(float(tmp[pole + 2])) - - lorentz_dispersion = AddLorentzDispersion(poles=poles, material_ids=material_ids, er_delta=er_delta, tau=tau, alpha=alpha) - scene_objects.append(lorentz_dispersion) - - cmdname = '#add_dispersion_drude' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - - if len(tmp) < 5: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') - - poles = int(tmp[0]) - material_ids = tmp[(3 * poles) + 1:len(tmp)] - tau = [] - alpha = [] - - for pole in range(1, 2 * poles, 2): - tau.append(float(tmp[pole])) - alpha.append(float(tmp[pole + 1])) - - drude_dispersion = AddDrudeDispersion(poles=poles, material_ids=material_ids, tau=tau, alpha=alpha) - scene_objects.append(drude_dispersion) - - cmdname = '#soil_peplinski' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - - if len(tmp) != 7: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at exactly seven parameters') - soil = SoilPeplinski(sand_fraction=float(tmp[0]), - clay_fraction=float(tmp[1]), - bulk_density=float(tmp[2]), - sand_density=float(tmp[3]), - water_fraction_lower=float(tmp[4]), - water_fraction_upper=float(tmp[5]), - id=tmp[6]) - scene_objects.append(soil) - - # Geometry views (creates VTK-based geometry files) - cmdname = '#geometry_view' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 11: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly eleven parameters') - - p1 = float(tmp[0]), float(tmp[1]), float(tmp[2]) - p2 = float(tmp[3]), float(tmp[4]), float(tmp[5]) - dl = float(tmp[6]), float(tmp[7]), float(tmp[8]) - - geometry_view = GeometryView(p1=p1, p2=p2, dl=dl, filename=tmp[9], output_type=tmp[10]) - - scene_objects.append(geometry_view) - - # Geometry object(s) output - cmdname = '#geometry_objects_write' - if multicmds[cmdname] is not None: - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 7: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly seven parameters') - - p1 = float(tmp[0]), float(tmp[1]), float(tmp[2]) - p2 = float(tmp[3]), float(tmp[4]), float(tmp[5]) - gow = GeometryObjectsWrite(p1=p1, p2=p2, filename=tmp[6]) - scene_objects.append(gow) - - - # Complex frequency shifted (CFS) PML parameter - cmdname = '#pml_cfs' - if multicmds[cmdname] is not None: - if len(multicmds[cmdname]) > 2: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' can only be used up to two times, for up to a 2nd order PML') - for cmdinstance in multicmds[cmdname]: - tmp = cmdinstance.split() - if len(tmp) != 12: - raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly twelve parameters') - - pml_cfs = PMLCFS(alphascalingprofile=tmp[0], - alphascalingdirection=tmp[1], - alphamin=tmp[2], - alphamax=tmp[3], - kappascalingprofile=tmp[4], - kappascalingdirection=tmp[5], - kappamin=tmp[6], - kappamax=tmp[7], - sigmascalingprofile=tmp[8], - sigmascalingdirection=tmp[9], - sigmamin=tmp[10], - sigmamax=tmp[11]) - - scene_objects.append(pml_cfs) - - return scene_objects - -def process_subgrid_hsg(cmdinstance): - - pass +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +# Copyright (C) 2015-2018: 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +from .exceptions import CmdInputError +from .cmds_multiple import Waveform +from .cmds_multiple import VoltageSource +from .cmds_multiple import HertzianDipole +from .cmds_multiple import MagneticDipole +from .cmds_multiple import TransmissionLine +from .cmds_multiple import Material +from .cmds_multiple import Snapshot +from .cmds_multiple import AddDebyeDispersion +from .cmds_multiple import AddLorentzDispersion +from .cmds_multiple import AddDrudeDispersion +from .cmds_multiple import SoilPeplinski +from .cmds_multiple import GeometryView +from .cmds_multiple import GeometryObjectsWrite +from .cmds_multiple import PMLCFS +from .cmds_multiple import Rx + + +def process_multicmds(multicmds): + """ + Checks the validity of command parameters and creates instances of + classes of parameters. + + Args: + multicmds (dict): Commands that can have multiple instances in the model. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + scene_objects = [] + + # Waveform definitions + cmdname = '#waveform' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 4: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly four parameters') + + waveform = Waveform(wave_type=tmp[0], amp=float(tmp[1]), freq=float(tmp[2]), id=tmp[3]) + scene_objects.append(waveform) + + # Voltage source + cmdname = '#voltage_source' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) == 6: + voltage_source = VoltageSource(polarisation=tmp[0].lower(), p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5]) + elif len(tmp) == 8: + voltage_source = VoltageSource(polarisation=tmp[0].lower(), p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5], start=float(tmp[6]), end=float(tmp[7])) + else: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least six parameters') + + scene_objects.append(voltage_source) + + # Hertzian dipole + cmdname = '#hertzian_dipole' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) < 5: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') + if len(tmp) == 5: + hertzian_dipole = HertzianDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4]) + elif len(tmp) == 7: + hertzian_dipole = HertzianDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4], start=float(tmp[5]), end=float(tmp[6])) + else: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' too many parameters') + + scene_objects.append(hertzian_dipole) + + # Magnetic dipole + cmdname = '#magnetic_dipole' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) < 5: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') + if len(tmp) == 5: + magnetic_dipole = MagneticDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4]) + elif len(tmp) == 7: + magnetic_dipole = MagneticDipole(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), waveform_id=tmp[4], start=float(tmp[5]), end=float(tmp[6])) + else: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' too many parameters') + + scene_objects.append(magnetic_dipole) + + # Transmission line + cmdname = '#transmission_line' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) < 6: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least six parameters') + + if len(tmp) == 6: + tl = TransmissionLine(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5]) + elif len(tmp) == 8: + tl = TransmissionLine(polarisation=tmp[0], p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])), resistance=float(tmp[4]), waveform_id=tmp[5], start=tmp[6], end=tmp[7]) + else: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' too many parameters') + + scene_objects.append(tl) + + # Receiver + cmdname = '#rx' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 3 and len(tmp) < 5: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' has an incorrect number of parameters') + if len(tmp) == 3: + rx = Rx(p1=(float(tmp[0]), float(tmp[1]), float(tmp[2]))) + else: + rx = Rx(p1=(float(tmp[0]), float(tmp[1]), float(tmp[2])), id=tmp[3], outputs=tmp[4:]) + scene_objects.append(rx) + + # Receiver array + cmdname = '#rx_array' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 9: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly nine parameters') + + p1 = (float(tmp[0], float(tmp[1]), float[tmp[2]])) + p2 = (float(tmp[3], float(tmp[4]), float[tmp[5]])) + dl = (float(tmp[6], float(tmp[7]), float[tmp[8]])) + + rx_array = RxArray(p1=p1, p2=p2, dl=dl) + scene_objects.append(rx_array) + + # Snapshot + cmdname = '#snapshot' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 11: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly eleven parameters') + + p1 = (float(tmp[0], float(tmp[1]), float[tmp[2]])) + p2 = (float(tmp[3], float(tmp[4]), float[tmp[5]])) + dl = (float(tmp[6], float(tmp[7]), float[tmp[8]])) + filename = tmp[10] + + try: + iterations = int(tmp[9]) + snapshot = Snapshot(p1=p1, p2=p2, dl=dl, iterations=iterations, filename=filename) + + except ValueError: + time = float(tmp[9]) + snapshot = Snapshot(p1=p1, p2=p2, dl=dl, time=time, filename=filename) + + scene_objects.append(snapshot) + + # Materials + cmdname = '#material' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 5: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly five parameters') + + material = Material(er=float(tmp[0]), se=float(tmp[1]), mr=float(tmp[2]), sm=float(tmp[3]), id=tmp[4]) + scene_objects.append(material) + + cmdname = '#add_dispersion_debye' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + + if len(tmp) < 4: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least four parameters') + + poles = int(tmp[0]) + er_delta = [] + tau = [] + material_ids = tmp[(2 * poles) + 1:len(tmp)] + + for pole in range(1, 2 * poles, 2): + er_delta.append(float(tmp[pole])) + tau.append(float(tmp[pole + 1])) + + debye_dispersion = AddDebyeDispersion(pole=poles, er_delta=er_delta, tau=tau, material_ids=material_ids) + scene_objects.append(debye_dispersion) + + cmdname = '#add_dispersion_lorentz' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + + if len(tmp) < 5: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') + + poles = int(tmp[0]) + material_ids = tmp[(3 * poles) + 1:len(tmp)] + er_delta = [] + tau = [] + alpha = [] + + for pole in range(1, 3 * poles, 3): + er_delta.append(float(tmp[pole])) + tau.append(float(tmp[pole + 1])) + alpha.append(float(tmp[pole + 2])) + + lorentz_dispersion = AddLorentzDispersion(poles=poles, material_ids=material_ids, er_delta=er_delta, tau=tau, alpha=alpha) + scene_objects.append(lorentz_dispersion) + + cmdname = '#add_dispersion_drude' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + + if len(tmp) < 5: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at least five parameters') + + poles = int(tmp[0]) + material_ids = tmp[(3 * poles) + 1:len(tmp)] + tau = [] + alpha = [] + + for pole in range(1, 2 * poles, 2): + tau.append(float(tmp[pole])) + alpha.append(float(tmp[pole + 1])) + + drude_dispersion = AddDrudeDispersion(poles=poles, material_ids=material_ids, tau=tau, alpha=alpha) + scene_objects.append(drude_dispersion) + + cmdname = '#soil_peplinski' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + + if len(tmp) != 7: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires at exactly seven parameters') + soil = SoilPeplinski(sand_fraction=float(tmp[0]), + clay_fraction=float(tmp[1]), + bulk_density=float(tmp[2]), + sand_density=float(tmp[3]), + water_fraction_lower=float(tmp[4]), + water_fraction_upper=float(tmp[5]), + id=tmp[6]) + scene_objects.append(soil) + + # Geometry views (creates VTK-based geometry files) + cmdname = '#geometry_view' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 11: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly eleven parameters') + + p1 = float(tmp[0]), float(tmp[1]), float(tmp[2]) + p2 = float(tmp[3]), float(tmp[4]), float(tmp[5]) + dl = float(tmp[6]), float(tmp[7]), float(tmp[8]) + + geometry_view = GeometryView(p1=p1, p2=p2, dl=dl, filename=tmp[9], output_type=tmp[10]) + + scene_objects.append(geometry_view) + + # Geometry object(s) output + cmdname = '#geometry_objects_write' + if multicmds[cmdname] is not None: + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 7: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly seven parameters') + + p1 = float(tmp[0]), float(tmp[1]), float(tmp[2]) + p2 = float(tmp[3]), float(tmp[4]), float(tmp[5]) + gow = GeometryObjectsWrite(p1=p1, p2=p2, filename=tmp[6]) + scene_objects.append(gow) + + + # Complex frequency shifted (CFS) PML parameter + cmdname = '#pml_cfs' + if multicmds[cmdname] is not None: + if len(multicmds[cmdname]) > 2: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' can only be used up to two times, for up to a 2nd order PML') + for cmdinstance in multicmds[cmdname]: + tmp = cmdinstance.split() + if len(tmp) != 12: + raise CmdInputError("'" + cmdname + ': ' + ' '.join(tmp) + "'" + ' requires exactly twelve parameters') + + pml_cfs = PMLCFS(alphascalingprofile=tmp[0], + alphascalingdirection=tmp[1], + alphamin=tmp[2], + alphamax=tmp[3], + kappascalingprofile=tmp[4], + kappascalingdirection=tmp[5], + kappamin=tmp[6], + kappamax=tmp[7], + sigmascalingprofile=tmp[8], + sigmascalingdirection=tmp[9], + sigmamin=tmp[10], + sigmamax=tmp[11]) + + scene_objects.append(pml_cfs) + + return scene_objects + +def process_subgrid_hsg(cmdinstance): + + pass diff --git a/gprMax/model_build_run.py b/gprMax/model_build_run.py index e9af5b99..4f7a708f 100644 --- a/gprMax/model_build_run.py +++ b/gprMax/model_build_run.py @@ -1,364 +1,364 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -import datetime -from importlib import import_module -import itertools -import os -import psutil -import sys - -from colorama import init -from colorama import Fore -from colorama import Style -init() -import cython -import numpy as np -from terminaltables import SingleTable -from tqdm import tqdm -from pathlib import Path - -import gprMax.config as config -from .cuda.fields_updates import kernel_template_fields -from .cuda.snapshots import kernel_template_store_snapshot -from .cuda.source_updates import kernel_template_sources -from .cython.yee_cell_build import build_electric_components -from .cython.yee_cell_build import build_magnetic_components -from .exceptions import GeneralError -from .fields_outputs import kernel_template_store_outputs -from .fields_outputs import write_hdf5_outputfile -from .grid import FDTDGrid -from .grid import dispersion_analysis -from .input_cmds_geometry import process_geometrycmds -from .input_cmds_file import process_python_include_code -from .input_cmds_file import write_processed_file -from .input_cmds_file import check_cmd_names -from .input_cmds_file import parse_hash_commands -from .input_cmds_singleuse import process_singlecmds -from .input_cmds_multiuse import process_multicmds -from .materials import Material -from .materials import process_materials -from .pml import CFS -from .pml import PML -from .pml import build_pml -from .pml import pml_information -from .receivers import gpu_initialise_rx_arrays -from .receivers import gpu_get_rx_array -from .receivers import Rx -from .snapshots import Snapshot -from .snapshots import gpu_initialise_snapshot_array -from .snapshots import gpu_get_snapshot_array -from .sources import gpu_initialise_src_arrays -from .utilities import get_terminal_width -from .utilities import human_size -from .utilities import open_path_file -from .utilities import round32 -from .utilities import timer -from .utilities import Printer -from .scene import Scene -from .solvers import create_solver - - - -class ModelBuildRun: - - def __init__(self, G, sim_config, model_config): - self.G = G - self.sim_config = sim_config - self.model_config = model_config - self.printer = Printer(config) - # Monitor memory usage - self.p = None - - def build(self): - """Runs a model - processes the input file; builds the Yee cells; calculates update coefficients; runs main FDTD loop. - - Args: - args (dict): Namespace with command line arguments - currentmodelrun (int): Current model run number. - modelend (int): Number of last model to run. - numbermodelruns (int): Total number of model runs. - inputfile (object): File object for the input file. - usernamespace (dict): Namespace that can be accessed by user - in any Python code blocks in input file. - - Returns: - tsolve (int): Length of time (seconds) of main FDTD calculations - """ - # Monitor memory usage - self.p = psutil.Process() - - # Normal model reading/building process; bypassed if geometry information to be reused - if self.model_config.reuse_geometry: - self.reuse_geometry() - else: - self.build_geometry() - - G = self.G - - # Adjust position of simple sources and receivers if required - if G.srcsteps[0] != 0 or G.srcsteps[1] != 0 or G.srcsteps[2] != 0: - for source in itertools.chain(G.hertziandipoles, G.magneticdipoles): - if currentmodelrun == 1: - if source.xcoord + G.srcsteps[0] * modelend < 0 or source.xcoord + G.srcsteps[0] * modelend > G.nx or source.ycoord + G.srcsteps[1] * modelend < 0 or source.ycoord + G.srcsteps[1] * modelend > G.ny or source.zcoord + G.srcsteps[2] * modelend < 0 or source.zcoord + G.srcsteps[2] * modelend > G.nz: - raise GeneralError('Source(s) will be stepped to a position outside the domain.') - source.xcoord = source.xcoordorigin + (currentmodelrun - 1) * G.srcsteps[0] - source.ycoord = source.ycoordorigin + (currentmodelrun - 1) * G.srcsteps[1] - source.zcoord = source.zcoordorigin + (currentmodelrun - 1) * G.srcsteps[2] - if G.rxsteps[0] != 0 or G.rxsteps[1] != 0 or G.rxsteps[2] != 0: - for receiver in G.rxs: - if currentmodelrun == 1: - if receiver.xcoord + G.rxsteps[0] * modelend < 0 or receiver.xcoord + G.rxsteps[0] * modelend > G.nx or receiver.ycoord + G.rxsteps[1] * modelend < 0 or receiver.ycoord + G.rxsteps[1] * modelend > G.ny or receiver.zcoord + G.rxsteps[2] * modelend < 0 or receiver.zcoord + G.rxsteps[2] * modelend > G.nz: - raise GeneralError('Receiver(s) will be stepped to a position outside the domain.') - receiver.xcoord = receiver.xcoordorigin + (currentmodelrun - 1) * G.rxsteps[0] - receiver.ycoord = receiver.ycoordorigin + (currentmodelrun - 1) * G.rxsteps[1] - receiver.zcoord = receiver.zcoordorigin + (currentmodelrun - 1) * G.rxsteps[2] - - # Write files for any geometry views and geometry object outputs - if not (G.geometryviews or G.geometryobjectswrite) and self.sim_config.geometry_only and config.is_messages(): - print(Fore.RED + '\nWARNING: No geometry views or geometry objects to output found.' + Style.RESET_ALL) - if config.is_messages(): print() - for i, geometryview in enumerate(G.geometryviews): - geometryview.set_filename(self.model_config.appendmodelnumber) - pbar = tqdm(total=geometryview.datawritesize, unit='byte', unit_scale=True, desc='Writing geometry view file {}/{}, {}'.format(i + 1, len(G.geometryviews), os.path.split(geometryview.filename)[1]), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) - geometryview.write_vtk(G, pbar) - pbar.close() - for i, geometryobject in enumerate(G.geometryobjectswrite): - pbar = tqdm(total=geometryobject.datawritesize, unit='byte', unit_scale=True, desc='Writing geometry object file {}/{}, {}'.format(i + 1, len(G.geometryobjectswrite), os.path.split(geometryobject.filename)[1]), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) - geometryobject.write_hdf5(G, pbar) - pbar.close() - - # If only writing geometry information - if self.sim_config.geometry_only: - tsolve = 0 - - def build_geometry(self): - model_config = self.model_config - sim_config = self.sim_config - - G = self.G - - printer = Printer(config) - printer.print(model_config.next_model) - - scene = self.build_scene() - - # combine available grids - grids = [G] + G.subgrids - gridbuilders = [GridBuilder(grid, self.printer) for grid in grids] - - for gb in gridbuilders: - gb.printer.print(pml_information(gb.grid)) - gb.build_pmls() - gb.build_components() - gb.tm_grid_update() - gb.update_voltage_source_materials() - gb.grid.initialise_std_update_coeff_arrays() - - # Set datatype for dispersive arrays if there are any dispersive materials. - if 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'] - config.materials['dispersiveCdtype'] = config.dtypes['C_complex'] - else: - config.materials['dispersivedtype'] = config.dtypes['float_or_double'] - config.materials['dispersiveCdtype'] = config.dtypes['C_float_or_double'] - - # Update estimated memory (RAM) usage - G.memoryusage += int(3 * config.materials['maxpoles'] * (G.nx + 1) * (G.ny + 1) * (G.nz + 1) * np.dtype(config.materials['dispersivedtype']).itemsize) - G.memory_check() - printer.print('\nMemory (RAM) required - updated (dispersive): ~{}\n'.format(human_size(G.memoryusage))) - - for gb in gridbuilders: - gb.grid.initialise_dispersive_arrays(config.materials['dispersivedtype']) - - # Check there is sufficient memory to store any snapshots - if G.snapshots: - snapsmemsize = 0 - for snap in G.snapshots: - # 2 x required to account for electric and magnetic fields - snapsmemsize += (2 * snap.datasizefield) - G.memoryusage += int(snapsmemsize) - G.memory_check(snapsmemsize=int(snapsmemsize)) - - printer.print('\nMemory (RAM) required - updated (snapshots): ~{}\n'.format(human_size(G.memoryusage))) - - for gb in gridbuilders: - gb.build_materials() - - # Check to see if numerical dispersion might be a problem - results = dispersion_analysis(G) - if results['error']: - printer.print(Fore.RED + "\nWARNING: Numerical dispersion analysis not carried out as {}".format(results['error']) + Style.RESET_ALL) - elif results['N'] < config.numdispersion['mingridsampling']: - raise GeneralError("Non-physical wave propagation: Material '{}' has wavelength sampled by {} cells, less than required minimum for physical wave propagation. Maximum significant frequency estimated as {:g}Hz".format(results['material'].ID, results['N'], results['maxfreq'])) - elif results['deltavp'] and np.abs(results['deltavp']) > config.numdispersion['maxnumericaldisp']: - printer.print(Fore.RED + "\nWARNING: Potentially significant numerical dispersion. Estimated largest physical phase-velocity error is {:.2f}% in material '{}' whose wavelength sampled by {} cells. Maximum significant frequency estimated as {:g}Hz".format(results['deltavp'], results['material'].ID, results['N'], results['maxfreq']) + Style.RESET_ALL) - elif results['deltavp']: - printer.print("\nNumerical dispersion analysis: estimated largest physical phase-velocity error is {:.2f}% in material '{}' whose wavelength sampled by {} cells. Maximum significant frequency estimated as {:g}Hz".format(results['deltavp'], results['material'].ID, results['N'], results['maxfreq'])) - - - def reuse_geometry(self): - G = self.G - 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 + '{} {}\n'.format(self.model_config.inputfilestr, '-' * (get_terminal_width() - 1 - len(inputfilestr))) + Style.RESET_ALL) - for grid in [G] + G.subgrids: - grid.reset_fields() - - def build_scene(self): - G = self.G - # api for multiple scenes / model runs - scene = self.model_config.get_scene() - - # if there is no scene - process the hash commands instead - if not scene: - scene = Scene() - # parse the input file into user objects and add them to the scene - scene = parse_hash_commands(self.model_config, G, scene) - - # Creates the internal simulation objects. - scene.create_internal_objects(G) - return scene - - def create_output_directory(self): - - if self.G.outputdirectory: - # Check and set output directory and filename - try: - os.mkdir(self.G.outputdirectory) - self.printer.print('\nCreated output directory: {}'.format(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('\nOutput file: {}\n'.format(self.model_config.output_file_path_ext)) - - tsolve = self.solve(solver) - - # 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 - snapshotdir = self.model_config.snapshot_dir - if not os.path.exists(snapshotdir): - os.mkdir(snapshotdir) - - self.printer.print() - for i, snap in enumerate(G.snapshots): - snap.filename = snapshotdir + snap.basefilename + '.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() - - memGPU = '' - if config.cuda['gpus']: - memGPU = ' host + ~{} GPU'.format(human_size(self.solver.get_memsolve())) - - 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 - - def solve(self, solver): - """ - Solving using FDTD method on CPU. Parallelised using Cython (OpenMP) for - electric and magnetic field updates, and PML updates. - - Args: - currentmodelrun (int): Current model run number. - modelend (int): Number of last model to run. - G (class): Grid class instance - holds essential parameters describing the model. - - Returns: - tsolve (float): Time taken to execute solving (seconds) - """ - G = self.G - - if config.is_messages(): - iterator = tqdm(range(G.iterations), desc='Running simulation, model ' + str(self.model_config - .i + 1) + '/' + str(self.sim_config.model_end), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) - else: - iterator = range(0, G.iterations) - - tsolve = solver.solve(iterator) - - return tsolve - - -class GridBuilder: - def __init__(self, grid, printer): - self.grid = grid - self.printer = printer - - def build_pmls(self): - - grid = self.grid - # build the PMLS - pbar = tqdm(total=sum(1 for value in grid.pmlthickness.values() if value > 0), desc='Building {} Grid PML boundaries'.format(self.grid.name), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) - - for pml_id, thickness in grid.pmlthickness.items(): - build_pml(grid, pml_id, thickness) - pbar.update() - pbar.close() - - def build_components(self): - # Build the model, i.e. set the material properties (ID) for every edge - # of every Yee cell - self.printer.print('') - 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() - build_magnetic_components(self.grid.solid, self.grid.rigidH, self.grid.ID, self.grid) - pbar.update() - pbar.close() - - def tm_grid_update(self): - if '2D TMx' == config.general['mode']: - self.grid.tmx() - elif '2D TMy' == config.general['mode']: - self.grid.tmy() - elif '2D TMz' == config.general['mode']: - self.grid.tmz() - - def update_voltage_source_materials(self): - # Process any voltage sources (that have resistance) to create a new - # material at the source location - for voltagesource in self.grid.voltagesources: - voltagesource.create_material(self.grid) - - def build_materials(self): - # Process complete list of materials - calculate update coefficients, - # store in arrays, and build text list of materials/properties - materialsdata = process_materials(self.grid) - materialstable = SingleTable(materialsdata) - materialstable.outer_border = False - materialstable.justify_columns[0] = 'right' - - self.printer.print('\n{} Grid Materials:'.format(self.grid.name)) - self.printer.print(materialstable.table) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import datetime +from importlib import import_module +import itertools +import os +import psutil +import sys + +from colorama import init +from colorama import Fore +from colorama import Style +init() +import cython +import numpy as np +from terminaltables import SingleTable +from tqdm import tqdm +from pathlib import Path + +import gprMax.config as config +from .cuda.fields_updates import kernel_template_fields +from .cuda.snapshots import kernel_template_store_snapshot +from .cuda.source_updates import kernel_template_sources +from .cython.yee_cell_build import build_electric_components +from .cython.yee_cell_build import build_magnetic_components +from .exceptions import GeneralError +from .fields_outputs import kernel_template_store_outputs +from .fields_outputs import write_hdf5_outputfile +from .grid import FDTDGrid +from .grid import dispersion_analysis +from .input_cmds_geometry import process_geometrycmds +from .input_cmds_file import process_python_include_code +from .input_cmds_file import write_processed_file +from .input_cmds_file import check_cmd_names +from .input_cmds_file import parse_hash_commands +from .input_cmds_singleuse import process_singlecmds +from .input_cmds_multiuse import process_multicmds +from .materials import Material +from .materials import process_materials +from .pml import CFS +from .pml import PML +from .pml import build_pml +from .pml import pml_information +from .receivers import gpu_initialise_rx_arrays +from .receivers import gpu_get_rx_array +from .receivers import Rx +from .snapshots import Snapshot +from .snapshots import gpu_initialise_snapshot_array +from .snapshots import gpu_get_snapshot_array +from .sources import gpu_initialise_src_arrays +from .utilities import get_terminal_width +from .utilities import human_size +from .utilities import open_path_file +from .utilities import round32 +from .utilities import timer +from .utilities import Printer +from .scene import Scene +from .solvers import create_solver + + + +class ModelBuildRun: + + def __init__(self, G, sim_config, model_config): + self.G = G + self.sim_config = sim_config + self.model_config = model_config + self.printer = Printer(config) + # Monitor memory usage + self.p = None + + def build(self): + """Runs a model - processes the input file; builds the Yee cells; calculates update coefficients; runs main FDTD loop. + + Args: + args (dict): Namespace with command line arguments + currentmodelrun (int): Current model run number. + modelend (int): Number of last model to run. + numbermodelruns (int): Total number of model runs. + inputfile (object): File object for the input file. + usernamespace (dict): Namespace that can be accessed by user + in any Python code blocks in input file. + + Returns: + tsolve (int): Length of time (seconds) of main FDTD calculations + """ + # Monitor memory usage + self.p = psutil.Process() + + # Normal model reading/building process; bypassed if geometry information to be reused + if self.model_config.reuse_geometry: + self.reuse_geometry() + else: + self.build_geometry() + + G = self.G + + # Adjust position of simple sources and receivers if required + if G.srcsteps[0] != 0 or G.srcsteps[1] != 0 or G.srcsteps[2] != 0: + for source in itertools.chain(G.hertziandipoles, G.magneticdipoles): + if currentmodelrun == 1: + if source.xcoord + G.srcsteps[0] * modelend < 0 or source.xcoord + G.srcsteps[0] * modelend > G.nx or source.ycoord + G.srcsteps[1] * modelend < 0 or source.ycoord + G.srcsteps[1] * modelend > G.ny or source.zcoord + G.srcsteps[2] * modelend < 0 or source.zcoord + G.srcsteps[2] * modelend > G.nz: + raise GeneralError('Source(s) will be stepped to a position outside the domain.') + source.xcoord = source.xcoordorigin + (currentmodelrun - 1) * G.srcsteps[0] + source.ycoord = source.ycoordorigin + (currentmodelrun - 1) * G.srcsteps[1] + source.zcoord = source.zcoordorigin + (currentmodelrun - 1) * G.srcsteps[2] + if G.rxsteps[0] != 0 or G.rxsteps[1] != 0 or G.rxsteps[2] != 0: + for receiver in G.rxs: + if currentmodelrun == 1: + if receiver.xcoord + G.rxsteps[0] * modelend < 0 or receiver.xcoord + G.rxsteps[0] * modelend > G.nx or receiver.ycoord + G.rxsteps[1] * modelend < 0 or receiver.ycoord + G.rxsteps[1] * modelend > G.ny or receiver.zcoord + G.rxsteps[2] * modelend < 0 or receiver.zcoord + G.rxsteps[2] * modelend > G.nz: + raise GeneralError('Receiver(s) will be stepped to a position outside the domain.') + receiver.xcoord = receiver.xcoordorigin + (currentmodelrun - 1) * G.rxsteps[0] + receiver.ycoord = receiver.ycoordorigin + (currentmodelrun - 1) * G.rxsteps[1] + receiver.zcoord = receiver.zcoordorigin + (currentmodelrun - 1) * G.rxsteps[2] + + # Write files for any geometry views and geometry object outputs + if not (G.geometryviews or G.geometryobjectswrite) and self.sim_config.geometry_only and config.is_messages(): + print(Fore.RED + '\nWARNING: No geometry views or geometry objects to output found.' + Style.RESET_ALL) + if config.is_messages(): print() + for i, geometryview in enumerate(G.geometryviews): + geometryview.set_filename(self.model_config.appendmodelnumber) + pbar = tqdm(total=geometryview.datawritesize, unit='byte', unit_scale=True, desc='Writing geometry view file {}/{}, {}'.format(i + 1, len(G.geometryviews), os.path.split(geometryview.filename)[1]), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) + geometryview.write_vtk(G, pbar) + pbar.close() + for i, geometryobject in enumerate(G.geometryobjectswrite): + pbar = tqdm(total=geometryobject.datawritesize, unit='byte', unit_scale=True, desc='Writing geometry object file {}/{}, {}'.format(i + 1, len(G.geometryobjectswrite), os.path.split(geometryobject.filename)[1]), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) + geometryobject.write_hdf5(G, pbar) + pbar.close() + + # If only writing geometry information + if self.sim_config.geometry_only: + tsolve = 0 + + def build_geometry(self): + model_config = self.model_config + sim_config = self.sim_config + + G = self.G + + printer = Printer(config) + printer.print(model_config.next_model) + + scene = self.build_scene() + + # combine available grids + grids = [G] + G.subgrids + gridbuilders = [GridBuilder(grid, self.printer) for grid in grids] + + for gb in gridbuilders: + gb.printer.print(pml_information(gb.grid)) + gb.build_pmls() + gb.build_components() + gb.tm_grid_update() + gb.update_voltage_source_materials() + gb.grid.initialise_std_update_coeff_arrays() + + # Set datatype for dispersive arrays if there are any dispersive materials. + if 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'] + config.materials['dispersiveCdtype'] = config.dtypes['C_complex'] + else: + config.materials['dispersivedtype'] = config.dtypes['float_or_double'] + config.materials['dispersiveCdtype'] = config.dtypes['C_float_or_double'] + + # Update estimated memory (RAM) usage + G.memoryusage += int(3 * config.materials['maxpoles'] * (G.nx + 1) * (G.ny + 1) * (G.nz + 1) * np.dtype(config.materials['dispersivedtype']).itemsize) + G.memory_check() + printer.print('\nMemory (RAM) required - updated (dispersive): ~{}\n'.format(human_size(G.memoryusage))) + + for gb in gridbuilders: + gb.grid.initialise_dispersive_arrays(config.materials['dispersivedtype']) + + # Check there is sufficient memory to store any snapshots + if G.snapshots: + snapsmemsize = 0 + for snap in G.snapshots: + # 2 x required to account for electric and magnetic fields + snapsmemsize += (2 * snap.datasizefield) + G.memoryusage += int(snapsmemsize) + G.memory_check(snapsmemsize=int(snapsmemsize)) + + printer.print('\nMemory (RAM) required - updated (snapshots): ~{}\n'.format(human_size(G.memoryusage))) + + for gb in gridbuilders: + gb.build_materials() + + # Check to see if numerical dispersion might be a problem + results = dispersion_analysis(G) + if results['error']: + printer.print(Fore.RED + "\nWARNING: Numerical dispersion analysis not carried out as {}".format(results['error']) + Style.RESET_ALL) + elif results['N'] < config.numdispersion['mingridsampling']: + raise GeneralError("Non-physical wave propagation: Material '{}' has wavelength sampled by {} cells, less than required minimum for physical wave propagation. Maximum significant frequency estimated as {:g}Hz".format(results['material'].ID, results['N'], results['maxfreq'])) + elif results['deltavp'] and np.abs(results['deltavp']) > config.numdispersion['maxnumericaldisp']: + printer.print(Fore.RED + "\nWARNING: Potentially significant numerical dispersion. Estimated largest physical phase-velocity error is {:.2f}% in material '{}' whose wavelength sampled by {} cells. Maximum significant frequency estimated as {:g}Hz".format(results['deltavp'], results['material'].ID, results['N'], results['maxfreq']) + Style.RESET_ALL) + elif results['deltavp']: + printer.print("\nNumerical dispersion analysis: estimated largest physical phase-velocity error is {:.2f}% in material '{}' whose wavelength sampled by {} cells. Maximum significant frequency estimated as {:g}Hz".format(results['deltavp'], results['material'].ID, results['N'], results['maxfreq'])) + + + def reuse_geometry(self): + G = self.G + 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 + '{} {}\n'.format(self.model_config.inputfilestr, '-' * (get_terminal_width() - 1 - len(inputfilestr))) + Style.RESET_ALL) + for grid in [G] + G.subgrids: + grid.reset_fields() + + def build_scene(self): + G = self.G + # api for multiple scenes / model runs + scene = self.model_config.get_scene() + + # if there is no scene - process the hash commands instead + if not scene: + scene = Scene() + # parse the input file into user objects and add them to the scene + scene = parse_hash_commands(self.model_config, G, scene) + + # Creates the internal simulation objects. + scene.create_internal_objects(G) + return scene + + def create_output_directory(self): + + if self.G.outputdirectory: + # Check and set output directory and filename + try: + os.mkdir(self.G.outputdirectory) + self.printer.print('\nCreated output directory: {}'.format(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('\nOutput file: {}\n'.format(self.model_config.output_file_path_ext)) + + tsolve = self.solve(solver) + + # 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 + snapshotdir = self.model_config.snapshot_dir + if not os.path.exists(snapshotdir): + os.mkdir(snapshotdir) + + self.printer.print() + for i, snap in enumerate(G.snapshots): + snap.filename = snapshotdir + snap.basefilename + '.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() + + memGPU = '' + if config.cuda['gpus']: + memGPU = ' host + ~{} GPU'.format(human_size(self.solver.get_memsolve())) + + 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 + + def solve(self, solver): + """ + Solving using FDTD method on CPU. Parallelised using Cython (OpenMP) for + electric and magnetic field updates, and PML updates. + + Args: + currentmodelrun (int): Current model run number. + modelend (int): Number of last model to run. + G (class): Grid class instance - holds essential parameters describing the model. + + Returns: + tsolve (float): Time taken to execute solving (seconds) + """ + G = self.G + + if config.is_messages(): + iterator = tqdm(range(G.iterations), desc='Running simulation, model ' + str(self.model_config + .i + 1) + '/' + str(self.sim_config.model_end), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) + else: + iterator = range(0, G.iterations) + + tsolve = solver.solve(iterator) + + return tsolve + + +class GridBuilder: + def __init__(self, grid, printer): + self.grid = grid + self.printer = printer + + def build_pmls(self): + + grid = self.grid + # build the PMLS + pbar = tqdm(total=sum(1 for value in grid.pmlthickness.values() if value > 0), desc='Building {} Grid PML boundaries'.format(self.grid.name), ncols=get_terminal_width() - 1, file=sys.stdout, disable=not config.general['progressbars']) + + for pml_id, thickness in grid.pmlthickness.items(): + build_pml(grid, pml_id, thickness) + pbar.update() + pbar.close() + + def build_components(self): + # Build the model, i.e. set the material properties (ID) for every edge + # of every Yee cell + self.printer.print('') + 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() + build_magnetic_components(self.grid.solid, self.grid.rigidH, self.grid.ID, self.grid) + pbar.update() + pbar.close() + + def tm_grid_update(self): + if '2D TMx' == config.general['mode']: + self.grid.tmx() + elif '2D TMy' == config.general['mode']: + self.grid.tmy() + elif '2D TMz' == config.general['mode']: + self.grid.tmz() + + def update_voltage_source_materials(self): + # Process any voltage sources (that have resistance) to create a new + # material at the source location + for voltagesource in self.grid.voltagesources: + voltagesource.create_material(self.grid) + + def build_materials(self): + # Process complete list of materials - calculate update coefficients, + # store in arrays, and build text list of materials/properties + materialsdata = process_materials(self.grid) + materialstable = SingleTable(materialsdata) + materialstable.outer_border = False + materialstable.justify_columns[0] = 'right' + + self.printer.print('\n{} Grid Materials:'.format(self.grid.name)) + self.printer.print(materialstable.table) diff --git a/gprMax/scene.py b/gprMax/scene.py index a298b887..20ef2125 100644 --- a/gprMax/scene.py +++ b/gprMax/scene.py @@ -1,121 +1,121 @@ -from .user_inputs import create_user_input_points -from .materials import create_built_in_materials -from .cmds_single_use import UserObjectSingle -from .cmds_single_use import DomainSingle -from .cmds_single_use import Discretisation -from .cmds_single_use import TimeWindow -from .cmds_multiple import UserObjectMulti -from .subgrids.user_objects import SubGridBase as SubGridUserBase -from .cmds_geometry.cmds_geometry import UserObjectGeometry -from .exceptions import CmdInputError -from .cmds_geometry.fractal_box_builder import FractalBoxBuilder -from .utilities import human_size - - -class Scene: - - """Scene stores all of the user created objects - """ - - def __init__(self): - self.multiple_cmds = [] - self.single_cmds = [] - self.geometry_cmds = [] - self.essential_cmds = [DomainSingle, TimeWindow, Discretisation] - - # fractal box commands have an additional nonuser object which - # process modifications - fbb = FractalBoxBuilder() - self.add(fbb) - - def add(self, node): - if isinstance(node, UserObjectMulti): - self.multiple_cmds.append(node) - elif isinstance(node, UserObjectGeometry): - self.geometry_cmds.append(node) - elif isinstance(node, UserObjectSingle): - self.single_cmds.append(node) - else: - raise Exception('This Object is Unknown to gprMax') - - def process_subgrid_commands(self, subgrids): - - # check for subgrid user objects - def func(obj): - if isinstance(obj, SubGridUserBase): - return True - else: - return False - - # subgrid user objects - subgrid_cmds = list(filter(func, self.multiple_cmds)) - - # iterate through the user command objects under the subgrid user object - for sg_cmd in subgrid_cmds: - # when the subgrid is created its reference is attached to its user - # object. this reference allows the multi and geo user objects - # to build in the correct subgrid. - sg = sg_cmd.subgrid - self.process_cmds(sg_cmd.children_multiple, sg) - self.process_cmds(sg_cmd.children_geometry, sg) - - def process_cmds(self, commands, grid): - cmds_sorted = sorted(commands, key=lambda cmd: cmd.order) - for obj in cmds_sorted: - # in the first level all objects belong to the main grid - uip = create_user_input_points(grid) - # Create an instance to check the geometry points provided by the - # user. The way the point are checked depends on which grid the - # points belong to. - obj.create(grid, uip) - - return self - - def process_singlecmds(self, G): - - # check for duplicate commands and warn user if they exist - cmds_unique = list(set(self.single_cmds)) - if len(cmds_unique) != len(self.single_cmds): - raise CmdInputError('Duplicate Single Commands exist in the input.') - - # check essential cmds and warn user if missing - for cmd_type in self.essential_cmds: - d = any([isinstance(cmd, cmd_type) for cmd in cmds_unique]) - if not d: - raise CmdInputError('Your input file is missing essential commands required to run a model. Essential commands are: Domain, Discretisation, Time Window') - - self.process_cmds(cmds_unique, G) - - - def create_internal_objects(self, G): - - # gprMax API presents the user with UserObjects in order to build - # the internal Rx(), Cylinder() etc... objects. This function - # essentially calls the UserObject.create() function in the correct - # way - - # Traverse all the user objects in the correct order and create them. - create_built_in_materials(G) - - # process commands that can onlyhave a single instance - self.process_singlecmds(G) - - # Process main grid multiple commands - self.process_cmds(self.multiple_cmds, G) - - # Estimate and check memory (RAM) usage - G.memory_check() - #snapshot_memory_check(G) - - # Initialise an array for volumetric material IDs (solid), boolean - # arrays for specifying materials not to be averaged (rigid), - # an array for cell edge IDs (ID) - G.initialise_grids() - - # Process the main grid geometry commands - self.process_cmds(self.geometry_cmds, G) - - # Process all the commands for the subgrid - self.process_subgrid_commands(G.subgrids) - - return self +from .user_inputs import create_user_input_points +from .materials import create_built_in_materials +from .cmds_single_use import UserObjectSingle +from .cmds_single_use import DomainSingle +from .cmds_single_use import Discretisation +from .cmds_single_use import TimeWindow +from .cmds_multiple import UserObjectMulti +from .subgrids.user_objects import SubGridBase as SubGridUserBase +from .cmds_geometry.cmds_geometry import UserObjectGeometry +from .exceptions import CmdInputError +from .cmds_geometry.fractal_box_builder import FractalBoxBuilder +from .utilities import human_size + + +class Scene: + + """Scene stores all of the user created objects + """ + + def __init__(self): + self.multiple_cmds = [] + self.single_cmds = [] + self.geometry_cmds = [] + self.essential_cmds = [DomainSingle, TimeWindow, Discretisation] + + # fractal box commands have an additional nonuser object which + # process modifications + fbb = FractalBoxBuilder() + self.add(fbb) + + def add(self, node): + if isinstance(node, UserObjectMulti): + self.multiple_cmds.append(node) + elif isinstance(node, UserObjectGeometry): + self.geometry_cmds.append(node) + elif isinstance(node, UserObjectSingle): + self.single_cmds.append(node) + else: + raise Exception('This Object is Unknown to gprMax') + + def process_subgrid_commands(self, subgrids): + + # check for subgrid user objects + def func(obj): + if isinstance(obj, SubGridUserBase): + return True + else: + return False + + # subgrid user objects + subgrid_cmds = list(filter(func, self.multiple_cmds)) + + # iterate through the user command objects under the subgrid user object + for sg_cmd in subgrid_cmds: + # when the subgrid is created its reference is attached to its user + # object. this reference allows the multi and geo user objects + # to build in the correct subgrid. + sg = sg_cmd.subgrid + self.process_cmds(sg_cmd.children_multiple, sg) + self.process_cmds(sg_cmd.children_geometry, sg) + + def process_cmds(self, commands, grid): + cmds_sorted = sorted(commands, key=lambda cmd: cmd.order) + for obj in cmds_sorted: + # in the first level all objects belong to the main grid + uip = create_user_input_points(grid) + # Create an instance to check the geometry points provided by the + # user. The way the point are checked depends on which grid the + # points belong to. + obj.create(grid, uip) + + return self + + def process_singlecmds(self, G): + + # check for duplicate commands and warn user if they exist + cmds_unique = list(set(self.single_cmds)) + if len(cmds_unique) != len(self.single_cmds): + raise CmdInputError('Duplicate Single Commands exist in the input.') + + # check essential cmds and warn user if missing + for cmd_type in self.essential_cmds: + d = any([isinstance(cmd, cmd_type) for cmd in cmds_unique]) + if not d: + raise CmdInputError('Your input file is missing essential commands required to run a model. Essential commands are: Domain, Discretisation, Time Window') + + self.process_cmds(cmds_unique, G) + + + def create_internal_objects(self, G): + + # gprMax API presents the user with UserObjects in order to build + # the internal Rx(), Cylinder() etc... objects. This function + # essentially calls the UserObject.create() function in the correct + # way + + # Traverse all the user objects in the correct order and create them. + create_built_in_materials(G) + + # process commands that can onlyhave a single instance + self.process_singlecmds(G) + + # Process main grid multiple commands + self.process_cmds(self.multiple_cmds, G) + + # Estimate and check memory (RAM) usage + G.memory_check() + #snapshot_memory_check(G) + + # Initialise an array for volumetric material IDs (solid), boolean + # arrays for specifying materials not to be averaged (rigid), + # an array for cell edge IDs (ID) + G.initialise_grids() + + # Process the main grid geometry commands + self.process_cmds(self.geometry_cmds, G) + + # Process all the commands for the subgrid + self.process_subgrid_commands(G.subgrids) + + return self diff --git a/gprMax/solvers.py b/gprMax/solvers.py index a323f64f..9370a908 100644 --- a/gprMax/solvers.py +++ b/gprMax/solvers.py @@ -1,93 +1,96 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -from gprMax.updates import CPUUpdates -from gprMax.updates import GPUUpdates -from gprMax.utilities import timer -from .grid import FDTDGrid -from .grid import GPUGrid -import gprMax.config as config -from .subgrids.solver import create_updates as create_subgrid_updates -from .subgrids.solver import SubGridSolver - - -def create_G(sim_config): - """Returns the configured solver.""" - if sim_config.gpu: - G = GPUGrid() - elif sim_config.subgrid: - G = FDTDGrid() - else: - G = FDTDGrid() - - return G - - -def create_solver(G, sim_config): - """Returns the configured solver.""" - if sim_config.gpu: - updates = GPUUpdates(G) - solver = Solver(updates) - elif sim_config.subgrid: - updates = create_subgrid_updates(G) - solver = SubGridSolver(G, updates) - else: - updates = CPUUpdates(G) - solver = Solver(updates) - # a large range of function exist to advance the time step for dispersive - # materials. The correct function is set here based on the - # the required numerical precision and dispersive material type. - props = updates.adapt_dispersive_config(config) - updates.set_dispersive_updates(props) - return solver - - - -class Solver: - - """Generic solver for Update objects""" - - def __init__(self, updates): - """Context for the model to run in. Sub-class this with contexts - i.e. an MPI context. - - Args: - updates (Updates): updates contains methods to run FDTD algorithm - iterator (iterator): can be range() or tqdm() - """ - self.updates = updates - - def get_G(self): - return self.updates.G - - def solve(self, iterator): - """Time step the FDTD model.""" - tsolvestart = timer() - for iteration in iterator: - self.updates.grid.iteration = iteration - self.updates.store_outputs() - self.updates.store_snapshots(iteration) - self.updates.update_magnetic() - self.updates.update_magnetic_pml() - self.updates.update_magnetic_sources(iteration) - self.updates.update_electric_a() - self.updates.update_electric_pml() - self.updates.update_electric_sources(iteration) - self.updates.update_electric_b() - - tsolve = timer() - tsolvestart - return tsolve +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +from gprMax.updates import CPUUpdates +from gprMax.updates import GPUUpdates +from .subgrids.updates import create_updates as create_subgrid_updates +from gprMax.utilities import timer +from .grid import FDTDGrid +from .grid import GPUGrid +import gprMax.config as config + + +def create_G(sim_config): + """Returns the configured solver.""" + if sim_config.gpu: + G = GPUGrid() + elif sim_config.subgrid: + G = FDTDGrid() + else: + G = FDTDGrid() + + return G + + +def create_solver(G, sim_config): + """Returns the configured solver.""" + if sim_config.gpu: + updates = GPUUpdates(G) + solver = Solver(updates) + elif sim_config.subgrid: + updates = create_subgrid_updates(G) + solver = Solver(updates, hsg=True) + else: + updates = CPUUpdates(G) + solver = Solver(updates) + # a large range of function exist to advance the time step for dispersive + # materials. The correct function is set here based on the + # the required numerical precision and dispersive material type. + props = updates.adapt_dispersive_config(config) + updates.set_dispersive_updates(props) + return solver + + + +class Solver: + + """Generic solver for Update objects""" + + def __init__(self, updates, hsg=False): + """Context for the model to run in. Sub-class this with contexts + i.e. an MPI context. + + Args: + updates (Updates): updates contains methods to run FDTD algorithm + iterator (iterator): can be range() or tqdm() + """ + self.updates = updates + self.hsg = hsg + + def get_G(self): + return self.updates.G + + def solve(self, iterator): + """Time step the FDTD model.""" + tsolvestart = timer() + for iteration in iterator: + self.updates.store_outputs() + self.updates.store_snapshots(iteration) + self.updates.update_magnetic() + self.updates.update_magnetic_pml() + self.updates.update_magnetic_sources() + if self.hsg: + self.updates.hsg_2() + self.updates.update_electric_a() + self.updates.update_electric_pml() + self.updates.update_electric_sources() + if self.hsg: + self.updates.hsg_1() + self.updates.update_electric_b() + + tsolve = timer() - tsolvestart + return tsolve diff --git a/gprMax/sources.py b/gprMax/sources.py index a1a9039f..0bd15189 100644 --- a/gprMax/sources.py +++ b/gprMax/sources.py @@ -1,413 +1,413 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -from copy import deepcopy - -import numpy as np - -import gprMax.config as config -from .grid import Ix -from .grid import Iy -from .grid import Iz -from .utilities import round_value - - -class Source(object): - """Super-class which describes a generic source.""" - - def __init__(self): - self.ID = None - self.polarisation = None - self.xcoord = None - self.ycoord = None - self.zcoord = None - self.xcoordorigin = None - self.ycoordorigin = None - self.zcoordorigin = None - self.start = None - self.stop = None - self.waveformID = None - - def calculate_waveform_values(self, G): - """Calculates all waveform values for source for duration of simulation. - - Args: - G (class): Grid class instance - holds essential parameters describing the model. - """ - # Waveform values for electric sources - calculated half a timestep later - self.waveformvaluesJ = np.zeros((G.iterations), dtype=config.dtypes['float_or_double']) - - # Waveform values for magnetic sources - self.waveformvaluesM = np.zeros((G.iterations), dtype=config.dtypes['float_or_double']) - - waveform = next(x for x in G.waveforms if x.ID == self.waveformID) - - for iteration in range(G.iterations): - time = G.dt * iteration - if time >= self.start and time <= self.stop: - # Set the time of the waveform evaluation to account for any delay in the start - time -= self.start - self.waveformvaluesJ[iteration] = waveform.calculate_value(time + 0.5 * G.dt, G.dt) - self.waveformvaluesM[iteration] = waveform.calculate_value(time, G.dt) - - -class VoltageSource(Source): - """ - A voltage source can be a hard source if it's resistance is zero, i.e. the - time variation of the specified electric field component is prescribed. - If it's resistance is non-zero it behaves as a resistive voltage source. - """ - - def __init__(self): - super().__init__() - self.resistance = None - - def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G): - """Updates electric field values for a voltage source. - - Args: - iteration (int): Current iteration (timestep). - updatecoeffsE (memory view): numpy array of electric field update coefficients. - ID (memory view): numpy array of numeric IDs corresponding to materials in the model. - Ex, Ey, Ez (memory view): numpy array of electric field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: - i = self.xcoord - j = self.ycoord - k = self.zcoord - componentID = 'E' + self.polarisation - - if self.polarisation == 'x': - if self.resistance != 0: - Ex[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * (1 / (self.resistance * G.dy * G.dz)) - else: - Ex[i, j, k] = -1 * self.waveformvaluesJ[iteration] / G.dx - - elif self.polarisation == 'y': - if self.resistance != 0: - Ey[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * (1 / (self.resistance * G.dx * G.dz)) - else: - Ey[i, j, k] = -1 * self.waveformvaluesJ[iteration] / G.dy - - elif self.polarisation == 'z': - if self.resistance != 0: - Ez[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * (1 / (self.resistance * G.dx * G.dy)) - else: - Ez[i, j, k] = -1 * self.waveformvaluesJ[iteration] / G.dz - - def create_material(self, G): - """Create a new material at the voltage source location that adds the - voltage source conductivity to the underlying parameters. - - Args: - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if self.resistance != 0: - i = self.xcoord - j = self.ycoord - k = self.zcoord - - componentID = 'E' + self.polarisation - requirednumID = G.ID[G.IDlookup[componentID], i, j, k] - material = next(x for x in G.materials if x.numID == requirednumID) - newmaterial = deepcopy(material) - newmaterial.ID = material.ID + '+' + self.ID - newmaterial.numID = len(G.materials) - newmaterial.averagable = False - newmaterial.type += ',\nvoltage-source' - - # Add conductivity of voltage source to underlying conductivity - if self.polarisation == 'x': - newmaterial.se += G.dx / (self.resistance * G.dy * G.dz) - elif self.polarisation == 'y': - newmaterial.se += G.dy / (self.resistance * G.dx * G.dz) - elif self.polarisation == 'z': - newmaterial.se += G.dz / (self.resistance * G.dx * G.dy) - - G.ID[G.IDlookup[componentID], i, j, k] = newmaterial.numID - G.materials.append(newmaterial) - - -class HertzianDipole(Source): - """A Hertzian dipole is an additive source (electric current density).""" - - def __init__(self): - super().__init__() - self.dl = None - - def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G): - """Updates electric field values for a Hertzian dipole. - - Args: - iteration (int): Current iteration (timestep). - updatecoeffsE (memory view): numpy array of electric field update coefficients. - ID (memory view): numpy array of numeric IDs corresponding to materials in the model. - Ex, Ey, Ez (memory view): numpy array of electric field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: - i = self.xcoord - j = self.ycoord - k = self.zcoord - componentID = 'E' + self.polarisation - if self.polarisation == 'x': - Ex[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * self.dl * (1 / (G.dx * G.dy * G.dz)) - - elif self.polarisation == 'y': - Ey[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * self.dl * (1 / (G.dx * G.dy * G.dz)) - - elif self.polarisation == 'z': - Ez[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * self.dl * (1 / (G.dx * G.dy * G.dz)) - - -class MagneticDipole(Source): - """A magnetic dipole is an additive source (magnetic current density).""" - - def __init__(self): - super().__init__() - - def update_magnetic(self, iteration, updatecoeffsH, ID, Hx, Hy, Hz, G): - """Updates magnetic field values for a magnetic dipole. - - Args: - iteration (int): Current iteration (timestep). - updatecoeffsH (memory view): numpy array of magnetic field update coefficients. - ID (memory view): numpy array of numeric IDs corresponding to materials in the model. - Hx, Hy, Hz (memory view): numpy array of magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: - i = self.xcoord - j = self.ycoord - k = self.zcoord - componentID = 'H' + self.polarisation - - if self.polarisation == 'x': - Hx[i, j, k] -= updatecoeffsH[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesM[iteration] * (1 / (G.dx * G.dy * G.dz)) - - elif self.polarisation == 'y': - Hy[i, j, k] -= updatecoeffsH[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesM[iteration] * (1 / (G.dx * G.dy * G.dz)) - - elif self.polarisation == 'z': - Hz[i, j, k] -= updatecoeffsH[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesM[iteration] * (1 / (G.dx * G.dy * G.dz)) - - -def gpu_initialise_src_arrays(sources, G): - """Initialise arrays on GPU for source coordinates/polarisation, other source information, and source waveform values. - - Args: - sources (list): List of sources of one class, e.g. HertzianDipoles. - G (class): Grid class instance - holds essential parameters describing the model. - - Returns: - srcinfo1_gpu (int): numpy array of source cell coordinates and polarisation information. - srcinfo2_gpu (float): numpy array of other source information, e.g. length, resistance etc... - srcwaves_gpu (float): numpy array of source waveform values. - """ - - import pycuda.gpuarray as gpuarray - - srcinfo1 = np.zeros((len(sources), 4), dtype=np.int32) - srcinfo2 = np.zeros((len(sources)), dtype=config.dtypes['float_or_double']) - srcwaves = np.zeros((len(sources), G.iterations), dtype=config.dtypes['float_or_double']) - for i, src in enumerate(sources): - srcinfo1[i, 0] = src.xcoord - srcinfo1[i, 1] = src.ycoord - srcinfo1[i, 2] = src.zcoord - - if src.polarisation == 'x': - srcinfo1[i, 3] = 0 - elif src.polarisation == 'y': - srcinfo1[i, 3] = 1 - elif src.polarisation == 'z': - srcinfo1[i, 3] = 2 - - if src.__class__.__name__ == 'HertzianDipole': - srcinfo2[i] = src.dl - srcwaves[i, :] = src.waveformvaluesJ - elif src.__class__.__name__ == 'VoltageSource': - srcinfo2[i] = src.resistance - srcwaves[i, :] = src.waveformvaluesJ - elif src.__class__.__name__ == 'MagneticDipole': - srcwaves[i, :] = src.waveformvaluesM - - srcinfo1_gpu = gpuarray.to_gpu(srcinfo1) - srcinfo2_gpu = gpuarray.to_gpu(srcinfo2) - srcwaves_gpu = gpuarray.to_gpu(srcwaves) - - return srcinfo1_gpu, srcinfo2_gpu, srcwaves_gpu - - -class TransmissionLine(Source): - """A transmission line source is a one-dimensional transmission line - which is attached virtually to a grid cell. - """ - - def __init__(self, G): - """ - Args: - G (class): Grid class instance - holds essential parameters describing the model. - """ - - super().__init__() - self.resistance = None - - # Coefficients for ABC termination of end of the transmission line - self.abcv0 = 0 - self.abcv1 = 0 - - # Spatial step of transmission line (N.B if the magic time step is - # used it results in instabilities for certain impedances) - self.dl = np.sqrt(3) * config.c * G.dt - - # Number of cells in the transmission line (initially a long line to - # calculate incident voltage and current); consider putting ABCs/PML at end - self.nl = round_value(0.667 * G.iterations) - - # Cell position of the one-way injector excitation in the transmission line - self.srcpos = 5 - - # Cell position of where line connects to antenna/main grid - self.antpos = 10 - - self.voltage = np.zeros(self.nl, dtype=config.dtypes['float_or_double']) - self.current = np.zeros(self.nl, dtype=config.dtypes['float_or_double']) - self.Vinc = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) - self.Iinc = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) - self.Vtotal = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) - self.Itotal = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) - - def calculate_incident_V_I(self, G): - """Calculates the incident voltage and current with a long length - transmission line not connected to the main grid from: http://dx.doi.org/10.1002/mop.10415 - - Args: - G (class): Grid class instance - holds essential parameters describing the model. - """ - - for iteration in range(G.iterations): - self.Iinc[iteration] = self.current[self.antpos] - self.Vinc[iteration] = self.voltage[self.antpos] - self.update_current(iteration, G) - self.update_voltage(iteration, G) - - # Shorten number of cells in the transmission line before use with main grid - self.nl = self.antpos + 1 - - def update_abc(self, G): - """Updates absorbing boundary condition at end of the transmission line. - - Args: - G (class): Grid class instance - holds essential parameters describing the model. - """ - - h = (config.c * G.dt - self.dl) / (config.c * G.dt + self.dl) - - self.voltage[0] = h * (self.voltage[1] - self.abcv0) + self.abcv1 - self.abcv0 = self.voltage[0] - self.abcv1 = self.voltage[1] - - def update_voltage(self, iteration, G): - """Updates voltage values along the transmission line. - - Args: - iteration (int): Current iteration (timestep). - G (class): Grid class instance - holds essential parameters describing the model. - """ - - # Update all the voltage values along the line - self.voltage[1:self.nl] -= self.resistance * (config.c * G.dt / self.dl) * (self.current[1:self.nl] - self.current[0:self.nl - 1]) - - # Update the voltage at the position of the one-way injector excitation - self.voltage[self.srcpos] += (config.c * G.dt / self.dl) * self.waveformvaluesJ[iteration] - - # Update ABC before updating current - self.update_abc(G) - - def update_current(self, iteration, G): - """Updates current values along the transmission line. - - Args: - iteration (int): Current iteration (timestep). - G (class): Grid class instance - holds essential parameters describing the model. - """ - - # Update all the current values along the line - self.current[0:self.nl - 1] -= (1 / self.resistance) * (config.c * G.dt / self.dl) * (self.voltage[1:self.nl] - self.voltage[0:self.nl - 1]) - - # Update the current one cell before the position of the one-way injector excitation - self.current[self.srcpos - 1] += (1 / self.resistance) * (config.c * G.dt / self.dl) * self.waveformvaluesM[iteration] - - def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G): - """Updates electric field value in the main grid from voltage value in the transmission line. - - Args: - iteration (int): Current iteration (timestep). - updatecoeffsE (memory view): numpy array of electric field update coefficients. - ID (memory view): numpy array of numeric IDs corresponding to materials in the model. - Ex, Ey, Ez (memory view): numpy array of electric field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: - i = self.xcoord - j = self.ycoord - k = self.zcoord - - self.update_voltage(iteration, G) - - if self.polarisation == 'x': - Ex[i, j, k] = - self.voltage[self.antpos] / G.dx - - elif self.polarisation == 'y': - Ey[i, j, k] = - self.voltage[self.antpos] / G.dy - - elif self.polarisation == 'z': - Ez[i, j, k] = - self.voltage[self.antpos] / G.dz - - def update_magnetic(self, iteration, updatecoeffsH, ID, Hx, Hy, Hz, G): - """Updates current value in transmission line from magnetic field values in the main grid. - - Args: - iteration (int): Current iteration (timestep). - updatecoeffsH (memory view): numpy array of magnetic field update coefficients. - ID (memory view): numpy array of numeric IDs corresponding to materials in the model. - Hx, Hy, Hz (memory view): numpy array of magnetic field values. - G (class): Grid class instance - holds essential parameters describing the model. - """ - - if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: - i = self.xcoord - j = self.ycoord - k = self.zcoord - - if self.polarisation == 'x': - self.current[self.antpos] = Ix(i, j, k, G.Hx, G.Hy, G.Hz, G) - - elif self.polarisation == 'y': - self.current[self.antpos] = Iy(i, j, k, G.Hx, G.Hy, G.Hz, G) - - elif self.polarisation == 'z': - self.current[self.antpos] = Iz(i, j, k, G.Hx, G.Hy, G.Hz, G) - - self.update_current(iteration, G) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +from copy import deepcopy + +import numpy as np + +import gprMax.config as config +from .grid import Ix +from .grid import Iy +from .grid import Iz +from .utilities import round_value + + +class Source(object): + """Super-class which describes a generic source.""" + + def __init__(self): + self.ID = None + self.polarisation = None + self.xcoord = None + self.ycoord = None + self.zcoord = None + self.xcoordorigin = None + self.ycoordorigin = None + self.zcoordorigin = None + self.start = None + self.stop = None + self.waveformID = None + + def calculate_waveform_values(self, G): + """Calculates all waveform values for source for duration of simulation. + + Args: + G (class): Grid class instance - holds essential parameters describing the model. + """ + # Waveform values for electric sources - calculated half a timestep later + self.waveformvaluesJ = np.zeros((G.iterations), dtype=config.dtypes['float_or_double']) + + # Waveform values for magnetic sources + self.waveformvaluesM = np.zeros((G.iterations), dtype=config.dtypes['float_or_double']) + + waveform = next(x for x in G.waveforms if x.ID == self.waveformID) + + for iteration in range(G.iterations): + time = G.dt * iteration + if time >= self.start and time <= self.stop: + # Set the time of the waveform evaluation to account for any delay in the start + time -= self.start + self.waveformvaluesJ[iteration] = waveform.calculate_value(time + 0.5 * G.dt, G.dt) + self.waveformvaluesM[iteration] = waveform.calculate_value(time, G.dt) + + +class VoltageSource(Source): + """ + A voltage source can be a hard source if it's resistance is zero, i.e. the + time variation of the specified electric field component is prescribed. + If it's resistance is non-zero it behaves as a resistive voltage source. + """ + + def __init__(self): + super().__init__() + self.resistance = None + + def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G): + """Updates electric field values for a voltage source. + + Args: + iteration (int): Current iteration (timestep). + updatecoeffsE (memory view): numpy array of electric field update coefficients. + ID (memory view): numpy array of numeric IDs corresponding to materials in the model. + Ex, Ey, Ez (memory view): numpy array of electric field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: + i = self.xcoord + j = self.ycoord + k = self.zcoord + componentID = 'E' + self.polarisation + + if self.polarisation == 'x': + if self.resistance != 0: + Ex[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * (1 / (self.resistance * G.dy * G.dz)) + else: + Ex[i, j, k] = -1 * self.waveformvaluesJ[iteration] / G.dx + + elif self.polarisation == 'y': + if self.resistance != 0: + Ey[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * (1 / (self.resistance * G.dx * G.dz)) + else: + Ey[i, j, k] = -1 * self.waveformvaluesJ[iteration] / G.dy + + elif self.polarisation == 'z': + if self.resistance != 0: + Ez[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * (1 / (self.resistance * G.dx * G.dy)) + else: + Ez[i, j, k] = -1 * self.waveformvaluesJ[iteration] / G.dz + + def create_material(self, G): + """Create a new material at the voltage source location that adds the + voltage source conductivity to the underlying parameters. + + Args: + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if self.resistance != 0: + i = self.xcoord + j = self.ycoord + k = self.zcoord + + componentID = 'E' + self.polarisation + requirednumID = G.ID[G.IDlookup[componentID], i, j, k] + material = next(x for x in G.materials if x.numID == requirednumID) + newmaterial = deepcopy(material) + newmaterial.ID = material.ID + '+' + self.ID + newmaterial.numID = len(G.materials) + newmaterial.averagable = False + newmaterial.type += ',\nvoltage-source' + + # Add conductivity of voltage source to underlying conductivity + if self.polarisation == 'x': + newmaterial.se += G.dx / (self.resistance * G.dy * G.dz) + elif self.polarisation == 'y': + newmaterial.se += G.dy / (self.resistance * G.dx * G.dz) + elif self.polarisation == 'z': + newmaterial.se += G.dz / (self.resistance * G.dx * G.dy) + + G.ID[G.IDlookup[componentID], i, j, k] = newmaterial.numID + G.materials.append(newmaterial) + + +class HertzianDipole(Source): + """A Hertzian dipole is an additive source (electric current density).""" + + def __init__(self): + super().__init__() + self.dl = None + + def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G): + """Updates electric field values for a Hertzian dipole. + + Args: + iteration (int): Current iteration (timestep). + updatecoeffsE (memory view): numpy array of electric field update coefficients. + ID (memory view): numpy array of numeric IDs corresponding to materials in the model. + Ex, Ey, Ez (memory view): numpy array of electric field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: + i = self.xcoord + j = self.ycoord + k = self.zcoord + componentID = 'E' + self.polarisation + if self.polarisation == 'x': + Ex[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * self.dl * (1 / (G.dx * G.dy * G.dz)) + + elif self.polarisation == 'y': + Ey[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * self.dl * (1 / (G.dx * G.dy * G.dz)) + + elif self.polarisation == 'z': + Ez[i, j, k] -= updatecoeffsE[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesJ[iteration] * self.dl * (1 / (G.dx * G.dy * G.dz)) + + +class MagneticDipole(Source): + """A magnetic dipole is an additive source (magnetic current density).""" + + def __init__(self): + super().__init__() + + def update_magnetic(self, iteration, updatecoeffsH, ID, Hx, Hy, Hz, G): + """Updates magnetic field values for a magnetic dipole. + + Args: + iteration (int): Current iteration (timestep). + updatecoeffsH (memory view): numpy array of magnetic field update coefficients. + ID (memory view): numpy array of numeric IDs corresponding to materials in the model. + Hx, Hy, Hz (memory view): numpy array of magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: + i = self.xcoord + j = self.ycoord + k = self.zcoord + componentID = 'H' + self.polarisation + + if self.polarisation == 'x': + Hx[i, j, k] -= updatecoeffsH[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesM[iteration] * (1 / (G.dx * G.dy * G.dz)) + + elif self.polarisation == 'y': + Hy[i, j, k] -= updatecoeffsH[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesM[iteration] * (1 / (G.dx * G.dy * G.dz)) + + elif self.polarisation == 'z': + Hz[i, j, k] -= updatecoeffsH[ID[G.IDlookup[componentID], i, j, k], 4] * self.waveformvaluesM[iteration] * (1 / (G.dx * G.dy * G.dz)) + + +def gpu_initialise_src_arrays(sources, G): + """Initialise arrays on GPU for source coordinates/polarisation, other source information, and source waveform values. + + Args: + sources (list): List of sources of one class, e.g. HertzianDipoles. + G (class): Grid class instance - holds essential parameters describing the model. + + Returns: + srcinfo1_gpu (int): numpy array of source cell coordinates and polarisation information. + srcinfo2_gpu (float): numpy array of other source information, e.g. length, resistance etc... + srcwaves_gpu (float): numpy array of source waveform values. + """ + + import pycuda.gpuarray as gpuarray + + srcinfo1 = np.zeros((len(sources), 4), dtype=np.int32) + srcinfo2 = np.zeros((len(sources)), dtype=config.dtypes['float_or_double']) + srcwaves = np.zeros((len(sources), G.iterations), dtype=config.dtypes['float_or_double']) + for i, src in enumerate(sources): + srcinfo1[i, 0] = src.xcoord + srcinfo1[i, 1] = src.ycoord + srcinfo1[i, 2] = src.zcoord + + if src.polarisation == 'x': + srcinfo1[i, 3] = 0 + elif src.polarisation == 'y': + srcinfo1[i, 3] = 1 + elif src.polarisation == 'z': + srcinfo1[i, 3] = 2 + + if src.__class__.__name__ == 'HertzianDipole': + srcinfo2[i] = src.dl + srcwaves[i, :] = src.waveformvaluesJ + elif src.__class__.__name__ == 'VoltageSource': + srcinfo2[i] = src.resistance + srcwaves[i, :] = src.waveformvaluesJ + elif src.__class__.__name__ == 'MagneticDipole': + srcwaves[i, :] = src.waveformvaluesM + + srcinfo1_gpu = gpuarray.to_gpu(srcinfo1) + srcinfo2_gpu = gpuarray.to_gpu(srcinfo2) + srcwaves_gpu = gpuarray.to_gpu(srcwaves) + + return srcinfo1_gpu, srcinfo2_gpu, srcwaves_gpu + + +class TransmissionLine(Source): + """A transmission line source is a one-dimensional transmission line + which is attached virtually to a grid cell. + """ + + def __init__(self, G): + """ + Args: + G (class): Grid class instance - holds essential parameters describing the model. + """ + + super().__init__() + self.resistance = None + + # Coefficients for ABC termination of end of the transmission line + self.abcv0 = 0 + self.abcv1 = 0 + + # Spatial step of transmission line (N.B if the magic time step is + # used it results in instabilities for certain impedances) + self.dl = np.sqrt(3) * config.c * G.dt + + # Number of cells in the transmission line (initially a long line to + # calculate incident voltage and current); consider putting ABCs/PML at end + self.nl = round_value(0.667 * G.iterations) + + # Cell position of the one-way injector excitation in the transmission line + self.srcpos = 5 + + # Cell position of where line connects to antenna/main grid + self.antpos = 10 + + self.voltage = np.zeros(self.nl, dtype=config.dtypes['float_or_double']) + self.current = np.zeros(self.nl, dtype=config.dtypes['float_or_double']) + self.Vinc = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) + self.Iinc = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) + self.Vtotal = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) + self.Itotal = np.zeros(G.iterations, dtype=config.dtypes['float_or_double']) + + def calculate_incident_V_I(self, G): + """Calculates the incident voltage and current with a long length + transmission line not connected to the main grid from: http://dx.doi.org/10.1002/mop.10415 + + Args: + G (class): Grid class instance - holds essential parameters describing the model. + """ + + for iteration in range(G.iterations): + self.Iinc[iteration] = self.current[self.antpos] + self.Vinc[iteration] = self.voltage[self.antpos] + self.update_current(iteration, G) + self.update_voltage(iteration, G) + + # Shorten number of cells in the transmission line before use with main grid + self.nl = self.antpos + 1 + + def update_abc(self, G): + """Updates absorbing boundary condition at end of the transmission line. + + Args: + G (class): Grid class instance - holds essential parameters describing the model. + """ + + h = (config.c * G.dt - self.dl) / (config.c * G.dt + self.dl) + + self.voltage[0] = h * (self.voltage[1] - self.abcv0) + self.abcv1 + self.abcv0 = self.voltage[0] + self.abcv1 = self.voltage[1] + + def update_voltage(self, iteration, G): + """Updates voltage values along the transmission line. + + Args: + iteration (int): Current iteration (timestep). + G (class): Grid class instance - holds essential parameters describing the model. + """ + + # Update all the voltage values along the line + self.voltage[1:self.nl] -= self.resistance * (config.c * G.dt / self.dl) * (self.current[1:self.nl] - self.current[0:self.nl - 1]) + + # Update the voltage at the position of the one-way injector excitation + self.voltage[self.srcpos] += (config.c * G.dt / self.dl) * self.waveformvaluesJ[iteration] + + # Update ABC before updating current + self.update_abc(G) + + def update_current(self, iteration, G): + """Updates current values along the transmission line. + + Args: + iteration (int): Current iteration (timestep). + G (class): Grid class instance - holds essential parameters describing the model. + """ + + # Update all the current values along the line + self.current[0:self.nl - 1] -= (1 / self.resistance) * (config.c * G.dt / self.dl) * (self.voltage[1:self.nl] - self.voltage[0:self.nl - 1]) + + # Update the current one cell before the position of the one-way injector excitation + self.current[self.srcpos - 1] += (1 / self.resistance) * (config.c * G.dt / self.dl) * self.waveformvaluesM[iteration] + + def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G): + """Updates electric field value in the main grid from voltage value in the transmission line. + + Args: + iteration (int): Current iteration (timestep). + updatecoeffsE (memory view): numpy array of electric field update coefficients. + ID (memory view): numpy array of numeric IDs corresponding to materials in the model. + Ex, Ey, Ez (memory view): numpy array of electric field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: + i = self.xcoord + j = self.ycoord + k = self.zcoord + + self.update_voltage(iteration, G) + + if self.polarisation == 'x': + Ex[i, j, k] = - self.voltage[self.antpos] / G.dx + + elif self.polarisation == 'y': + Ey[i, j, k] = - self.voltage[self.antpos] / G.dy + + elif self.polarisation == 'z': + Ez[i, j, k] = - self.voltage[self.antpos] / G.dz + + def update_magnetic(self, iteration, updatecoeffsH, ID, Hx, Hy, Hz, G): + """Updates current value in transmission line from magnetic field values in the main grid. + + Args: + iteration (int): Current iteration (timestep). + updatecoeffsH (memory view): numpy array of magnetic field update coefficients. + ID (memory view): numpy array of numeric IDs corresponding to materials in the model. + Hx, Hy, Hz (memory view): numpy array of magnetic field values. + G (class): Grid class instance - holds essential parameters describing the model. + """ + + if iteration * G.dt >= self.start and iteration * G.dt <= self.stop: + i = self.xcoord + j = self.ycoord + k = self.zcoord + + if self.polarisation == 'x': + self.current[self.antpos] = Ix(i, j, k, G.Hx, G.Hy, G.Hz, G) + + elif self.polarisation == 'y': + self.current[self.antpos] = Iy(i, j, k, G.Hx, G.Hy, G.Hz, G) + + elif self.polarisation == 'z': + self.current[self.antpos] = Iz(i, j, k, G.Hx, G.Hy, G.Hz, G) + + self.update_current(iteration, G) diff --git a/gprMax/subgrids/base.py b/gprMax/subgrids/base.py index 7b0fb741..d16ad879 100644 --- a/gprMax/subgrids/base.py +++ b/gprMax/subgrids/base.py @@ -1,72 +1,72 @@ -from ..grid import FDTDGrid - -from ..materials import Material - -from scipy.constants import mu_0 -from scipy.constants import epsilon_0 -from scipy.constants import c - -import numpy as np - -from colorama import init -from colorama import Fore -from colorama import Style -init() - - -class SubGridBase(FDTDGrid): - - def __init__(self, **kwargs): - super().__init__() - - self.mode = '3D' - self.ratio = kwargs['ratio'] - - if self.ratio % 2 == 0: - raise ValueError('Subgrid Error: Only odd ratios are supported') - - # Name of the grid - self.name = kwargs['ID'] - - self.filter = kwargs['filter'] - - # Number of main grid cells between the IS and OS - self.is_os_sep = kwargs['is_os_sep'] - # Number of subgrid grid cells between the IS and OS - self.s_is_os_sep = self.is_os_sep * self.ratio - - # Distance from OS to pml or the edge of the grid when pml is off - self.pml_separation = kwargs['pml_separation'] - - self.pmlthickness['x0'] = kwargs['subgrid_pml_thickness'] - self.pmlthickness['y0'] = kwargs['subgrid_pml_thickness'] - self.pmlthickness['z0'] = kwargs['subgrid_pml_thickness'] - self.pmlthickness['xmax'] = kwargs['subgrid_pml_thickness'] - self.pmlthickness['ymax'] = kwargs['subgrid_pml_thickness'] - self.pmlthickness['zmax'] = kwargs['subgrid_pml_thickness'] - - # Number of sub cells to extend the sub grid beyond the IS boundary - d_to_pml = self.s_is_os_sep + self.pml_separation - self.n_boundary_cells = d_to_pml + self.pmlthickness['x0'] - self.n_boundary_cells_x = d_to_pml + self.pmlthickness['x0'] - self.n_boundary_cells_y = d_to_pml + self.pmlthickness['y0'] - self.n_boundary_cells_z = d_to_pml + self.pmlthickness['z0'] - - self.interpolation = kwargs['interpolation'] - - 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 main_grid_index_to_subgrid_index(self, i, j, k): - 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() +from ..grid import FDTDGrid + +from ..materials import Material + +from scipy.constants import mu_0 +from scipy.constants import epsilon_0 +from scipy.constants import c + +import numpy as np + +from colorama import init +from colorama import Fore +from colorama import Style +init() + + +class SubGridBase(FDTDGrid): + + def __init__(self, **kwargs): + super().__init__() + + self.mode = '3D' + self.ratio = kwargs['ratio'] + + if self.ratio % 2 == 0: + raise ValueError('Subgrid Error: Only odd ratios are supported') + + # Name of the grid + self.name = kwargs['ID'] + + self.filter = kwargs['filter'] + + # Number of main grid cells between the IS and OS + self.is_os_sep = kwargs['is_os_sep'] + # Number of subgrid grid cells between the IS and OS + self.s_is_os_sep = self.is_os_sep * self.ratio + + # Distance from OS to pml or the edge of the grid when pml is off + self.pml_separation = kwargs['pml_separation'] + + self.pmlthickness['x0'] = kwargs['subgrid_pml_thickness'] + self.pmlthickness['y0'] = kwargs['subgrid_pml_thickness'] + self.pmlthickness['z0'] = kwargs['subgrid_pml_thickness'] + self.pmlthickness['xmax'] = kwargs['subgrid_pml_thickness'] + self.pmlthickness['ymax'] = kwargs['subgrid_pml_thickness'] + self.pmlthickness['zmax'] = kwargs['subgrid_pml_thickness'] + + # Number of sub cells to extend the sub grid beyond the IS boundary + d_to_pml = self.s_is_os_sep + self.pml_separation + self.n_boundary_cells = d_to_pml + self.pmlthickness['x0'] + self.n_boundary_cells_x = d_to_pml + self.pmlthickness['x0'] + self.n_boundary_cells_y = d_to_pml + self.pmlthickness['y0'] + self.n_boundary_cells_z = d_to_pml + self.pmlthickness['z0'] + + self.interpolation = kwargs['interpolation'] + + 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 main_grid_index_to_subgrid_index(self, i, j, k): + 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 7888c01b..60151d0f 100644 --- a/gprMax/subgrids/multi.py +++ b/gprMax/subgrids/multi.py @@ -1,146 +1,146 @@ -from ..receivers import Rx - - -class ReferenceRx(Rx): - """Receiver that micks a receiver in coarse grid.""" - - """We often want to compare an output in a fine reference solution with a - the solution in the coarse grid of a subgridded solution. This receiver - moves the output points in the fine grid such that they are in the same - 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. - - Parameters - ---------- - str_id : str - 'Ex' etc... - field : np array - e.g. numpy array of grid.Ez - - Returns - ------- - float - Field value - - """ - d = { - 'Ex': self.get_Ex_from_field, - 'Ey': self.get_Ey_from_field, - 'Ez': self.get_Ez_from_field, - 'Hx': self.get_Hx_from_field, - 'Hy': self.get_Hy_from_field, - 'Hz': self.get_Hz_from_field - } - - e = d[str_id](field) - return e - - def get_Ex_from_field(self, Ex): - """Return the Ex field value from the equivalent coarse yee cell. - - Parameters - ---------- - Ex : 3d numpy array - e.g. grid.Ex - - Returns - ------- - float - Ex field value - - """ - - # offset = ratio // 2 - e = Ex[self.xcoord + self.offset, self.ycoord, self.zcoord] - return e - - def get_Ey_from_field(self, Ey): - """Return the Ey field value from the equivalent coarse yee cell. - - Parameters - ---------- - Ex : 3d numpy array - e.g. grid.Ex - - Returns - ------- - float - Ey field value - - """ - e = Ey[self.xcoord, self.ycoord + self.offset, self.zcoord] - return e - - def get_Ez_from_field(self, Ez): - """Return the Ez field value from the equivalent coarse yee cell. - - Parameters - ---------- - Ex : 3d numpy array - e.g. grid.Ez - - Returns - ------- - float - Ez field value - - """ - e = Ez[self.xcoord, self.ycoord, self.zcoord + self.offset] - return e - - def get_Hx_from_field(self, Hx): - """Return the Hx field value from the equivalent coarse yee cell. - - Parameters - ---------- - Ex : 3d numpy array - e.g. grid.Hx - - Returns - ------- - float - Hx field value - - """ - e = Hx[self.xcoord, self.ycoord + self.offset, self.zcoord + self.offset] - return e - - def get_Hy_from_field(self, Hy): - """Return the Hy field value from the equivalent coarse yee cell. - - Parameters - ---------- - Ex : 3d numpy array - e.g. grid.Hx - - Returns - ------- - float - Hy field value - - """ - e = Hy[self.xcoord + self.offset, self.ycoord, self.zcoord + self.offset] - return e - - def get_Hz_from_field(self, Hz): - """Return the Hz field value from the equivalent coarse yee cell. - - Parameters - ---------- - Ex : 3d numpy array - e.g. grid.Hx - - Returns - ------- - float - Hz field value - - """ - e = Hz[self.xcoord + self.offset, self.ycoord + self.offset, self.zcoord] - return e +from ..receivers import Rx + + +class ReferenceRx(Rx): + """Receiver that micks a receiver in coarse grid.""" + + """We often want to compare an output in a fine reference solution with a + the solution in the coarse grid of a subgridded solution. This receiver + moves the output points in the fine grid such that they are in the same + 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. + + Parameters + ---------- + str_id : str + 'Ex' etc... + field : np array + e.g. numpy array of grid.Ez + + Returns + ------- + float + Field value + + """ + d = { + 'Ex': self.get_Ex_from_field, + 'Ey': self.get_Ey_from_field, + 'Ez': self.get_Ez_from_field, + 'Hx': self.get_Hx_from_field, + 'Hy': self.get_Hy_from_field, + 'Hz': self.get_Hz_from_field + } + + e = d[str_id](field) + return e + + def get_Ex_from_field(self, Ex): + """Return the Ex field value from the equivalent coarse yee cell. + + Parameters + ---------- + Ex : 3d numpy array + e.g. grid.Ex + + Returns + ------- + float + Ex field value + + """ + + # offset = ratio // 2 + e = Ex[self.xcoord + self.offset, self.ycoord, self.zcoord] + return e + + def get_Ey_from_field(self, Ey): + """Return the Ey field value from the equivalent coarse yee cell. + + Parameters + ---------- + Ex : 3d numpy array + e.g. grid.Ex + + Returns + ------- + float + Ey field value + + """ + e = Ey[self.xcoord, self.ycoord + self.offset, self.zcoord] + return e + + def get_Ez_from_field(self, Ez): + """Return the Ez field value from the equivalent coarse yee cell. + + Parameters + ---------- + Ex : 3d numpy array + e.g. grid.Ez + + Returns + ------- + float + Ez field value + + """ + e = Ez[self.xcoord, self.ycoord, self.zcoord + self.offset] + return e + + def get_Hx_from_field(self, Hx): + """Return the Hx field value from the equivalent coarse yee cell. + + Parameters + ---------- + Ex : 3d numpy array + e.g. grid.Hx + + Returns + ------- + float + Hx field value + + """ + e = Hx[self.xcoord, self.ycoord + self.offset, self.zcoord + self.offset] + return e + + def get_Hy_from_field(self, Hy): + """Return the Hy field value from the equivalent coarse yee cell. + + Parameters + ---------- + Ex : 3d numpy array + e.g. grid.Hx + + Returns + ------- + float + Hy field value + + """ + e = Hy[self.xcoord + self.offset, self.ycoord, self.zcoord + self.offset] + return e + + def get_Hz_from_field(self, Hz): + """Return the Hz field value from the equivalent coarse yee cell. + + Parameters + ---------- + Ex : 3d numpy array + e.g. grid.Hx + + Returns + ------- + float + Hz field value + + """ + e = Hz[self.xcoord + self.offset, self.ycoord + self.offset, self.zcoord] + return e diff --git a/gprMax/subgrids/precursor_nodes.py b/gprMax/subgrids/precursor_nodes.py index a837d18a..6e8a3890 100644 --- a/gprMax/subgrids/precursor_nodes.py +++ b/gprMax/subgrids/precursor_nodes.py @@ -1,633 +1,633 @@ -import numpy as np -from scipy import interpolate -import sys - - -def calculate_weighting_coefficients(x1, x): - c1 = (x - x1) / x - c2 = x1 / x - return (c1, c2) - - -class PrecusorNodes2dBase(object): - - def __init__(self, fdtd_grid, sub_grid): - self.G = fdtd_grid - self.ratio = sub_grid.ratio - self.nwx = sub_grid.nwx - self.nwy = sub_grid.nwy - self.sub_grid = sub_grid - self.interpolation = sub_grid.interpolation - - self.Hx = fdtd_grid.Hx - self.Hy = fdtd_grid.Hy - self.Hz = fdtd_grid.Hz - self.Ex = fdtd_grid.Ex - self.Ey = fdtd_grid.Ey - self.Ez = fdtd_grid.Ez - - # Main grid indices of subgrids - self.i0 = sub_grid.i0 - self.j0 = sub_grid.j0 - self.k0 = sub_grid.k0 - self.i1 = sub_grid.i1 - self.j1 = sub_grid.j1 - self.k1 = sub_grid.k1 - - # dl / 2 sub cell - self.d = 1 / (2 * self.ratio) - - self._initialize_fields() - self._initialize_field_names() - - self.l_weight = self.ratio // 2 - self.r_weight = self.ratio - self.l_weight - #self.l_weight = 1 - #self.r_weight = 2 - - def _initialize_fields(self): - print('dont call me') - sys.exit() - pass - - def _initialize_field_names(self): - print('dont call me') - sys.exit() - pass - - def interpolate_magnetic_in_time(self, m): - self.weight_pre_and_current_fields(m, self.fn_m) - - def interpolate_electric_in_time(self, m): - self.weight_pre_and_current_fields(m, self.fn_e) - - def weight_pre_and_current_fields(self, m, field_names): - c1, c2 = calculate_weighting_coefficients(m, self.ratio) - - for f in field_names: - try: - val = c1 * getattr(self, f + '_0') + c2 * getattr(self, f + '_1') - except ValueError: - print(self.ex_front_0.shape) - print(self.ex_front_1.shape) - raise Exception(f) - setattr(self, f, val) - - def calc_exact_field(self, field_names): - """ - Function to set the fields used in update calculations to the - values at the current main time step. - i.e. ey_left = copy.ey_left_1 - """ - for f in field_names: - val = np.copy(getattr(self, f + '_1')) - setattr(self, f, val) - - def calc_exact_magnetic_in_time(self): - self.calc_exact_field(self.fn_m) - - def calc_exact_electric_in_time(self): - self.calc_exact_field(self.fn_e) - - -class PrecursorNodes2dTM(PrecusorNodes2dBase): - - def __init__(self, fdtd_grid, sub_grid): - super().__init__(fdtd_grid, sub_grid) - - def _initialize_fields(self): - - # H precursors - self.hx_front_0 = np.zeros(self.nwx + 1) - self.hx_front_1 = np.zeros(self.nwx + 1) - - self.hx_back_0 = np.zeros(self.nwx + 1) - self.hx_back_1 = np.zeros(self.nwx + 1) - - self.hy_left_0 = np.zeros(self.nwy + 1) - self.hy_left_1 = np.zeros(self.nwy + 1) - - self.hy_right_0 = np.zeros(self.nwy + 1) - self.hy_right_1 = np.zeros(self.nwy + 1) - - # E precursors - self.ez_front_0 = np.zeros(self.nwx + 1) - self.ez_front_1 = np.zeros(self.nwx + 1) - - self.ez_back_0 = np.zeros(self.nwx + 1) - self.ez_back_1 = np.zeros(self.nwx + 1) - - self.ez_left_0 = np.zeros(self.nwy + 1) - self.ez_left_1 = np.zeros(self.nwy + 1) - - self.ez_right_0 = np.zeros(self.nwy + 1) - self.ez_right_1 = np.zeros(self.nwy + 1) - - def _initialize_field_names(self): - self.fn_m = ['hy_left', 'hy_right', 'hx_back', 'hx_front'] - self.fn_e = ['ez_left', 'ez_right', 'ez_back', 'ez_front'] - - def interpolate_across_sub_cells(self, y, n0, n1): - n = n1 - n0 - x = np.arange(0, y.shape[0], 1.0) - x_new = np.linspace(1, y.shape[0] - 2, n * self.ratio + 1) - f = interpolate.interp1d(x, y, kind='linear') - y_new = f(x_new) - """plt.plot(x, y, 'x', x_new, y_new, '.') - plt.show() - sys.exit()""" - return y_new - - def update_electric(self): - # f = np.sin(np.arange(0, 13)) - - # line of Ez nodes along the left edge of the IS - self.ez_left_0 = np.copy(self.ez_left_1) - # interpolate nodes between two Ez nodes 1 cell beyond/infront the IS - f = self.Ez[self.i0, self.j0 - 1:self.j1 + 2, 0] - self.ez_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - self.ez_right_0 = np.copy(self.ez_right_1) - f = self.Ez[self.i1, self.j0 - 1:self.j1 + 2, 0] - self.ez_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - self.ez_front_0 = np.copy(self.ez_front_1) - f = self.Ez[self.i0 - 1:self.i1 + 2, self.j0, 0] - self.ez_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - self.ez_back_0 = np.copy(self.ez_back_1) - f = self.Ez[self.i0 - 1:self.i1 + 2, self.j1, 0] - self.ez_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - def update_hy_left_1(self): - - self.hy_left_0 = np.copy(self.hy_left_1) - c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) - l = self.Hy[self.i0 - 1, self.j0 - 1:self.j1 + 2, 0] - r = self.Hy[self.i0, self.j0 - 1:self.j1 + 2, 0] - - # Tranverse interpolated hz - hz_t_i = c1 * l + c2 * r - # Tangential interpolated hz - self.hy_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hy_right_1(self): - - self.hy_right_0 = np.copy(self.hy_right_1) - c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) - l = self.Hy[self.i1 - 1, self.j0 - 1:self.j1 + 2, 0] - r = self.Hy[self.i1, self.j0 - 1:self.j1 + 2, 0] - hz_t_i = c1 * l + c2 * r - self.hy_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hx_back_1(self): - self.hx_back_0 = np.copy(self.hx_back_1) - c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) - l = self.Hx[self.i0 - 1:self.i1 + 2, self.j1 - 1, 0] - r = self.Hx[self.i0 - 1:self.i1 + 2, self.j1, 0] - hz_t_i = c1 * l + c2 * r - self.hx_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_hx_front_1(self): - - self.hx_front_0 = np.copy(self.hx_front_1) - c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) - l = self.Hx[self.i0 - 1:self.i1 + 2, self.j0 - 1, 0] - r = self.Hx[self.i0 - 1:self.i1 + 2, self.j0, 0] - hz_t_i = c1 * l + c2 * r - self.hx_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_magnetic(self): - self.update_hy_left_1() - self.update_hy_right_1() - self.update_hx_back_1() - self.update_hx_front_1() - - -class PrecursorNodes2d(PrecusorNodes2dBase): - - def __init__(self, fdtd_grid, sub_grid): - super().__init__(fdtd_grid, sub_grid) - - def _initialize_fields(self): - # E precursors - self.ex_front_0 = np.zeros(self.nwx) - self.ex_front_1 = np.zeros(self.nwx) - - self.ex_back_0 = np.zeros(self.nwx) - self.ex_back_1 = np.zeros(self.nwx) - - self.ey_left_0 = np.zeros(self.nwy) - self.ey_left_1 = np.zeros(self.nwy) - - self.ey_right_0 = np.zeros(self.nwy) - self.ey_right_1 = np.zeros(self.nwy) - - # H precursors - self.hz_front_0 = np.zeros(self.nwx) - self.hz_front_1 = np.zeros(self.nwx) - - self.hz_back_0 = np.zeros(self.nwx) - self.hz_back_1 = np.zeros(self.nwx) - - self.hz_left_0 = np.zeros(self.nwy) - self.hz_left_1 = np.zeros(self.nwy) - - self.hz_right_0 = np.zeros(self.nwy) - self.hz_right_1 = np.zeros(self.nwy) - - def _initialize_field_names(self): - # Field names - self.fn_m = ['hz_left', 'hz_right', 'hz_back', 'hz_front'] - self.fn_e = ['ey_left', 'ey_right', 'ex_back', 'ex_front'] - - def update_hz_left_1(self): - self.hz_left_0 = np.copy(self.hz_left_1) - c1, c2 = calculate_weighting_coefficients(1, self.ratio) - l = self.Hz[self.i0 - 1, self.j0 - 1:self.j1 + 1, 1] - r = self.Hz[self.i0, self.j0 - 1:self.j1 + 1, 1] - - # Tranverse interpolated hz - hz_t_i = c1 * l + c2 * r - # Tangential interpolated hz - self.hz_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hz_right_1(self): - - self.hz_right_0 = np.copy(self.hz_right_1) - c1, c2 = calculate_weighting_coefficients(2, self.ratio) - l = self.Hz[self.i1 - 1, self.j0 - 1:self.j1 + 1, 1] - r = self.Hz[self.i1, self.j0 - 1:self.j1 + 1, 1] - hz_t_i = c1 * l + c2 * r - self.hz_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hz_back_1(self): - self.hz_back_0 = np.copy(self.hz_back_1) - c1, c2 = calculate_weighting_coefficients(2, self.ratio) - l = self.Hz[self.i0 - 1:self.i1 + 1, self.j1 - 1, 1] - r = self.Hz[self.i0 - 1:self.i1 + 1, self.j1, 1] - hz_t_i = c1 * l + c2 * r - self.hz_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_hz_front_1(self): - self.hz_front_0 = np.copy(self.hz_front_1) - c1, c2 = calculate_weighting_coefficients(1, self.ratio) - l = self.Hz[self.i0 - 1:self.i1 + 1, self.j0 - 1, 1] - r = self.Hz[self.i0 - 1:self.i1 + 1, self.j0, 1] - hz_t_i = c1 * l + c2 * r - self.hz_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_magnetic(self): - self.update_hz_left_1() - self.update_hz_right_1() - self.update_hz_back_1() - self.update_hz_front_1() - - def update_electric(self): - - """Function to calculate the precursor nodes at the next main - grid time-step - """ - - # LEFT BOUNDARY - # Save the last precursor node values - these will be used interpolate - # field values at sub grid time step values. - self.ey_left_0 = np.copy(self.ey_left_1) - # line of Ex nodes along the left edge of the IS - f = self.Ey[self.i0, self.j0 - 1:self.j1 + 1, 1] - self.ey_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - # RIGHT BOUNDARY - self.ey_right_0 = np.copy(self.ey_right_1) - f = self.Ey[self.i1, self.j0 - 1:self.j1 + 1, 1] - self.ey_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - # BACK BOUNDARY - self.ex_back_0 = np.copy(self.ex_back_1) - f = self.Ex[self.i0 - 1:self.i1 + 1, self.j1, 1] - self.ex_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - # FRONT BOUNDARY - self.ex_front_0 = np.copy(self.ex_front_1) - f = self.Ex[self.i0 - 1:self.i1 + 1, self.j0, 1] - self.ex_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - def interpolate_across_sub_cells(self, y, n0, n1): - n = n1 - n0 - x = np.arange(0.5, y.shape[0], 1.0) - x_new = np.linspace(1 + self.d, (1 + n) - self.d, n * self.ratio) - f = interpolate.interp1d(x, y, kind='linear') - y_new = f(x_new) - - return y_new - - -class PrecursorNodes(PrecusorNodes2dBase): - - def __init__(self, fdtd_grid, sub_grid): - - self.nwz = sub_grid.nwz - super().__init__(fdtd_grid, sub_grid) - self.initialize_magnetic_slices_array() - self.initialize_electric_slices_array() - - def build_coefficient_matrix(self, update_coefficients, lookup_id, inc_field, name): - """Function builds a 2d matrix of update coefficients for each face and field. - This allows the is nodes to be updated via slicing rather than iterating which is much faster - """ - pass - - def _initialize_fields(self): - - # Initialise the precursor arrays - - # The precursors are divided up into the 6. Each represent 1 - # face of a huygens cube surface. We represent each face as a 2d array - # containing a field in a particular direction. - - # _1 are the fields at the current main grid timestep - # _0 are the fields at the previous main grid timestep - # We store both fields so we can do different interpolations between - # them on the fly. - - # Front face - self.ex_front_1 = np.zeros((self.nwx, self.nwz + 1)) - self.ex_front_0 = np.copy(self.ex_front_1) - self.ez_front_1 = np.zeros((self.nwx + 1, self.nwz)) - self.ez_front_0 = np.copy(self.ez_front_1) - - # The same as the opposite face - self.ex_back_1 = np.copy(self.ex_front_1) - self.ex_back_0 = np.copy(self.ex_front_1) - self.ez_back_1 = np.copy(self.ez_front_1) - self.ez_back_0 = np.copy(self.ez_front_1) - - self.ey_left_1 = np.zeros((self.nwy, self.nwz + 1)) - self.ey_left_0 = np.copy(self.ey_left_1) - self.ez_left_1 = np.zeros((self.nwy + 1, self.nwz)) - self.ez_left_0 = np.copy(self.ez_left_1) - - self.ey_right_1 = np.copy(self.ey_left_1) - self.ey_right_0 = np.copy(self.ey_left_1) - self.ez_right_1 = np.copy(self.ez_left_1) - self.ez_right_0 = np.copy(self.ez_left_1) - - self.ex_bottom_1 = np.zeros((self.nwx, self.nwy + 1)) - self.ex_bottom_0 = np.copy(self.ex_bottom_1) - self.ey_bottom_1 = np.zeros((self.nwx + 1, self.nwy)) - self.ey_bottom_0 = np.copy(self.ey_bottom_1) - - self.ex_top_1 = np.copy(self.ex_bottom_1) - self.ex_top_0 = np.copy(self.ex_bottom_1) - self.ey_top_1 = np.copy(self.ey_bottom_1) - self.ey_top_0 = np.copy(self.ey_bottom_1) - - # Initialize the H precursor fields - self.hx_front_1 = np.copy(self.ez_front_1) - self.hx_front_0 = np.copy(self.ez_front_1) - self.hz_front_1 = np.copy(self.ex_front_1) - self.hz_front_0 = np.copy(self.ex_front_1) - - self.hx_back_1 = np.copy(self.hx_front_1) - self.hx_back_0 = np.copy(self.hx_front_1) - self.hz_back_1 = np.copy(self.hz_front_1) - self.hz_back_0 = np.copy(self.hz_front_1) - - self.hy_left_1 = np.copy(self.ez_left_1) - self.hy_left_0 = np.copy(self.ez_left_1) - self.hz_left_1 = np.copy(self.ey_left_1) - self.hz_left_0 = np.copy(self.ey_left_1) - - self.hy_right_1 = np.copy(self.hy_left_1) - self.hy_right_0 = np.copy(self.hy_left_1) - self.hz_right_1 = np.copy(self.hz_left_1) - self.hz_right_0 = np.copy(self.hz_left_1) - - self.hx_top_1 = np.copy(self.ey_top_1) - self.hx_top_0 = np.copy(self.ey_top_1) - self.hy_top_1 = np.copy(self.ex_top_1) - self.hy_top_0 = np.copy(self.ex_top_1) - - self.hx_bottom_1 = np.copy(self.hx_top_1) - self.hx_bottom_0 = np.copy(self.hx_top_1) - self.hy_bottom_1 = np.copy(self.hy_top_1) - self.hy_bottom_0 = np.copy(self.hy_top_1) - - def _initialize_field_names(self): - - self.fn_m = [ - 'hx_front', 'hz_front', - 'hx_back', 'hz_back', - 'hy_left', 'hz_left', - 'hy_right', 'hz_right', - 'hx_top', 'hy_top', - 'hx_bottom', 'hy_bottom' - ] - - self.fn_e = [ - 'ex_front', 'ez_front', - 'ex_back', 'ez_back', - 'ey_left', 'ez_left', - 'ey_right', 'ez_right', - 'ex_top', 'ey_top', - 'ex_bottom', 'ey_bottom' - ] - - def update_previous_timestep_fields(self, field_names): - for fn in field_names: - val = getattr(self, fn + '_1') - val_c = np.copy(val) - setattr(self, fn + '_0', val_c) - - def initialize_magnetic_slices_array(self): - - # Array contains the indices at which the main grid should be sliced - # to obtain the 2 2d array of H nodes required for interpolation - # across the IS boundary for each h field on each face of the subgrid - - # Extend the surface so that the outer fields can be interpolated - # more accurately - #i0 = self.i0 - 1 - #j0 = self.j0 - 1 - #k0 = self.k0 - 1 - #i1 = self.i1 + 1 - #j1 = self.j1 + 1 - #k1 = self.k1 + 1 - - # not extended - i0 = self.i0 - j0 = self.j0 - k0 = self.k0 - i1 = self.i1 - j1 = self.j1 - k1 = self.k1 - - slices = [ - ['hy_left_1', False, - (self.i0 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], - ['hy_right_1', False, - (self.i1 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], - ['hz_left_1', True, - (self.i0 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], - ['hz_right_1', True, - (self.i1 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], - ['hx_front_1', False, - (slice(i0, i1 + 1, 1), self.j0 - 1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), self.Hx], - ['hx_back_1', False, - (slice(i0, i1 + 1, 1), self.j1 - 1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), self.Hx], - ['hz_front_1', True, - (slice(i0, i1, 1), self.j0 - 1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), self.Hz], - ['hz_back_1', True, - (slice(i0, i1, 1), self.j1 - 1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), self.Hz], - ['hx_bottom_1', False, - # check these indexes - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), self.Hx], - ['hx_top_1', False, - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), self.Hx], - ['hy_bottom_1', True, - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), self.Hy], - ['hy_top_1', True, - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), self.Hy] - ] - - for obj in slices: - sliced_field = obj[-1][obj[2]] - obj[1] = self.create_interpolated_coords(obj[1], sliced_field) - - self.magnetic_slices = slices - - def initialize_electric_slices_array(self): - # Extend the region sliced from the main grid by 1 cell. - # this allows more accurate interpolation for the outernodes - #i0 = self.i0 - 1 - #j0 = self.j0 - 1 - #k0 = self.k0 - 1 - #i1 = self.i1 + 1 - #j1 = self.j1 + 1 - #k1 = self.k1 + 1 - - # not extended - i0 = self.i0 - j0 = self.j0 - k0 = self.k0 - i1 = self.i1 - j1 = self.j1 - k1 = self.k1 - - # Spatially interpolate nodes - slices = [ - ['ex_front_1', True, (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), self.Ex], - ['ex_back_1', True, (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), self.Ex], - ['ez_front_1', False, (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), self.Ez], - ['ez_back_1', False, (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), self.Ez], - - ['ey_left_1', True, (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Ey], - ['ey_right_1', True, (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Ey], - ['ez_left_1', False, (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Ez], - ['ez_right_1', False, (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Ez], - - ['ex_bottom_1', True, (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), self.Ex], - ['ex_top_1', True, (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), self.Ex], - ['ey_bottom_1', False, (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), self.Ey], - ['ey_top_1', False, (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), self.Ey] - ] - - for obj in slices: - sliced_field = obj[-1][obj[2]] - obj[1] = self.create_interpolated_coords(obj[1], sliced_field) - - self.electric_slices = slices - - def create_interpolated_coords(self, mid, field): - - n_x = field.shape[0] - n_y = field.shape[1] - - if mid: - x = np.arange(0.5, n_x, 1.0) - z = np.arange(0, n_y, 1.0) - - # Coordinates that require interpolated values - x_sg = np.linspace(self.d, n_x - self.d, n_x * self.ratio) - z_sg = np.linspace(0, n_y - 1, (n_y - 1) * self.ratio + 1) - - else: - x = np.arange(0, n_x, 1.0) - z = np.arange(0.5, n_y, 1.0) - - # Coordinates that require interpolated values - x_sg = np.linspace(0, n_x - 1, (n_x - 1) * self.ratio + 1) - z_sg = np.linspace(self.d, n_y - self.d, n_y * self.ratio) - - return (x, z, x_sg, z_sg) - - def update_magnetic(self): - - # Copy previous time step magnetic field values to the previous - # time step variables - self.update_previous_timestep_fields(self.fn_m) - - for obj in self.magnetic_slices: - - # Grab the main grid fields used to interpolate across the IS - # f = self.Hi[slice] - f_1 = obj[-1][obj[2]] - f_2 = obj[-1][obj[3]] - if ('left' in obj[0] or - 'bottom' in obj[0] or - 'front' in obj[0]): - w = self.l_weight - else: - w = self.r_weight - c1, c2 = calculate_weighting_coefficients(w, self.ratio) - # transverse interpolated h field - f_t = c1 * f_1 + c2 * f_2 - - # interpolate over a fine grid - f_i = self.interpolate_to_sub_grid(f_t, obj[1]) - - if f_i == f_t: - raise ValueError - - # discard the outer nodes only required for interpolation - #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] - f = f_i - setattr(self, obj[0], f) - - def update_electric(self): - - self.update_previous_timestep_fields(self.fn_e) - - for obj in self.electric_slices: - f_m = obj[-1][obj[2]] - f_i = self.interpolate_to_sub_grid(f_m, obj[1]) - f = f_i - #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] - setattr(self, obj[0], f) - - def interpolate_to_sub_grid(self, field, coords): - x, z, x_sg, z_sg = coords - ex_t = np.transpose(field) - f = interpolate.interp2d(x, z, ex_t, kind=self.interpolation) - #f = interpolate.RectBivariateSpline(x, z, field) - ex_inter_t = f(x_sg, z_sg) - ex_inter = np.transpose(ex_inter_t) - #ex_inter = ex_inter_t - - return ex_inter - - -class PlaneError(Exception): - pass +import numpy as np +from scipy import interpolate +import sys + + +def calculate_weighting_coefficients(x1, x): + c1 = (x - x1) / x + c2 = x1 / x + return (c1, c2) + + +class PrecusorNodes2dBase(object): + + def __init__(self, fdtd_grid, sub_grid): + self.G = fdtd_grid + self.ratio = sub_grid.ratio + self.nwx = sub_grid.nwx + self.nwy = sub_grid.nwy + self.sub_grid = sub_grid + self.interpolation = sub_grid.interpolation + + self.Hx = fdtd_grid.Hx + self.Hy = fdtd_grid.Hy + self.Hz = fdtd_grid.Hz + self.Ex = fdtd_grid.Ex + self.Ey = fdtd_grid.Ey + self.Ez = fdtd_grid.Ez + + # Main grid indices of subgrids + self.i0 = sub_grid.i0 + self.j0 = sub_grid.j0 + self.k0 = sub_grid.k0 + self.i1 = sub_grid.i1 + self.j1 = sub_grid.j1 + self.k1 = sub_grid.k1 + + # dl / 2 sub cell + self.d = 1 / (2 * self.ratio) + + self._initialize_fields() + self._initialize_field_names() + + self.l_weight = self.ratio // 2 + self.r_weight = self.ratio - self.l_weight + #self.l_weight = 1 + #self.r_weight = 2 + + def _initialize_fields(self): + print('dont call me') + sys.exit() + pass + + def _initialize_field_names(self): + print('dont call me') + sys.exit() + pass + + def interpolate_magnetic_in_time(self, m): + self.weight_pre_and_current_fields(m, self.fn_m) + + def interpolate_electric_in_time(self, m): + self.weight_pre_and_current_fields(m, self.fn_e) + + def weight_pre_and_current_fields(self, m, field_names): + c1, c2 = calculate_weighting_coefficients(m, self.ratio) + + for f in field_names: + try: + val = c1 * getattr(self, f + '_0') + c2 * getattr(self, f + '_1') + except ValueError: + print(self.ex_front_0.shape) + print(self.ex_front_1.shape) + raise Exception(f) + setattr(self, f, val) + + def calc_exact_field(self, field_names): + """ + Function to set the fields used in update calculations to the + values at the current main time step. + i.e. ey_left = copy.ey_left_1 + """ + for f in field_names: + val = np.copy(getattr(self, f + '_1')) + setattr(self, f, val) + + def calc_exact_magnetic_in_time(self): + self.calc_exact_field(self.fn_m) + + def calc_exact_electric_in_time(self): + self.calc_exact_field(self.fn_e) + + +class PrecursorNodes2dTM(PrecusorNodes2dBase): + + def __init__(self, fdtd_grid, sub_grid): + super().__init__(fdtd_grid, sub_grid) + + def _initialize_fields(self): + + # H precursors + self.hx_front_0 = np.zeros(self.nwx + 1) + self.hx_front_1 = np.zeros(self.nwx + 1) + + self.hx_back_0 = np.zeros(self.nwx + 1) + self.hx_back_1 = np.zeros(self.nwx + 1) + + self.hy_left_0 = np.zeros(self.nwy + 1) + self.hy_left_1 = np.zeros(self.nwy + 1) + + self.hy_right_0 = np.zeros(self.nwy + 1) + self.hy_right_1 = np.zeros(self.nwy + 1) + + # E precursors + self.ez_front_0 = np.zeros(self.nwx + 1) + self.ez_front_1 = np.zeros(self.nwx + 1) + + self.ez_back_0 = np.zeros(self.nwx + 1) + self.ez_back_1 = np.zeros(self.nwx + 1) + + self.ez_left_0 = np.zeros(self.nwy + 1) + self.ez_left_1 = np.zeros(self.nwy + 1) + + self.ez_right_0 = np.zeros(self.nwy + 1) + self.ez_right_1 = np.zeros(self.nwy + 1) + + def _initialize_field_names(self): + self.fn_m = ['hy_left', 'hy_right', 'hx_back', 'hx_front'] + self.fn_e = ['ez_left', 'ez_right', 'ez_back', 'ez_front'] + + def interpolate_across_sub_cells(self, y, n0, n1): + n = n1 - n0 + x = np.arange(0, y.shape[0], 1.0) + x_new = np.linspace(1, y.shape[0] - 2, n * self.ratio + 1) + f = interpolate.interp1d(x, y, kind='linear') + y_new = f(x_new) + """plt.plot(x, y, 'x', x_new, y_new, '.') + plt.show() + sys.exit()""" + return y_new + + def update_electric(self): + # f = np.sin(np.arange(0, 13)) + + # line of Ez nodes along the left edge of the IS + self.ez_left_0 = np.copy(self.ez_left_1) + # interpolate nodes between two Ez nodes 1 cell beyond/infront the IS + f = self.Ez[self.i0, self.j0 - 1:self.j1 + 2, 0] + self.ez_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + self.ez_right_0 = np.copy(self.ez_right_1) + f = self.Ez[self.i1, self.j0 - 1:self.j1 + 2, 0] + self.ez_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + self.ez_front_0 = np.copy(self.ez_front_1) + f = self.Ez[self.i0 - 1:self.i1 + 2, self.j0, 0] + self.ez_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + self.ez_back_0 = np.copy(self.ez_back_1) + f = self.Ez[self.i0 - 1:self.i1 + 2, self.j1, 0] + self.ez_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + def update_hy_left_1(self): + + self.hy_left_0 = np.copy(self.hy_left_1) + c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) + l = self.Hy[self.i0 - 1, self.j0 - 1:self.j1 + 2, 0] + r = self.Hy[self.i0, self.j0 - 1:self.j1 + 2, 0] + + # Tranverse interpolated hz + hz_t_i = c1 * l + c2 * r + # Tangential interpolated hz + self.hy_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hy_right_1(self): + + self.hy_right_0 = np.copy(self.hy_right_1) + c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) + l = self.Hy[self.i1 - 1, self.j0 - 1:self.j1 + 2, 0] + r = self.Hy[self.i1, self.j0 - 1:self.j1 + 2, 0] + hz_t_i = c1 * l + c2 * r + self.hy_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hx_back_1(self): + self.hx_back_0 = np.copy(self.hx_back_1) + c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) + l = self.Hx[self.i0 - 1:self.i1 + 2, self.j1 - 1, 0] + r = self.Hx[self.i0 - 1:self.i1 + 2, self.j1, 0] + hz_t_i = c1 * l + c2 * r + self.hx_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_hx_front_1(self): + + self.hx_front_0 = np.copy(self.hx_front_1) + c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) + l = self.Hx[self.i0 - 1:self.i1 + 2, self.j0 - 1, 0] + r = self.Hx[self.i0 - 1:self.i1 + 2, self.j0, 0] + hz_t_i = c1 * l + c2 * r + self.hx_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_magnetic(self): + self.update_hy_left_1() + self.update_hy_right_1() + self.update_hx_back_1() + self.update_hx_front_1() + + +class PrecursorNodes2d(PrecusorNodes2dBase): + + def __init__(self, fdtd_grid, sub_grid): + super().__init__(fdtd_grid, sub_grid) + + def _initialize_fields(self): + # E precursors + self.ex_front_0 = np.zeros(self.nwx) + self.ex_front_1 = np.zeros(self.nwx) + + self.ex_back_0 = np.zeros(self.nwx) + self.ex_back_1 = np.zeros(self.nwx) + + self.ey_left_0 = np.zeros(self.nwy) + self.ey_left_1 = np.zeros(self.nwy) + + self.ey_right_0 = np.zeros(self.nwy) + self.ey_right_1 = np.zeros(self.nwy) + + # H precursors + self.hz_front_0 = np.zeros(self.nwx) + self.hz_front_1 = np.zeros(self.nwx) + + self.hz_back_0 = np.zeros(self.nwx) + self.hz_back_1 = np.zeros(self.nwx) + + self.hz_left_0 = np.zeros(self.nwy) + self.hz_left_1 = np.zeros(self.nwy) + + self.hz_right_0 = np.zeros(self.nwy) + self.hz_right_1 = np.zeros(self.nwy) + + def _initialize_field_names(self): + # Field names + self.fn_m = ['hz_left', 'hz_right', 'hz_back', 'hz_front'] + self.fn_e = ['ey_left', 'ey_right', 'ex_back', 'ex_front'] + + def update_hz_left_1(self): + self.hz_left_0 = np.copy(self.hz_left_1) + c1, c2 = calculate_weighting_coefficients(1, self.ratio) + l = self.Hz[self.i0 - 1, self.j0 - 1:self.j1 + 1, 1] + r = self.Hz[self.i0, self.j0 - 1:self.j1 + 1, 1] + + # Tranverse interpolated hz + hz_t_i = c1 * l + c2 * r + # Tangential interpolated hz + self.hz_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hz_right_1(self): + + self.hz_right_0 = np.copy(self.hz_right_1) + c1, c2 = calculate_weighting_coefficients(2, self.ratio) + l = self.Hz[self.i1 - 1, self.j0 - 1:self.j1 + 1, 1] + r = self.Hz[self.i1, self.j0 - 1:self.j1 + 1, 1] + hz_t_i = c1 * l + c2 * r + self.hz_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hz_back_1(self): + self.hz_back_0 = np.copy(self.hz_back_1) + c1, c2 = calculate_weighting_coefficients(2, self.ratio) + l = self.Hz[self.i0 - 1:self.i1 + 1, self.j1 - 1, 1] + r = self.Hz[self.i0 - 1:self.i1 + 1, self.j1, 1] + hz_t_i = c1 * l + c2 * r + self.hz_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_hz_front_1(self): + self.hz_front_0 = np.copy(self.hz_front_1) + c1, c2 = calculate_weighting_coefficients(1, self.ratio) + l = self.Hz[self.i0 - 1:self.i1 + 1, self.j0 - 1, 1] + r = self.Hz[self.i0 - 1:self.i1 + 1, self.j0, 1] + hz_t_i = c1 * l + c2 * r + self.hz_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_magnetic(self): + self.update_hz_left_1() + self.update_hz_right_1() + self.update_hz_back_1() + self.update_hz_front_1() + + def update_electric(self): + + """Function to calculate the precursor nodes at the next main + grid time-step + """ + + # LEFT BOUNDARY + # Save the last precursor node values - these will be used interpolate + # field values at sub grid time step values. + self.ey_left_0 = np.copy(self.ey_left_1) + # line of Ex nodes along the left edge of the IS + f = self.Ey[self.i0, self.j0 - 1:self.j1 + 1, 1] + self.ey_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + # RIGHT BOUNDARY + self.ey_right_0 = np.copy(self.ey_right_1) + f = self.Ey[self.i1, self.j0 - 1:self.j1 + 1, 1] + self.ey_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + # BACK BOUNDARY + self.ex_back_0 = np.copy(self.ex_back_1) + f = self.Ex[self.i0 - 1:self.i1 + 1, self.j1, 1] + self.ex_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + # FRONT BOUNDARY + self.ex_front_0 = np.copy(self.ex_front_1) + f = self.Ex[self.i0 - 1:self.i1 + 1, self.j0, 1] + self.ex_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + def interpolate_across_sub_cells(self, y, n0, n1): + n = n1 - n0 + x = np.arange(0.5, y.shape[0], 1.0) + x_new = np.linspace(1 + self.d, (1 + n) - self.d, n * self.ratio) + f = interpolate.interp1d(x, y, kind='linear') + y_new = f(x_new) + + return y_new + + +class PrecursorNodes(PrecusorNodes2dBase): + + def __init__(self, fdtd_grid, sub_grid): + + self.nwz = sub_grid.nwz + super().__init__(fdtd_grid, sub_grid) + self.initialize_magnetic_slices_array() + self.initialize_electric_slices_array() + + def build_coefficient_matrix(self, update_coefficients, lookup_id, inc_field, name): + """Function builds a 2d matrix of update coefficients for each face and field. + This allows the is nodes to be updated via slicing rather than iterating which is much faster + """ + pass + + def _initialize_fields(self): + + # Initialise the precursor arrays + + # The precursors are divided up into the 6. Each represent 1 + # face of a huygens cube surface. We represent each face as a 2d array + # containing a field in a particular direction. + + # _1 are the fields at the current main grid timestep + # _0 are the fields at the previous main grid timestep + # We store both fields so we can do different interpolations between + # them on the fly. + + # Front face + self.ex_front_1 = np.zeros((self.nwx, self.nwz + 1)) + self.ex_front_0 = np.copy(self.ex_front_1) + self.ez_front_1 = np.zeros((self.nwx + 1, self.nwz)) + self.ez_front_0 = np.copy(self.ez_front_1) + + # The same as the opposite face + self.ex_back_1 = np.copy(self.ex_front_1) + self.ex_back_0 = np.copy(self.ex_front_1) + self.ez_back_1 = np.copy(self.ez_front_1) + self.ez_back_0 = np.copy(self.ez_front_1) + + self.ey_left_1 = np.zeros((self.nwy, self.nwz + 1)) + self.ey_left_0 = np.copy(self.ey_left_1) + self.ez_left_1 = np.zeros((self.nwy + 1, self.nwz)) + self.ez_left_0 = np.copy(self.ez_left_1) + + self.ey_right_1 = np.copy(self.ey_left_1) + self.ey_right_0 = np.copy(self.ey_left_1) + self.ez_right_1 = np.copy(self.ez_left_1) + self.ez_right_0 = np.copy(self.ez_left_1) + + self.ex_bottom_1 = np.zeros((self.nwx, self.nwy + 1)) + self.ex_bottom_0 = np.copy(self.ex_bottom_1) + self.ey_bottom_1 = np.zeros((self.nwx + 1, self.nwy)) + self.ey_bottom_0 = np.copy(self.ey_bottom_1) + + self.ex_top_1 = np.copy(self.ex_bottom_1) + self.ex_top_0 = np.copy(self.ex_bottom_1) + self.ey_top_1 = np.copy(self.ey_bottom_1) + self.ey_top_0 = np.copy(self.ey_bottom_1) + + # Initialize the H precursor fields + self.hx_front_1 = np.copy(self.ez_front_1) + self.hx_front_0 = np.copy(self.ez_front_1) + self.hz_front_1 = np.copy(self.ex_front_1) + self.hz_front_0 = np.copy(self.ex_front_1) + + self.hx_back_1 = np.copy(self.hx_front_1) + self.hx_back_0 = np.copy(self.hx_front_1) + self.hz_back_1 = np.copy(self.hz_front_1) + self.hz_back_0 = np.copy(self.hz_front_1) + + self.hy_left_1 = np.copy(self.ez_left_1) + self.hy_left_0 = np.copy(self.ez_left_1) + self.hz_left_1 = np.copy(self.ey_left_1) + self.hz_left_0 = np.copy(self.ey_left_1) + + self.hy_right_1 = np.copy(self.hy_left_1) + self.hy_right_0 = np.copy(self.hy_left_1) + self.hz_right_1 = np.copy(self.hz_left_1) + self.hz_right_0 = np.copy(self.hz_left_1) + + self.hx_top_1 = np.copy(self.ey_top_1) + self.hx_top_0 = np.copy(self.ey_top_1) + self.hy_top_1 = np.copy(self.ex_top_1) + self.hy_top_0 = np.copy(self.ex_top_1) + + self.hx_bottom_1 = np.copy(self.hx_top_1) + self.hx_bottom_0 = np.copy(self.hx_top_1) + self.hy_bottom_1 = np.copy(self.hy_top_1) + self.hy_bottom_0 = np.copy(self.hy_top_1) + + def _initialize_field_names(self): + + self.fn_m = [ + 'hx_front', 'hz_front', + 'hx_back', 'hz_back', + 'hy_left', 'hz_left', + 'hy_right', 'hz_right', + 'hx_top', 'hy_top', + 'hx_bottom', 'hy_bottom' + ] + + self.fn_e = [ + 'ex_front', 'ez_front', + 'ex_back', 'ez_back', + 'ey_left', 'ez_left', + 'ey_right', 'ez_right', + 'ex_top', 'ey_top', + 'ex_bottom', 'ey_bottom' + ] + + def update_previous_timestep_fields(self, field_names): + for fn in field_names: + val = getattr(self, fn + '_1') + val_c = np.copy(val) + setattr(self, fn + '_0', val_c) + + def initialize_magnetic_slices_array(self): + + # Array contains the indices at which the main grid should be sliced + # to obtain the 2 2d array of H nodes required for interpolation + # across the IS boundary for each h field on each face of the subgrid + + # Extend the surface so that the outer fields can be interpolated + # more accurately + #i0 = self.i0 - 1 + #j0 = self.j0 - 1 + #k0 = self.k0 - 1 + #i1 = self.i1 + 1 + #j1 = self.j1 + 1 + #k1 = self.k1 + 1 + + # not extended + i0 = self.i0 + j0 = self.j0 + k0 = self.k0 + i1 = self.i1 + j1 = self.j1 + k1 = self.k1 + + slices = [ + ['hy_left_1', False, + (self.i0 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], + ['hy_right_1', False, + (self.i1 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], + ['hz_left_1', True, + (self.i0 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], + ['hz_right_1', True, + (self.i1 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], + ['hx_front_1', False, + (slice(i0, i1 + 1, 1), self.j0 - 1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), self.Hx], + ['hx_back_1', False, + (slice(i0, i1 + 1, 1), self.j1 - 1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), self.Hx], + ['hz_front_1', True, + (slice(i0, i1, 1), self.j0 - 1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), self.Hz], + ['hz_back_1', True, + (slice(i0, i1, 1), self.j1 - 1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), self.Hz], + ['hx_bottom_1', False, + # check these indexes + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), self.Hx], + ['hx_top_1', False, + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), self.Hx], + ['hy_bottom_1', True, + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), self.Hy], + ['hy_top_1', True, + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), self.Hy] + ] + + for obj in slices: + sliced_field = obj[-1][obj[2]] + obj[1] = self.create_interpolated_coords(obj[1], sliced_field) + + self.magnetic_slices = slices + + def initialize_electric_slices_array(self): + # Extend the region sliced from the main grid by 1 cell. + # this allows more accurate interpolation for the outernodes + #i0 = self.i0 - 1 + #j0 = self.j0 - 1 + #k0 = self.k0 - 1 + #i1 = self.i1 + 1 + #j1 = self.j1 + 1 + #k1 = self.k1 + 1 + + # not extended + i0 = self.i0 + j0 = self.j0 + k0 = self.k0 + i1 = self.i1 + j1 = self.j1 + k1 = self.k1 + + # Spatially interpolate nodes + slices = [ + ['ex_front_1', True, (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), self.Ex], + ['ex_back_1', True, (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), self.Ex], + ['ez_front_1', False, (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), self.Ez], + ['ez_back_1', False, (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), self.Ez], + + ['ey_left_1', True, (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Ey], + ['ey_right_1', True, (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Ey], + ['ez_left_1', False, (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Ez], + ['ez_right_1', False, (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Ez], + + ['ex_bottom_1', True, (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), self.Ex], + ['ex_top_1', True, (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), self.Ex], + ['ey_bottom_1', False, (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), self.Ey], + ['ey_top_1', False, (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), self.Ey] + ] + + for obj in slices: + sliced_field = obj[-1][obj[2]] + obj[1] = self.create_interpolated_coords(obj[1], sliced_field) + + self.electric_slices = slices + + def create_interpolated_coords(self, mid, field): + + n_x = field.shape[0] + n_y = field.shape[1] + + if mid: + x = np.arange(0.5, n_x, 1.0) + z = np.arange(0, n_y, 1.0) + + # Coordinates that require interpolated values + x_sg = np.linspace(self.d, n_x - self.d, n_x * self.ratio) + z_sg = np.linspace(0, n_y - 1, (n_y - 1) * self.ratio + 1) + + else: + x = np.arange(0, n_x, 1.0) + z = np.arange(0.5, n_y, 1.0) + + # Coordinates that require interpolated values + x_sg = np.linspace(0, n_x - 1, (n_x - 1) * self.ratio + 1) + z_sg = np.linspace(self.d, n_y - self.d, n_y * self.ratio) + + return (x, z, x_sg, z_sg) + + def update_magnetic(self): + + # Copy previous time step magnetic field values to the previous + # time step variables + self.update_previous_timestep_fields(self.fn_m) + + for obj in self.magnetic_slices: + + # Grab the main grid fields used to interpolate across the IS + # f = self.Hi[slice] + f_1 = obj[-1][obj[2]] + f_2 = obj[-1][obj[3]] + if ('left' in obj[0] or + 'bottom' in obj[0] or + 'front' in obj[0]): + w = self.l_weight + else: + w = self.r_weight + c1, c2 = calculate_weighting_coefficients(w, self.ratio) + # transverse interpolated h field + f_t = c1 * f_1 + c2 * f_2 + + # interpolate over a fine grid + f_i = self.interpolate_to_sub_grid(f_t, obj[1]) + + if f_i == f_t: + raise ValueError + + # discard the outer nodes only required for interpolation + #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] + f = f_i + setattr(self, obj[0], f) + + def update_electric(self): + + self.update_previous_timestep_fields(self.fn_e) + + for obj in self.electric_slices: + f_m = obj[-1][obj[2]] + f_i = self.interpolate_to_sub_grid(f_m, obj[1]) + f = f_i + #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] + setattr(self, obj[0], f) + + def interpolate_to_sub_grid(self, field, coords): + x, z, x_sg, z_sg = coords + ex_t = np.transpose(field) + f = interpolate.interp2d(x, z, ex_t, kind=self.interpolation) + #f = interpolate.RectBivariateSpline(x, z, field) + ex_inter_t = f(x_sg, z_sg) + ex_inter = np.transpose(ex_inter_t) + #ex_inter = ex_inter_t + + return ex_inter + + +class PlaneError(Exception): + pass diff --git a/gprMax/subgrids/precursor_nodes_filtered.py b/gprMax/subgrids/precursor_nodes_filtered.py index a1046c76..145330cd 100644 --- a/gprMax/subgrids/precursor_nodes_filtered.py +++ b/gprMax/subgrids/precursor_nodes_filtered.py @@ -1,738 +1,738 @@ -import numpy as np -from scipy import interpolate -import sys - - -def calculate_weighting_coefficients(x1, x): - c1 = (x - x1) / x - c2 = x1 / x - return (c1, c2) - - -class PrecusorNodes2dBase(object): - - def __init__(self, fdtd_grid, sub_grid): - self.G = fdtd_grid - self.ratio = sub_grid.ratio - self.nwx = sub_grid.nwx - self.nwy = sub_grid.nwy - self.sub_grid = sub_grid - self.interpolation = sub_grid.interpolation - - self.Hx = fdtd_grid.Hx - self.Hy = fdtd_grid.Hy - self.Hz = fdtd_grid.Hz - self.Ex = fdtd_grid.Ex - self.Ey = fdtd_grid.Ey - self.Ez = fdtd_grid.Ez - - # Main grid indices of subgrids - self.i0 = sub_grid.i0 - self.j0 = sub_grid.j0 - self.k0 = sub_grid.k0 - self.i1 = sub_grid.i1 - self.j1 = sub_grid.j1 - self.k1 = sub_grid.k1 - - # dl / 2 sub cell - self.d = 1 / (2 * self.ratio) - - self._initialize_fields() - self._initialize_field_names() - - self.l_weight = self.ratio // 2 - self.r_weight = self.ratio - self.l_weight - #self.l_weight = 1 - #self.r_weight = 2 - - def _initialize_fields(self): - print('dont call me') - sys.exit() - pass - - def _initialize_field_names(self): - print('dont call me') - sys.exit() - pass - - def interpolate_magnetic_in_time(self, m): - self.weight_pre_and_current_fields(m, self.fn_m) - - def interpolate_electric_in_time(self, m): - self.weight_pre_and_current_fields(m, self.fn_e) - - def weight_pre_and_current_fields(self, m, field_names): - c1, c2 = calculate_weighting_coefficients(m, self.ratio) - - for f in field_names: - try: - val = c1 * getattr(self, f + '_0') + c2 * getattr(self, f + '_1') - except ValueError: - print(self.ex_front_0.shape) - print(self.ex_front_1.shape) - raise Exception(f) - setattr(self, f, val) - - def calc_exact_field(self, field_names): - """ - Function to set the fields used in update calculations to the - values at the current main time step. - i.e. ey_left = copy.ey_left_1 - """ - for f in field_names: - val = np.copy(getattr(self, f + '_1')) - setattr(self, f, val) - - def calc_exact_magnetic_in_time(self): - self.calc_exact_field(self.fn_m) - - def calc_exact_electric_in_time(self): - self.calc_exact_field(self.fn_e) - - -class PrecursorNodes2dTM(PrecusorNodes2dBase): - - def __init__(self, fdtd_grid, sub_grid): - super().__init__(fdtd_grid, sub_grid) - - def _initialize_fields(self): - - # H precursors - self.hx_front_0 = np.zeros(self.nwx + 1) - self.hx_front_1 = np.zeros(self.nwx + 1) - - self.hx_back_0 = np.zeros(self.nwx + 1) - self.hx_back_1 = np.zeros(self.nwx + 1) - - self.hy_left_0 = np.zeros(self.nwy + 1) - self.hy_left_1 = np.zeros(self.nwy + 1) - - self.hy_right_0 = np.zeros(self.nwy + 1) - self.hy_right_1 = np.zeros(self.nwy + 1) - - # E precursors - self.ez_front_0 = np.zeros(self.nwx + 1) - self.ez_front_1 = np.zeros(self.nwx + 1) - - self.ez_back_0 = np.zeros(self.nwx + 1) - self.ez_back_1 = np.zeros(self.nwx + 1) - - self.ez_left_0 = np.zeros(self.nwy + 1) - self.ez_left_1 = np.zeros(self.nwy + 1) - - self.ez_right_0 = np.zeros(self.nwy + 1) - self.ez_right_1 = np.zeros(self.nwy + 1) - - def _initialize_field_names(self): - self.fn_m = ['hy_left', 'hy_right', 'hx_back', 'hx_front'] - self.fn_e = ['ez_left', 'ez_right', 'ez_back', 'ez_front'] - - def interpolate_across_sub_cells(self, y, n0, n1): - n = n1 - n0 - x = np.arange(0, y.shape[0], 1.0) - x_new = np.linspace(1, y.shape[0] - 2, n * self.ratio + 1) - f = interpolate.interp1d(x, y, kind='linear') - y_new = f(x_new) - """plt.plot(x, y, 'x', x_new, y_new, '.') - plt.show() - sys.exit()""" - return y_new - - def update_electric(self): - # f = np.sin(np.arange(0, 13)) - - # line of Ez nodes along the left edge of the IS - self.ez_left_0 = np.copy(self.ez_left_1) - # interpolate nodes between two Ez nodes 1 cell beyond/infront the IS - f = self.Ez[self.i0, self.j0 - 1:self.j1 + 2, 0] - self.ez_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - self.ez_right_0 = np.copy(self.ez_right_1) - f = self.Ez[self.i1, self.j0 - 1:self.j1 + 2, 0] - self.ez_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - self.ez_front_0 = np.copy(self.ez_front_1) - f = self.Ez[self.i0 - 1:self.i1 + 2, self.j0, 0] - self.ez_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - self.ez_back_0 = np.copy(self.ez_back_1) - f = self.Ez[self.i0 - 1:self.i1 + 2, self.j1, 0] - self.ez_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - def update_hy_left_1(self): - - self.hy_left_0 = np.copy(self.hy_left_1) - c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) - l = self.Hy[self.i0 - 1, self.j0 - 1:self.j1 + 2, 0] - r = self.Hy[self.i0, self.j0 - 1:self.j1 + 2, 0] - - # Tranverse interpolated hz - hz_t_i = c1 * l + c2 * r - # Tangential interpolated hz - self.hy_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hy_right_1(self): - - self.hy_right_0 = np.copy(self.hy_right_1) - c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) - l = self.Hy[self.i1 - 1, self.j0 - 1:self.j1 + 2, 0] - r = self.Hy[self.i1, self.j0 - 1:self.j1 + 2, 0] - hz_t_i = c1 * l + c2 * r - self.hy_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hx_back_1(self): - self.hx_back_0 = np.copy(self.hx_back_1) - c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) - l = self.Hx[self.i0 - 1:self.i1 + 2, self.j1 - 1, 0] - r = self.Hx[self.i0 - 1:self.i1 + 2, self.j1, 0] - hz_t_i = c1 * l + c2 * r - self.hx_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_hx_front_1(self): - - self.hx_front_0 = np.copy(self.hx_front_1) - c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) - l = self.Hx[self.i0 - 1:self.i1 + 2, self.j0 - 1, 0] - r = self.Hx[self.i0 - 1:self.i1 + 2, self.j0, 0] - hz_t_i = c1 * l + c2 * r - self.hx_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_magnetic(self): - self.update_hy_left_1() - self.update_hy_right_1() - self.update_hx_back_1() - self.update_hx_front_1() - - -class PrecursorNodes2d(PrecusorNodes2dBase): - - def __init__(self, fdtd_grid, sub_grid): - super().__init__(fdtd_grid, sub_grid) - - def _initialize_fields(self): - # E precursors - self.ex_front_0 = np.zeros(self.nwx) - self.ex_front_1 = np.zeros(self.nwx) - - self.ex_back_0 = np.zeros(self.nwx) - self.ex_back_1 = np.zeros(self.nwx) - - self.ey_left_0 = np.zeros(self.nwy) - self.ey_left_1 = np.zeros(self.nwy) - - self.ey_right_0 = np.zeros(self.nwy) - self.ey_right_1 = np.zeros(self.nwy) - - # H precursors - self.hz_front_0 = np.zeros(self.nwx) - self.hz_front_1 = np.zeros(self.nwx) - - self.hz_back_0 = np.zeros(self.nwx) - self.hz_back_1 = np.zeros(self.nwx) - - self.hz_left_0 = np.zeros(self.nwy) - self.hz_left_1 = np.zeros(self.nwy) - - self.hz_right_0 = np.zeros(self.nwy) - self.hz_right_1 = np.zeros(self.nwy) - - def _initialize_field_names(self): - # Field names - self.fn_m = ['hz_left', 'hz_right', 'hz_back', 'hz_front'] - self.fn_e = ['ey_left', 'ey_right', 'ex_back', 'ex_front'] - - def update_hz_left_1(self): - self.hz_left_0 = np.copy(self.hz_left_1) - c1, c2 = calculate_weighting_coefficients(1, self.ratio) - l = self.Hz[self.i0 - 1, self.j0 - 1:self.j1 + 1, 1] - r = self.Hz[self.i0, self.j0 - 1:self.j1 + 1, 1] - - # Tranverse interpolated hz - hz_t_i = c1 * l + c2 * r - # Tangential interpolated hz - self.hz_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hz_right_1(self): - - self.hz_right_0 = np.copy(self.hz_right_1) - c1, c2 = calculate_weighting_coefficients(2, self.ratio) - l = self.Hz[self.i1 - 1, self.j0 - 1:self.j1 + 1, 1] - r = self.Hz[self.i1, self.j0 - 1:self.j1 + 1, 1] - hz_t_i = c1 * l + c2 * r - self.hz_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) - - def update_hz_back_1(self): - self.hz_back_0 = np.copy(self.hz_back_1) - c1, c2 = calculate_weighting_coefficients(2, self.ratio) - l = self.Hz[self.i0 - 1:self.i1 + 1, self.j1 - 1, 1] - r = self.Hz[self.i0 - 1:self.i1 + 1, self.j1, 1] - hz_t_i = c1 * l + c2 * r - self.hz_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_hz_front_1(self): - self.hz_front_0 = np.copy(self.hz_front_1) - c1, c2 = calculate_weighting_coefficients(1, self.ratio) - l = self.Hz[self.i0 - 1:self.i1 + 1, self.j0 - 1, 1] - r = self.Hz[self.i0 - 1:self.i1 + 1, self.j0, 1] - hz_t_i = c1 * l + c2 * r - self.hz_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) - - def update_magnetic(self): - self.update_hz_left_1() - self.update_hz_right_1() - self.update_hz_back_1() - self.update_hz_front_1() - - def update_electric(self): - - """Function to calculate the precursor nodes at the next main - grid time-step - """ - - # LEFT BOUNDARY - # Save the last precursor node values - these will be used interpolate - # field values at sub grid time step values. - self.ey_left_0 = np.copy(self.ey_left_1) - # line of Ex nodes along the left edge of the IS - f = self.Ey[self.i0, self.j0 - 1:self.j1 + 1, 1] - self.ey_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - # RIGHT BOUNDARY - self.ey_right_0 = np.copy(self.ey_right_1) - f = self.Ey[self.i1, self.j0 - 1:self.j1 + 1, 1] - self.ey_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) - - # BACK BOUNDARY - self.ex_back_0 = np.copy(self.ex_back_1) - f = self.Ex[self.i0 - 1:self.i1 + 1, self.j1, 1] - self.ex_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - # FRONT BOUNDARY - self.ex_front_0 = np.copy(self.ex_front_1) - f = self.Ex[self.i0 - 1:self.i1 + 1, self.j0, 1] - self.ex_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) - - def interpolate_across_sub_cells(self, y, n0, n1): - n = n1 - n0 - x = np.arange(0.5, y.shape[0], 1.0) - x_new = np.linspace(1 + self.d, (1 + n) - self.d, n * self.ratio) - f = interpolate.interp1d(x, y, kind='linear') - y_new = f(x_new) - - return y_new - - -class PrecursorNodes(PrecusorNodes2dBase): - - def __init__(self, fdtd_grid, sub_grid): - - self.nwz = sub_grid.nwz - super().__init__(fdtd_grid, sub_grid) - self.initialize_magnetic_slices_array() - self.initialize_electric_slices_array() - - def build_coefficient_matrix(self, update_coefficients, lookup_id, inc_field, name): - """Function builds a 2d matrix of update coefficients for each face and field. - This allows the is nodes to be updated via slicing rather than iterating which is much faster - """ - pass - - def _initialize_fields(self): - - # Initialise the precursor arrays - - # The precursors are divided up into the 6. Each represent 1 - # face of a huygens cube surface. We represent each face as a 2d array - # containing a field in a particular direction. - - # _1 are the fields at the current main grid timestep - # _0 are the fields at the previous main grid timestep - # We store both fields so we can do different interpolations between - # them on the fly. - - # Front face - self.ex_front_1 = np.zeros((self.nwx, self.nwz + 1)) - self.ex_front_0 = np.copy(self.ex_front_1) - self.ez_front_1 = np.zeros((self.nwx + 1, self.nwz)) - self.ez_front_0 = np.copy(self.ez_front_1) - - # The same as the opposite face - self.ex_back_1 = np.copy(self.ex_front_1) - self.ex_back_0 = np.copy(self.ex_front_1) - self.ez_back_1 = np.copy(self.ez_front_1) - self.ez_back_0 = np.copy(self.ez_front_1) - - self.ey_left_1 = np.zeros((self.nwy, self.nwz + 1)) - self.ey_left_0 = np.copy(self.ey_left_1) - self.ez_left_1 = np.zeros((self.nwy + 1, self.nwz)) - self.ez_left_0 = np.copy(self.ez_left_1) - - self.ey_right_1 = np.copy(self.ey_left_1) - self.ey_right_0 = np.copy(self.ey_left_1) - self.ez_right_1 = np.copy(self.ez_left_1) - self.ez_right_0 = np.copy(self.ez_left_1) - - self.ex_bottom_1 = np.zeros((self.nwx, self.nwy + 1)) - self.ex_bottom_0 = np.copy(self.ex_bottom_1) - self.ey_bottom_1 = np.zeros((self.nwx + 1, self.nwy)) - self.ey_bottom_0 = np.copy(self.ey_bottom_1) - - self.ex_top_1 = np.copy(self.ex_bottom_1) - self.ex_top_0 = np.copy(self.ex_bottom_1) - self.ey_top_1 = np.copy(self.ey_bottom_1) - self.ey_top_0 = np.copy(self.ey_bottom_1) - - # Initialize the H precursor fields - self.hx_front_1 = np.copy(self.ez_front_1) - self.hx_front_0 = np.copy(self.ez_front_1) - self.hz_front_1 = np.copy(self.ex_front_1) - self.hz_front_0 = np.copy(self.ex_front_1) - - self.hx_back_1 = np.copy(self.hx_front_1) - self.hx_back_0 = np.copy(self.hx_front_1) - self.hz_back_1 = np.copy(self.hz_front_1) - self.hz_back_0 = np.copy(self.hz_front_1) - - self.hy_left_1 = np.copy(self.ez_left_1) - self.hy_left_0 = np.copy(self.ez_left_1) - self.hz_left_1 = np.copy(self.ey_left_1) - self.hz_left_0 = np.copy(self.ey_left_1) - - self.hy_right_1 = np.copy(self.hy_left_1) - self.hy_right_0 = np.copy(self.hy_left_1) - self.hz_right_1 = np.copy(self.hz_left_1) - self.hz_right_0 = np.copy(self.hz_left_1) - - self.hx_top_1 = np.copy(self.ey_top_1) - self.hx_top_0 = np.copy(self.ey_top_1) - self.hy_top_1 = np.copy(self.ex_top_1) - self.hy_top_0 = np.copy(self.ex_top_1) - - self.hx_bottom_1 = np.copy(self.hx_top_1) - self.hx_bottom_0 = np.copy(self.hx_top_1) - self.hy_bottom_1 = np.copy(self.hy_top_1) - self.hy_bottom_0 = np.copy(self.hy_top_1) - - def _initialize_field_names(self): - - self.fn_m = [ - 'hx_front', 'hz_front', - 'hx_back', 'hz_back', - 'hy_left', 'hz_left', - 'hy_right', 'hz_right', - 'hx_top', 'hy_top', - 'hx_bottom', 'hy_bottom' - ] - - self.fn_e = [ - 'ex_front', 'ez_front', - 'ex_back', 'ez_back', - 'ey_left', 'ez_left', - 'ey_right', 'ez_right', - 'ex_top', 'ey_top', - 'ex_bottom', 'ey_bottom' - ] - - def update_previous_timestep_fields(self, field_names): - for fn in field_names: - val = getattr(self, fn + '_1') - val_c = np.copy(val) - setattr(self, fn + '_0', val_c) - - def initialize_magnetic_slices_array(self): - - # Array contains the indices at which the main grid should be sliced - # to obtain the 2 2d array of H nodes required for interpolation - # across the IS boundary for each h field on each face of the subgrid - - # Extend the surface so that the outer fields can be interpolated - # more accurately - #i0 = self.i0 - 1 - #j0 = self.j0 - 1 - #k0 = self.k0 - 1 - #i1 = self.i1 + 1 - #j1 = self.j1 + 1 - #k1 = self.k1 + 1 - - # not extended - i0 = self.i0 - j0 = self.j0 - k0 = self.k0 - i1 = self.i1 - j1 = self.j1 - k1 = self.k1 - - slices = [ - ['hy_left_1', False, - (self.i0 - 2, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i0 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i0 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], - ['hy_right_1', False, - (self.i1 - 2, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i1 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i1 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], - ['hz_left_1', True, - (self.i0 - 2, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i0 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i0 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], - ['hz_right_1', True, - (self.i1 - 2, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i1 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i1 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], - ['hx_front_1', False, - (slice(i0, i1 + 1, 1), self.j0 - 2, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j0 - 1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j0 + 1, slice(k0, k1, 1)), self.Hx], - ['hx_back_1', False, - (slice(i0, i1 + 1, 1), self.j1 - 2, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j1 - 1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j1 + 1, slice(k0, k1, 1)), self.Hx], - ['hz_front_1', True, - (slice(i0, i1, 1), self.j0 - 2, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j0 - 1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j0 + 1, slice(k0, k1 + 1, 1)), self.Hz], - ['hz_back_1', True, - (slice(i0, i1, 1), self.j1 - 2, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j1 - 1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j1 + 1, slice(k0, k1 + 1, 1)), self.Hz], - ['hx_bottom_1', False, - # check these indexes - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 2), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 + 1), self.Hx], - ['hx_top_1', False, - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 2), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 + 1), self.Hx], - ['hy_bottom_1', True, - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 2), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 + 1), self.Hy], - ['hy_top_1', True, - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 2), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 + 1), self.Hy] - ] - - for obj in slices: - sliced_field = obj[-1][obj[2]] - obj[1] = self.create_interpolated_coords(obj[1], sliced_field) - - self.magnetic_slices = slices - - def initialize_electric_slices_array(self): - # Extend the region sliced from the main grid by 1 cell. - # this allows more accurate interpolation for the outernodes - #i0 = self.i0 - 1 - #j0 = self.j0 - 1 - #k0 = self.k0 - 1 - #i1 = self.i1 + 1 - #j1 = self.j1 + 1 - #k1 = self.k1 + 1 - - # not extended - i0 = self.i0 - j0 = self.j0 - k0 = self.k0 - i1 = self.i1 - j1 = self.j1 - k1 = self.k1 - - # Spatially interpolate nodes - slices = [ - ['ex_front_1', True, - (slice(i0, i1, 1), self.j0 - 1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j0 + 1, slice(k0, k1 + 1, 1)), - self.Ex], - ['ex_back_1', True, - (slice(i0, i1, 1), self.j1 - 1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), - (slice(i0, i1, 1), self.j1 + 1, slice(k0, k1 + 1, 1)), - self.Ex], - ['ez_front_1', False, - (slice(i0, i1 + 1, 1), self.j0 - 1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j0 + 1, slice(k0, k1, 1)), - self.Ez], - ['ez_back_1', False, - (slice(i0, i1 + 1, 1), self.j1 - 1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), - (slice(i0, i1 + 1, 1), self.j1 + 1, slice(k0, k1, 1)), - self.Ez], - ['ey_left_1', True, - (self.i0 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i0 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - self.Ey], - ['ey_right_1', True, - (self.i1 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - (self.i1 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), - self.Ey], - ['ez_left_1', False, - (self.i0 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i0 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - self.Ez], - ['ez_right_1', False, - (self.i1 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - (self.i1 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), - self.Ez], - - ['ex_bottom_1', True, - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 + 1), - self.Ex], - ['ex_top_1', True, - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), - (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 + 1), - self.Ex], - ['ey_bottom_1', False, - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 + 1), - self.Ey], - ['ey_top_1', False, - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), - (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 + 1), - self.Ey] - ] - - for obj in slices: - sliced_field = obj[-1][obj[2]] - obj[1] = self.create_interpolated_coords(obj[1], sliced_field) - - self.electric_slices = slices - - def create_interpolated_coords(self, mid, field): - - n_x = field.shape[0] - n_y = field.shape[1] - - if mid: - x = np.arange(0.5, n_x, 1.0) - z = np.arange(0, n_y, 1.0) - - # Coordinates that require interpolated values - x_sg = np.linspace(self.d, n_x - self.d, n_x * self.ratio) - z_sg = np.linspace(0, n_y - 1, (n_y - 1) * self.ratio + 1) - - else: - x = np.arange(0, n_x, 1.0) - z = np.arange(0.5, n_y, 1.0) - - # Coordinates that require interpolated values - x_sg = np.linspace(0, n_x - 1, (n_x - 1) * self.ratio + 1) - z_sg = np.linspace(self.d, n_y - self.d, n_y * self.ratio) - - return (x, z, x_sg, z_sg) - - def update_magnetic(self): - - # Copy previous time step magnetic field values to the previous - # time step variables - self.update_previous_timestep_fields(self.fn_m) - - for obj in self.magnetic_slices: - - # Grab the main grid fields used to interpolate across the IS - # f = self.Hi[slice] - - f_u_1 = obj[-1][obj[2]] - f_u_2 = obj[-1][obj[3]] - f_u_3 = obj[-1][obj[4]] - f_u_4 = obj[-1][obj[5]] - - f_1 = 0.25 * f_u_1 + 0.5 * f_u_2 + 0.25 * f_u_3 - f_2 = 0.25 * f_u_2 + 0.5 * f_u_3 + 0.25 * f_u_4 - - #f_1 = obj[-1][obj[2]] - #f_2 = obj[-1][obj[3]] - if ('left' in obj[0] or - 'bottom' in obj[0] or - 'front' in obj[0]): - w = self.l_weight - else: - w = self.r_weight - c1, c2 = calculate_weighting_coefficients(w, self.ratio) - # transverse interpolated h field - f_t = c1 * f_1 + c2 * f_2 - - # interpolate over a fine grid - f_i = self.interpolate_to_sub_grid(f_t, obj[1]) - - if f_i == f_t: - raise ValueError - - if obj[0] == 'hz_front_1' or obj[0] == 'hz_back_1': - - pre_0 = int(f_i.shape[0] / 2 - 1 - self.ratio // 2) - pre_1 = int(f_i.shape[0] / 2 + self.ratio // 2) - - f_i[pre_0 + 1:pre_0 + 1 + self.ratio // 2, :] = np.stack((f_i[pre_0, :], f_i[pre_0, :])) - f_i[pre_1 - self.ratio // 2:pre_1, :] = np.stack((f_i[pre_1, :], f_i[pre_1, :])) - - - # discard the outer nodes only required for interpolation - #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] - f = f_i - setattr(self, obj[0], f) - - def update_electric(self): - - self.update_previous_timestep_fields(self.fn_e) - - for obj in self.electric_slices: - - f_u_1 = obj[-1][obj[2]] - f_u_2 = obj[-1][obj[3]] - f_u_3 = obj[-1][obj[4]] - - f_m = 0.25 * f_u_1 + 0.5 * f_u_2 + 0.25 * f_u_3 - - f_i = self.interpolate_to_sub_grid(f_m, obj[1]) - - # correction interpolated points at the pec traversal point - if obj[0] == 'ex_front_1' or obj[0] == 'ex_back_1': - - pre_0 = int(f_i.shape[0] / 2 - 1 - self.ratio // 2) - pre_1 = int(f_i.shape[0] / 2 + self.ratio // 2) - - f_i[pre_0 + 1:pre_0 + 1 + self.ratio // 2, :] = np.stack((f_i[pre_0, :], f_i[pre_0, :])) - f_i[pre_1 - self.ratio // 2:pre_1, :] = np.stack((f_i[pre_1, :], f_i[pre_1, :])) - - f = f_i - #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] - setattr(self, obj[0], f) - - def interpolate_to_sub_grid(self, field, coords): - x, z, x_sg, z_sg = coords - ex_t = np.transpose(field) - f = interpolate.interp2d(x, z, ex_t, kind=self.interpolation) - #f = interpolate.RectBivariateSpline(x, z, field) - ex_inter_t = f(x_sg, z_sg) - ex_inter = np.transpose(ex_inter_t) - #ex_inter = ex_inter_t - - return ex_inter - - -class PlaneError(Exception): - pass +import numpy as np +from scipy import interpolate +import sys + + +def calculate_weighting_coefficients(x1, x): + c1 = (x - x1) / x + c2 = x1 / x + return (c1, c2) + + +class PrecusorNodes2dBase(object): + + def __init__(self, fdtd_grid, sub_grid): + self.G = fdtd_grid + self.ratio = sub_grid.ratio + self.nwx = sub_grid.nwx + self.nwy = sub_grid.nwy + self.sub_grid = sub_grid + self.interpolation = sub_grid.interpolation + + self.Hx = fdtd_grid.Hx + self.Hy = fdtd_grid.Hy + self.Hz = fdtd_grid.Hz + self.Ex = fdtd_grid.Ex + self.Ey = fdtd_grid.Ey + self.Ez = fdtd_grid.Ez + + # Main grid indices of subgrids + self.i0 = sub_grid.i0 + self.j0 = sub_grid.j0 + self.k0 = sub_grid.k0 + self.i1 = sub_grid.i1 + self.j1 = sub_grid.j1 + self.k1 = sub_grid.k1 + + # dl / 2 sub cell + self.d = 1 / (2 * self.ratio) + + self._initialize_fields() + self._initialize_field_names() + + self.l_weight = self.ratio // 2 + self.r_weight = self.ratio - self.l_weight + #self.l_weight = 1 + #self.r_weight = 2 + + def _initialize_fields(self): + print('dont call me') + sys.exit() + pass + + def _initialize_field_names(self): + print('dont call me') + sys.exit() + pass + + def interpolate_magnetic_in_time(self, m): + self.weight_pre_and_current_fields(m, self.fn_m) + + def interpolate_electric_in_time(self, m): + self.weight_pre_and_current_fields(m, self.fn_e) + + def weight_pre_and_current_fields(self, m, field_names): + c1, c2 = calculate_weighting_coefficients(m, self.ratio) + + for f in field_names: + try: + val = c1 * getattr(self, f + '_0') + c2 * getattr(self, f + '_1') + except ValueError: + print(self.ex_front_0.shape) + print(self.ex_front_1.shape) + raise Exception(f) + setattr(self, f, val) + + def calc_exact_field(self, field_names): + """ + Function to set the fields used in update calculations to the + values at the current main time step. + i.e. ey_left = copy.ey_left_1 + """ + for f in field_names: + val = np.copy(getattr(self, f + '_1')) + setattr(self, f, val) + + def calc_exact_magnetic_in_time(self): + self.calc_exact_field(self.fn_m) + + def calc_exact_electric_in_time(self): + self.calc_exact_field(self.fn_e) + + +class PrecursorNodes2dTM(PrecusorNodes2dBase): + + def __init__(self, fdtd_grid, sub_grid): + super().__init__(fdtd_grid, sub_grid) + + def _initialize_fields(self): + + # H precursors + self.hx_front_0 = np.zeros(self.nwx + 1) + self.hx_front_1 = np.zeros(self.nwx + 1) + + self.hx_back_0 = np.zeros(self.nwx + 1) + self.hx_back_1 = np.zeros(self.nwx + 1) + + self.hy_left_0 = np.zeros(self.nwy + 1) + self.hy_left_1 = np.zeros(self.nwy + 1) + + self.hy_right_0 = np.zeros(self.nwy + 1) + self.hy_right_1 = np.zeros(self.nwy + 1) + + # E precursors + self.ez_front_0 = np.zeros(self.nwx + 1) + self.ez_front_1 = np.zeros(self.nwx + 1) + + self.ez_back_0 = np.zeros(self.nwx + 1) + self.ez_back_1 = np.zeros(self.nwx + 1) + + self.ez_left_0 = np.zeros(self.nwy + 1) + self.ez_left_1 = np.zeros(self.nwy + 1) + + self.ez_right_0 = np.zeros(self.nwy + 1) + self.ez_right_1 = np.zeros(self.nwy + 1) + + def _initialize_field_names(self): + self.fn_m = ['hy_left', 'hy_right', 'hx_back', 'hx_front'] + self.fn_e = ['ez_left', 'ez_right', 'ez_back', 'ez_front'] + + def interpolate_across_sub_cells(self, y, n0, n1): + n = n1 - n0 + x = np.arange(0, y.shape[0], 1.0) + x_new = np.linspace(1, y.shape[0] - 2, n * self.ratio + 1) + f = interpolate.interp1d(x, y, kind='linear') + y_new = f(x_new) + """plt.plot(x, y, 'x', x_new, y_new, '.') + plt.show() + sys.exit()""" + return y_new + + def update_electric(self): + # f = np.sin(np.arange(0, 13)) + + # line of Ez nodes along the left edge of the IS + self.ez_left_0 = np.copy(self.ez_left_1) + # interpolate nodes between two Ez nodes 1 cell beyond/infront the IS + f = self.Ez[self.i0, self.j0 - 1:self.j1 + 2, 0] + self.ez_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + self.ez_right_0 = np.copy(self.ez_right_1) + f = self.Ez[self.i1, self.j0 - 1:self.j1 + 2, 0] + self.ez_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + self.ez_front_0 = np.copy(self.ez_front_1) + f = self.Ez[self.i0 - 1:self.i1 + 2, self.j0, 0] + self.ez_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + self.ez_back_0 = np.copy(self.ez_back_1) + f = self.Ez[self.i0 - 1:self.i1 + 2, self.j1, 0] + self.ez_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + def update_hy_left_1(self): + + self.hy_left_0 = np.copy(self.hy_left_1) + c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) + l = self.Hy[self.i0 - 1, self.j0 - 1:self.j1 + 2, 0] + r = self.Hy[self.i0, self.j0 - 1:self.j1 + 2, 0] + + # Tranverse interpolated hz + hz_t_i = c1 * l + c2 * r + # Tangential interpolated hz + self.hy_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hy_right_1(self): + + self.hy_right_0 = np.copy(self.hy_right_1) + c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) + l = self.Hy[self.i1 - 1, self.j0 - 1:self.j1 + 2, 0] + r = self.Hy[self.i1, self.j0 - 1:self.j1 + 2, 0] + hz_t_i = c1 * l + c2 * r + self.hy_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hx_back_1(self): + self.hx_back_0 = np.copy(self.hx_back_1) + c1, c2 = calculate_weighting_coefficients(self.r_weight, self.ratio) + l = self.Hx[self.i0 - 1:self.i1 + 2, self.j1 - 1, 0] + r = self.Hx[self.i0 - 1:self.i1 + 2, self.j1, 0] + hz_t_i = c1 * l + c2 * r + self.hx_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_hx_front_1(self): + + self.hx_front_0 = np.copy(self.hx_front_1) + c1, c2 = calculate_weighting_coefficients(self.l_weight, self.ratio) + l = self.Hx[self.i0 - 1:self.i1 + 2, self.j0 - 1, 0] + r = self.Hx[self.i0 - 1:self.i1 + 2, self.j0, 0] + hz_t_i = c1 * l + c2 * r + self.hx_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_magnetic(self): + self.update_hy_left_1() + self.update_hy_right_1() + self.update_hx_back_1() + self.update_hx_front_1() + + +class PrecursorNodes2d(PrecusorNodes2dBase): + + def __init__(self, fdtd_grid, sub_grid): + super().__init__(fdtd_grid, sub_grid) + + def _initialize_fields(self): + # E precursors + self.ex_front_0 = np.zeros(self.nwx) + self.ex_front_1 = np.zeros(self.nwx) + + self.ex_back_0 = np.zeros(self.nwx) + self.ex_back_1 = np.zeros(self.nwx) + + self.ey_left_0 = np.zeros(self.nwy) + self.ey_left_1 = np.zeros(self.nwy) + + self.ey_right_0 = np.zeros(self.nwy) + self.ey_right_1 = np.zeros(self.nwy) + + # H precursors + self.hz_front_0 = np.zeros(self.nwx) + self.hz_front_1 = np.zeros(self.nwx) + + self.hz_back_0 = np.zeros(self.nwx) + self.hz_back_1 = np.zeros(self.nwx) + + self.hz_left_0 = np.zeros(self.nwy) + self.hz_left_1 = np.zeros(self.nwy) + + self.hz_right_0 = np.zeros(self.nwy) + self.hz_right_1 = np.zeros(self.nwy) + + def _initialize_field_names(self): + # Field names + self.fn_m = ['hz_left', 'hz_right', 'hz_back', 'hz_front'] + self.fn_e = ['ey_left', 'ey_right', 'ex_back', 'ex_front'] + + def update_hz_left_1(self): + self.hz_left_0 = np.copy(self.hz_left_1) + c1, c2 = calculate_weighting_coefficients(1, self.ratio) + l = self.Hz[self.i0 - 1, self.j0 - 1:self.j1 + 1, 1] + r = self.Hz[self.i0, self.j0 - 1:self.j1 + 1, 1] + + # Tranverse interpolated hz + hz_t_i = c1 * l + c2 * r + # Tangential interpolated hz + self.hz_left_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hz_right_1(self): + + self.hz_right_0 = np.copy(self.hz_right_1) + c1, c2 = calculate_weighting_coefficients(2, self.ratio) + l = self.Hz[self.i1 - 1, self.j0 - 1:self.j1 + 1, 1] + r = self.Hz[self.i1, self.j0 - 1:self.j1 + 1, 1] + hz_t_i = c1 * l + c2 * r + self.hz_right_1 = self.interpolate_across_sub_cells(hz_t_i, self.j0, self.j1) + + def update_hz_back_1(self): + self.hz_back_0 = np.copy(self.hz_back_1) + c1, c2 = calculate_weighting_coefficients(2, self.ratio) + l = self.Hz[self.i0 - 1:self.i1 + 1, self.j1 - 1, 1] + r = self.Hz[self.i0 - 1:self.i1 + 1, self.j1, 1] + hz_t_i = c1 * l + c2 * r + self.hz_back_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_hz_front_1(self): + self.hz_front_0 = np.copy(self.hz_front_1) + c1, c2 = calculate_weighting_coefficients(1, self.ratio) + l = self.Hz[self.i0 - 1:self.i1 + 1, self.j0 - 1, 1] + r = self.Hz[self.i0 - 1:self.i1 + 1, self.j0, 1] + hz_t_i = c1 * l + c2 * r + self.hz_front_1 = self.interpolate_across_sub_cells(hz_t_i, self.i0, self.i1) + + def update_magnetic(self): + self.update_hz_left_1() + self.update_hz_right_1() + self.update_hz_back_1() + self.update_hz_front_1() + + def update_electric(self): + + """Function to calculate the precursor nodes at the next main + grid time-step + """ + + # LEFT BOUNDARY + # Save the last precursor node values - these will be used interpolate + # field values at sub grid time step values. + self.ey_left_0 = np.copy(self.ey_left_1) + # line of Ex nodes along the left edge of the IS + f = self.Ey[self.i0, self.j0 - 1:self.j1 + 1, 1] + self.ey_left_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + # RIGHT BOUNDARY + self.ey_right_0 = np.copy(self.ey_right_1) + f = self.Ey[self.i1, self.j0 - 1:self.j1 + 1, 1] + self.ey_right_1 = self.interpolate_across_sub_cells(f, self.j0, self.j1) + + # BACK BOUNDARY + self.ex_back_0 = np.copy(self.ex_back_1) + f = self.Ex[self.i0 - 1:self.i1 + 1, self.j1, 1] + self.ex_back_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + # FRONT BOUNDARY + self.ex_front_0 = np.copy(self.ex_front_1) + f = self.Ex[self.i0 - 1:self.i1 + 1, self.j0, 1] + self.ex_front_1 = self.interpolate_across_sub_cells(f, self.i0, self.i1) + + def interpolate_across_sub_cells(self, y, n0, n1): + n = n1 - n0 + x = np.arange(0.5, y.shape[0], 1.0) + x_new = np.linspace(1 + self.d, (1 + n) - self.d, n * self.ratio) + f = interpolate.interp1d(x, y, kind='linear') + y_new = f(x_new) + + return y_new + + +class PrecursorNodes(PrecusorNodes2dBase): + + def __init__(self, fdtd_grid, sub_grid): + + self.nwz = sub_grid.nwz + super().__init__(fdtd_grid, sub_grid) + self.initialize_magnetic_slices_array() + self.initialize_electric_slices_array() + + def build_coefficient_matrix(self, update_coefficients, lookup_id, inc_field, name): + """Function builds a 2d matrix of update coefficients for each face and field. + This allows the is nodes to be updated via slicing rather than iterating which is much faster + """ + pass + + def _initialize_fields(self): + + # Initialise the precursor arrays + + # The precursors are divided up into the 6. Each represent 1 + # face of a huygens cube surface. We represent each face as a 2d array + # containing a field in a particular direction. + + # _1 are the fields at the current main grid timestep + # _0 are the fields at the previous main grid timestep + # We store both fields so we can do different interpolations between + # them on the fly. + + # Front face + self.ex_front_1 = np.zeros((self.nwx, self.nwz + 1)) + self.ex_front_0 = np.copy(self.ex_front_1) + self.ez_front_1 = np.zeros((self.nwx + 1, self.nwz)) + self.ez_front_0 = np.copy(self.ez_front_1) + + # The same as the opposite face + self.ex_back_1 = np.copy(self.ex_front_1) + self.ex_back_0 = np.copy(self.ex_front_1) + self.ez_back_1 = np.copy(self.ez_front_1) + self.ez_back_0 = np.copy(self.ez_front_1) + + self.ey_left_1 = np.zeros((self.nwy, self.nwz + 1)) + self.ey_left_0 = np.copy(self.ey_left_1) + self.ez_left_1 = np.zeros((self.nwy + 1, self.nwz)) + self.ez_left_0 = np.copy(self.ez_left_1) + + self.ey_right_1 = np.copy(self.ey_left_1) + self.ey_right_0 = np.copy(self.ey_left_1) + self.ez_right_1 = np.copy(self.ez_left_1) + self.ez_right_0 = np.copy(self.ez_left_1) + + self.ex_bottom_1 = np.zeros((self.nwx, self.nwy + 1)) + self.ex_bottom_0 = np.copy(self.ex_bottom_1) + self.ey_bottom_1 = np.zeros((self.nwx + 1, self.nwy)) + self.ey_bottom_0 = np.copy(self.ey_bottom_1) + + self.ex_top_1 = np.copy(self.ex_bottom_1) + self.ex_top_0 = np.copy(self.ex_bottom_1) + self.ey_top_1 = np.copy(self.ey_bottom_1) + self.ey_top_0 = np.copy(self.ey_bottom_1) + + # Initialize the H precursor fields + self.hx_front_1 = np.copy(self.ez_front_1) + self.hx_front_0 = np.copy(self.ez_front_1) + self.hz_front_1 = np.copy(self.ex_front_1) + self.hz_front_0 = np.copy(self.ex_front_1) + + self.hx_back_1 = np.copy(self.hx_front_1) + self.hx_back_0 = np.copy(self.hx_front_1) + self.hz_back_1 = np.copy(self.hz_front_1) + self.hz_back_0 = np.copy(self.hz_front_1) + + self.hy_left_1 = np.copy(self.ez_left_1) + self.hy_left_0 = np.copy(self.ez_left_1) + self.hz_left_1 = np.copy(self.ey_left_1) + self.hz_left_0 = np.copy(self.ey_left_1) + + self.hy_right_1 = np.copy(self.hy_left_1) + self.hy_right_0 = np.copy(self.hy_left_1) + self.hz_right_1 = np.copy(self.hz_left_1) + self.hz_right_0 = np.copy(self.hz_left_1) + + self.hx_top_1 = np.copy(self.ey_top_1) + self.hx_top_0 = np.copy(self.ey_top_1) + self.hy_top_1 = np.copy(self.ex_top_1) + self.hy_top_0 = np.copy(self.ex_top_1) + + self.hx_bottom_1 = np.copy(self.hx_top_1) + self.hx_bottom_0 = np.copy(self.hx_top_1) + self.hy_bottom_1 = np.copy(self.hy_top_1) + self.hy_bottom_0 = np.copy(self.hy_top_1) + + def _initialize_field_names(self): + + self.fn_m = [ + 'hx_front', 'hz_front', + 'hx_back', 'hz_back', + 'hy_left', 'hz_left', + 'hy_right', 'hz_right', + 'hx_top', 'hy_top', + 'hx_bottom', 'hy_bottom' + ] + + self.fn_e = [ + 'ex_front', 'ez_front', + 'ex_back', 'ez_back', + 'ey_left', 'ez_left', + 'ey_right', 'ez_right', + 'ex_top', 'ey_top', + 'ex_bottom', 'ey_bottom' + ] + + def update_previous_timestep_fields(self, field_names): + for fn in field_names: + val = getattr(self, fn + '_1') + val_c = np.copy(val) + setattr(self, fn + '_0', val_c) + + def initialize_magnetic_slices_array(self): + + # Array contains the indices at which the main grid should be sliced + # to obtain the 2 2d array of H nodes required for interpolation + # across the IS boundary for each h field on each face of the subgrid + + # Extend the surface so that the outer fields can be interpolated + # more accurately + #i0 = self.i0 - 1 + #j0 = self.j0 - 1 + #k0 = self.k0 - 1 + #i1 = self.i1 + 1 + #j1 = self.j1 + 1 + #k1 = self.k1 + 1 + + # not extended + i0 = self.i0 + j0 = self.j0 + k0 = self.k0 + i1 = self.i1 + j1 = self.j1 + k1 = self.k1 + + slices = [ + ['hy_left_1', False, + (self.i0 - 2, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i0 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i0 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], + ['hy_right_1', False, + (self.i1 - 2, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i1 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i1 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), self.Hy], + ['hz_left_1', True, + (self.i0 - 2, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i0 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i0 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], + ['hz_right_1', True, + (self.i1 - 2, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i1 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i1 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), self.Hz], + ['hx_front_1', False, + (slice(i0, i1 + 1, 1), self.j0 - 2, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j0 - 1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j0 + 1, slice(k0, k1, 1)), self.Hx], + ['hx_back_1', False, + (slice(i0, i1 + 1, 1), self.j1 - 2, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j1 - 1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j1 + 1, slice(k0, k1, 1)), self.Hx], + ['hz_front_1', True, + (slice(i0, i1, 1), self.j0 - 2, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j0 - 1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j0 + 1, slice(k0, k1 + 1, 1)), self.Hz], + ['hz_back_1', True, + (slice(i0, i1, 1), self.j1 - 2, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j1 - 1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j1 + 1, slice(k0, k1 + 1, 1)), self.Hz], + ['hx_bottom_1', False, + # check these indexes + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 2), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 + 1), self.Hx], + ['hx_top_1', False, + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 2), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 + 1), self.Hx], + ['hy_bottom_1', True, + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 2), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 + 1), self.Hy], + ['hy_top_1', True, + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 2), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 + 1), self.Hy] + ] + + for obj in slices: + sliced_field = obj[-1][obj[2]] + obj[1] = self.create_interpolated_coords(obj[1], sliced_field) + + self.magnetic_slices = slices + + def initialize_electric_slices_array(self): + # Extend the region sliced from the main grid by 1 cell. + # this allows more accurate interpolation for the outernodes + #i0 = self.i0 - 1 + #j0 = self.j0 - 1 + #k0 = self.k0 - 1 + #i1 = self.i1 + 1 + #j1 = self.j1 + 1 + #k1 = self.k1 + 1 + + # not extended + i0 = self.i0 + j0 = self.j0 + k0 = self.k0 + i1 = self.i1 + j1 = self.j1 + k1 = self.k1 + + # Spatially interpolate nodes + slices = [ + ['ex_front_1', True, + (slice(i0, i1, 1), self.j0 - 1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j0, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j0 + 1, slice(k0, k1 + 1, 1)), + self.Ex], + ['ex_back_1', True, + (slice(i0, i1, 1), self.j1 - 1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j1, slice(k0, k1 + 1, 1)), + (slice(i0, i1, 1), self.j1 + 1, slice(k0, k1 + 1, 1)), + self.Ex], + ['ez_front_1', False, + (slice(i0, i1 + 1, 1), self.j0 - 1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j0, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j0 + 1, slice(k0, k1, 1)), + self.Ez], + ['ez_back_1', False, + (slice(i0, i1 + 1, 1), self.j1 - 1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j1, slice(k0, k1, 1)), + (slice(i0, i1 + 1, 1), self.j1 + 1, slice(k0, k1, 1)), + self.Ez], + ['ey_left_1', True, + (self.i0 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i0, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i0 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + self.Ey], + ['ey_right_1', True, + (self.i1 - 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + (self.i1 + 1, slice(j0, j1, 1), slice(k0, k1 + 1, 1)), + self.Ey], + ['ez_left_1', False, + (self.i0 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i0, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i0 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + self.Ez], + ['ez_right_1', False, + (self.i1 - 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + (self.i1 + 1, slice(j0, j1 + 1, 1), slice(k0, k1, 1)), + self.Ez], + + ['ex_bottom_1', True, + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 - 1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k0 + 1), + self.Ex], + ['ex_top_1', True, + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 - 1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1), + (slice(i0, i1, 1), slice(j0, j1 + 1, 1), self.k1 + 1), + self.Ex], + ['ey_bottom_1', False, + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 - 1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k0 + 1), + self.Ey], + ['ey_top_1', False, + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 - 1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1), + (slice(i0, i1 + 1, 1), slice(j0, j1, 1), self.k1 + 1), + self.Ey] + ] + + for obj in slices: + sliced_field = obj[-1][obj[2]] + obj[1] = self.create_interpolated_coords(obj[1], sliced_field) + + self.electric_slices = slices + + def create_interpolated_coords(self, mid, field): + + n_x = field.shape[0] + n_y = field.shape[1] + + if mid: + x = np.arange(0.5, n_x, 1.0) + z = np.arange(0, n_y, 1.0) + + # Coordinates that require interpolated values + x_sg = np.linspace(self.d, n_x - self.d, n_x * self.ratio) + z_sg = np.linspace(0, n_y - 1, (n_y - 1) * self.ratio + 1) + + else: + x = np.arange(0, n_x, 1.0) + z = np.arange(0.5, n_y, 1.0) + + # Coordinates that require interpolated values + x_sg = np.linspace(0, n_x - 1, (n_x - 1) * self.ratio + 1) + z_sg = np.linspace(self.d, n_y - self.d, n_y * self.ratio) + + return (x, z, x_sg, z_sg) + + def update_magnetic(self): + + # Copy previous time step magnetic field values to the previous + # time step variables + self.update_previous_timestep_fields(self.fn_m) + + for obj in self.magnetic_slices: + + # Grab the main grid fields used to interpolate across the IS + # f = self.Hi[slice] + + f_u_1 = obj[-1][obj[2]] + f_u_2 = obj[-1][obj[3]] + f_u_3 = obj[-1][obj[4]] + f_u_4 = obj[-1][obj[5]] + + f_1 = 0.25 * f_u_1 + 0.5 * f_u_2 + 0.25 * f_u_3 + f_2 = 0.25 * f_u_2 + 0.5 * f_u_3 + 0.25 * f_u_4 + + #f_1 = obj[-1][obj[2]] + #f_2 = obj[-1][obj[3]] + if ('left' in obj[0] or + 'bottom' in obj[0] or + 'front' in obj[0]): + w = self.l_weight + else: + w = self.r_weight + c1, c2 = calculate_weighting_coefficients(w, self.ratio) + # transverse interpolated h field + f_t = c1 * f_1 + c2 * f_2 + + # interpolate over a fine grid + f_i = self.interpolate_to_sub_grid(f_t, obj[1]) + + if f_i == f_t: + raise ValueError + + if obj[0] == 'hz_front_1' or obj[0] == 'hz_back_1': + + pre_0 = int(f_i.shape[0] / 2 - 1 - self.ratio // 2) + pre_1 = int(f_i.shape[0] / 2 + self.ratio // 2) + + f_i[pre_0 + 1:pre_0 + 1 + self.ratio // 2, :] = np.stack((f_i[pre_0, :], f_i[pre_0, :])) + f_i[pre_1 - self.ratio // 2:pre_1, :] = np.stack((f_i[pre_1, :], f_i[pre_1, :])) + + + # discard the outer nodes only required for interpolation + #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] + f = f_i + setattr(self, obj[0], f) + + def update_electric(self): + + self.update_previous_timestep_fields(self.fn_e) + + for obj in self.electric_slices: + + f_u_1 = obj[-1][obj[2]] + f_u_2 = obj[-1][obj[3]] + f_u_3 = obj[-1][obj[4]] + + f_m = 0.25 * f_u_1 + 0.5 * f_u_2 + 0.25 * f_u_3 + + f_i = self.interpolate_to_sub_grid(f_m, obj[1]) + + # correction interpolated points at the pec traversal point + if obj[0] == 'ex_front_1' or obj[0] == 'ex_back_1': + + pre_0 = int(f_i.shape[0] / 2 - 1 - self.ratio // 2) + pre_1 = int(f_i.shape[0] / 2 + self.ratio // 2) + + f_i[pre_0 + 1:pre_0 + 1 + self.ratio // 2, :] = np.stack((f_i[pre_0, :], f_i[pre_0, :])) + f_i[pre_1 - self.ratio // 2:pre_1, :] = np.stack((f_i[pre_1, :], f_i[pre_1, :])) + + f = f_i + #f = f_i[self.ratio:-self.ratio, self.ratio:-self.ratio] + setattr(self, obj[0], f) + + def interpolate_to_sub_grid(self, field, coords): + x, z, x_sg, z_sg = coords + ex_t = np.transpose(field) + f = interpolate.interp2d(x, z, ex_t, kind=self.interpolation) + #f = interpolate.RectBivariateSpline(x, z, field) + ex_inter_t = f(x_sg, z_sg) + ex_inter = np.transpose(ex_inter_t) + #ex_inter = ex_inter_t + + return ex_inter + + +class PlaneError(Exception): + pass diff --git a/gprMax/subgrids/subgrid_hsg.py b/gprMax/subgrids/subgrid_hsg.py index 8aa1d1a1..29be160d 100644 --- a/gprMax/subgrids/subgrid_hsg.py +++ b/gprMax/subgrids/subgrid_hsg.py @@ -1,148 +1,148 @@ -from .base import SubGridBase -from ..cython.fields_updates_hsg import cython_update_is -from ..cython.fields_updates_hsg import cython_update_magnetic_os -from ..cython.fields_updates_hsg import cython_update_electric_os -from ..utilities import human_size - -from colorama import init, Fore, Style -init() - - -class SubGridHSG(SubGridBase): - - gridtype = '3DSUBGRID' - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.gridtype = SubGridHSG.gridtype - - def update_magnetic_is(self, precursors): - """Update the subgrid nodes at the IS with the currents derived - from the main grid. - Args: nwl, nwm, nwn, face, field, inc_field, lookup_id, sign, mod, co - """ - # Hz = c0Hz - c1Ey + c2Ex - # Hy = c0Hy - c3Ex + c1Ez - # Hx = c0Hx - c2Ez + c3Ey - # bottom and top - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx, self.nwy + 1, self.nwz, 1, self.Hy, precursors.ex_bottom, precursors.ex_top, self.IDlookup['Hy'], 1, -1, 3, self.nthreads) - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx + 1, self.nwy, self.nwz, 1, self.Hx, precursors.ey_bottom, precursors.ey_top, self.IDlookup['Hx'], -1, 1, 3, self.nthreads) - - # left and right - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwy, self.nwz + 1, self.nwx, 2, self.Hz, precursors.ey_left, precursors.ey_right, self.IDlookup['Hz'], 1, -1, 1, self.nthreads) - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwy + 1, self.nwz, self.nwx, 2, self.Hy, precursors.ez_left, precursors.ez_right, self.IDlookup['Hy'], -1, 1, 1, self.nthreads) - - # front and back - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx, self.nwz + 1, self.nwy, 3, self.Hz, precursors.ex_front, precursors.ex_back, self.IDlookup['Hz'], -1, 1, 2, self.nthreads) - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx + 1, self.nwz, self.nwy, 3, self.Hx, precursors.ez_front, precursors.ez_back, self.IDlookup['Hx'], 1, -1, 2, self.nthreads) - - def update_electric_is(self, precursors): - # Args: nwl, nwm, nwn, face, field, inc_field, lookup_id, sign, mod, co - - # Ex = c0(Ex) + c2(dHz) - c3(dHy) - # Ey = c0(Ey) + c3(dHx) - c1(dHz) - # Ez = c0(Ez) + c1(dHy) - c2(dHx) - - # bottom and top - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx, self.nwy + 1, self.nwz, 1, self.Ex, precursors.hy_bottom, precursors.hy_top, self.IDlookup['Ex'], 1, -1, 3, self.nthreads) - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx + 1, self.nwy, self.nwz, 1, self.Ey, precursors.hx_bottom, precursors.hx_top, self.IDlookup['Ey'], -1, 1, 3, self.nthreads) - - # left and right - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwy, self.nwz + 1, self.nwx, 2, self.Ey, precursors.hz_left, precursors.hz_right, self.IDlookup['Ey'], 1, -1, 1, self.nthreads) - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwy + 1, self.nwz, self.nwx, 2, self.Ez, precursors.hy_left, precursors.hy_right, self.IDlookup['Ez'], -1, 1, 1, self.nthreads) - - # front and back - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx, self.nwz + 1, self.nwy, 3, self.Ex, precursors.hz_front, precursors.hz_back, self.IDlookup['Ex'], -1, 1, 2, self.nthreads) - cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx + 1, self.nwz, self.nwy, 3, self.Ez, precursors.hx_front, precursors.hx_back, self.IDlookup['Ez'], 1, -1, 2, self.nthreads) - - def update_electric_os(self, main_grid): - i_l = self.i0 - self.is_os_sep - i_u = self.i1 + self.is_os_sep - j_l = self.j0 - self.is_os_sep - j_u = self.j1 + self.is_os_sep - k_l = self.k0 - self.is_os_sep - k_u = self.k1 + self.is_os_sep - - # Args: sub_grid, normal, l_l, l_u, m_l, m_u, n_l, n_u, nwn, lookup_id, field, inc_field, co, sign_n, sign_f - - # Form of FDTD update equations for E - # Ex = c0(Ex) + c2(dHz) - c3(dHy) - # Ey = c0(Ey) + c3(dHx) - c1(dHz) - # Ez = c0(Ez) + c1(dHy) - c2(dHx) - - cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 3, i_l, i_u, k_l, k_u + 1, j_l, j_u, self.nwy, main_grid.IDlookup['Ex'], main_grid.Ex, self.Hz, 2, 1, -1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 3, i_l, i_u + 1, k_l, k_u, j_l, j_u, self.nwy, main_grid.IDlookup['Ez'], main_grid.Ez, self.Hx, 2, -1, 1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - - # Left and Right - cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 2, j_l, j_u, k_l, k_u + 1, i_l, i_u, self.nwx, main_grid.IDlookup['Ey'], main_grid.Ey, self.Hz, 1, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 2, j_l, j_u + 1, k_l, k_u, i_l, i_u, self.nwx, main_grid.IDlookup['Ez'], main_grid.Ez, self.Hy, 1, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - - # Bottom and Top - cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 1, i_l, i_u, j_l, j_u + 1, k_l, k_u, self.nwz, main_grid.IDlookup['Ex'], main_grid.Ex, self.Hy, 3, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 1, i_l, i_u + 1, j_l, j_u, k_l, k_u, self.nwz, main_grid.IDlookup['Ey'], main_grid.Ey, self.Hx, 3, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - - def update_magnetic_os(self, main_grid): - - i_l = self.i0 - self.is_os_sep - i_u = self.i1 + self.is_os_sep - j_l = self.j0 - self.is_os_sep - j_u = self.j1 + self.is_os_sep - k_l = self.k0 - self.is_os_sep - k_u = self.k1 + self.is_os_sep - - # Form of FDTD update equations for H - # Hz = c0Hz - c1Ey + c2Ex - # Hy = c0Hy - c3Ex + c1Ez - # Hx = c0Hx - c2Ez + c3Ey - - # Args: sub_grid, normal, l_l, l_u, m_l, m_u, n_l, n_u, nwn, lookup_id, field, inc_field, co, sign_n, sign_f): - - # Front and back - cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 3, i_l, i_u, k_l, k_u + 1, j_l - 1, j_u, self.nwy, main_grid.IDlookup['Hz'], main_grid.Hz, self.Ex, 2, 1, -1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 3, i_l, i_u + 1, k_l, k_u, j_l - 1, j_u, self.nwy, main_grid.IDlookup['Hx'], main_grid.Hx, self.Ez, 2, -1, 1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - - # Left and Right - cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 2, j_l, j_u, k_l, k_u + 1, i_l - 1, i_u, self.nwx, main_grid.IDlookup['Hz'], main_grid.Hz, self.Ey, 1, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 2, j_l, j_u + 1, k_l, k_u, i_l - 1, i_u, self.nwx, main_grid.IDlookup['Hy'], main_grid.Hy, self.Ez, 1, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - - # bottom and top - cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 1, i_l, i_u, j_l, j_u + 1, k_l - 1, k_u, self.nwz, main_grid.IDlookup['Hy'], main_grid.Hy, self.Ex, 3, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 1, i_l, i_u + 1, j_l, j_u, k_l - 1, k_u, self.nwz, main_grid.IDlookup['Hx'], main_grid.Hx, self.Ey, 3, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) - - def __str__(self): - - self.memory_estimate_basic() - - s = '\n' - s += Fore.CYAN - s += 'Sub Grid HSG\n' - s += 'Name: {}\n'.format(self.name) - s += 'dx, dy, dz: {}m {}m {}m\n'.format(self.dx, self.dy, self.dz) - s += 'dt: {}s\n'.format(self.dt) - s += 'Memory Estimate: {}\n'.format(human_size(self.memoryusage)) - s += 'Position: ({}m, {}m, {}m), ({}m, {}m, {}m)\n'.format(self.x1, - self.y1, - self.z1, - self.x2, - self.y2, - self.z2) - s += 'Main Grid Indices: lower left({}, {}, {}), upper right({}, {}, {})\n'.format(self.i0, self.j0, self.k0, self.i1, self.j1, self.k1) - s += 'Total Cells: {} {} {}\n'.format(self.nx, self.ny, self.nz) - s += 'Working Region Cells: {} {} {}\n'.format(self.nwx, - self.nwy, - self.nwz) - for h in self.hertziandipoles: - s += 'Hertizian dipole: {} {} {}\n'.format(h.xcoord, - h.ycoord, - h.zcoord) - s += str([x for x in self.waveforms - if x.ID == h.waveformID][0]) + '\n' - for r in self.rxs: - s += 'Receiver: {} {} {}\n'.format(r.xcoord, r.ycoord, r.zcoord) - - for tl in self.transmissionlines: - s += 'Transmission Line: {} {} {}\n'.format(tl.xcoord, tl.ycoord, tl.zcoord) - s += str([x for x in self.waveforms - if x.ID == tl.waveformID][0]) + '\n' - s += Style.RESET_ALL - return s +from .base import SubGridBase +from ..cython.fields_updates_hsg import cython_update_is +from ..cython.fields_updates_hsg import cython_update_magnetic_os +from ..cython.fields_updates_hsg import cython_update_electric_os +from ..utilities import human_size + +from colorama import init, Fore, Style +init() + + +class SubGridHSG(SubGridBase): + + gridtype = '3DSUBGRID' + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.gridtype = SubGridHSG.gridtype + + def update_magnetic_is(self, precursors): + """Update the subgrid nodes at the IS with the currents derived + from the main grid. + Args: nwl, nwm, nwn, face, field, inc_field, lookup_id, sign, mod, co + """ + # Hz = c0Hz - c1Ey + c2Ex + # Hy = c0Hy - c3Ex + c1Ez + # Hx = c0Hx - c2Ez + c3Ey + # bottom and top + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx, self.nwy + 1, self.nwz, 1, self.Hy, precursors.ex_bottom, precursors.ex_top, self.IDlookup['Hy'], 1, -1, 3, self.nthreads) + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx + 1, self.nwy, self.nwz, 1, self.Hx, precursors.ey_bottom, precursors.ey_top, self.IDlookup['Hx'], -1, 1, 3, self.nthreads) + + # left and right + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwy, self.nwz + 1, self.nwx, 2, self.Hz, precursors.ey_left, precursors.ey_right, self.IDlookup['Hz'], 1, -1, 1, self.nthreads) + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwy + 1, self.nwz, self.nwx, 2, self.Hy, precursors.ez_left, precursors.ez_right, self.IDlookup['Hy'], -1, 1, 1, self.nthreads) + + # front and back + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx, self.nwz + 1, self.nwy, 3, self.Hz, precursors.ex_front, precursors.ex_back, self.IDlookup['Hz'], -1, 1, 2, self.nthreads) + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsH, self.ID, self.n_boundary_cells, -1, self.nwx + 1, self.nwz, self.nwy, 3, self.Hx, precursors.ez_front, precursors.ez_back, self.IDlookup['Hx'], 1, -1, 2, self.nthreads) + + def update_electric_is(self, precursors): + # Args: nwl, nwm, nwn, face, field, inc_field, lookup_id, sign, mod, co + + # Ex = c0(Ex) + c2(dHz) - c3(dHy) + # Ey = c0(Ey) + c3(dHx) - c1(dHz) + # Ez = c0(Ez) + c1(dHy) - c2(dHx) + + # bottom and top + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx, self.nwy + 1, self.nwz, 1, self.Ex, precursors.hy_bottom, precursors.hy_top, self.IDlookup['Ex'], 1, -1, 3, self.nthreads) + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx + 1, self.nwy, self.nwz, 1, self.Ey, precursors.hx_bottom, precursors.hx_top, self.IDlookup['Ey'], -1, 1, 3, self.nthreads) + + # left and right + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwy, self.nwz + 1, self.nwx, 2, self.Ey, precursors.hz_left, precursors.hz_right, self.IDlookup['Ey'], 1, -1, 1, self.nthreads) + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwy + 1, self.nwz, self.nwx, 2, self.Ez, precursors.hy_left, precursors.hy_right, self.IDlookup['Ez'], -1, 1, 1, self.nthreads) + + # front and back + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx, self.nwz + 1, self.nwy, 3, self.Ex, precursors.hz_front, precursors.hz_back, self.IDlookup['Ex'], -1, 1, 2, self.nthreads) + cython_update_is(self.nwx, self.nwy, self.nwz, self.updatecoeffsE, self.ID, self.n_boundary_cells, 0, self.nwx + 1, self.nwz, self.nwy, 3, self.Ez, precursors.hx_front, precursors.hx_back, self.IDlookup['Ez'], 1, -1, 2, self.nthreads) + + def update_electric_os(self, main_grid): + i_l = self.i0 - self.is_os_sep + i_u = self.i1 + self.is_os_sep + j_l = self.j0 - self.is_os_sep + j_u = self.j1 + self.is_os_sep + k_l = self.k0 - self.is_os_sep + k_u = self.k1 + self.is_os_sep + + # Args: sub_grid, normal, l_l, l_u, m_l, m_u, n_l, n_u, nwn, lookup_id, field, inc_field, co, sign_n, sign_f + + # Form of FDTD update equations for E + # Ex = c0(Ex) + c2(dHz) - c3(dHy) + # Ey = c0(Ey) + c3(dHx) - c1(dHz) + # Ez = c0(Ez) + c1(dHy) - c2(dHx) + + cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 3, i_l, i_u, k_l, k_u + 1, j_l, j_u, self.nwy, main_grid.IDlookup['Ex'], main_grid.Ex, self.Hz, 2, 1, -1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 3, i_l, i_u + 1, k_l, k_u, j_l, j_u, self.nwy, main_grid.IDlookup['Ez'], main_grid.Ez, self.Hx, 2, -1, 1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + + # Left and Right + cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 2, j_l, j_u, k_l, k_u + 1, i_l, i_u, self.nwx, main_grid.IDlookup['Ey'], main_grid.Ey, self.Hz, 1, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 2, j_l, j_u + 1, k_l, k_u, i_l, i_u, self.nwx, main_grid.IDlookup['Ez'], main_grid.Ez, self.Hy, 1, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + + # Bottom and Top + cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 1, i_l, i_u, j_l, j_u + 1, k_l, k_u, self.nwz, main_grid.IDlookup['Ex'], main_grid.Ex, self.Hy, 3, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + cython_update_electric_os(main_grid.updatecoeffsE, main_grid.ID, 1, i_l, i_u + 1, j_l, j_u, k_l, k_u, self.nwz, main_grid.IDlookup['Ey'], main_grid.Ey, self.Hx, 3, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + + def update_magnetic_os(self, main_grid): + + i_l = self.i0 - self.is_os_sep + i_u = self.i1 + self.is_os_sep + j_l = self.j0 - self.is_os_sep + j_u = self.j1 + self.is_os_sep + k_l = self.k0 - self.is_os_sep + k_u = self.k1 + self.is_os_sep + + # Form of FDTD update equations for H + # Hz = c0Hz - c1Ey + c2Ex + # Hy = c0Hy - c3Ex + c1Ez + # Hx = c0Hx - c2Ez + c3Ey + + # Args: sub_grid, normal, l_l, l_u, m_l, m_u, n_l, n_u, nwn, lookup_id, field, inc_field, co, sign_n, sign_f): + + # Front and back + cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 3, i_l, i_u, k_l, k_u + 1, j_l - 1, j_u, self.nwy, main_grid.IDlookup['Hz'], main_grid.Hz, self.Ex, 2, 1, -1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 3, i_l, i_u + 1, k_l, k_u, j_l - 1, j_u, self.nwy, main_grid.IDlookup['Hx'], main_grid.Hx, self.Ez, 2, -1, 1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + + # Left and Right + cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 2, j_l, j_u, k_l, k_u + 1, i_l - 1, i_u, self.nwx, main_grid.IDlookup['Hz'], main_grid.Hz, self.Ey, 1, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 2, j_l, j_u + 1, k_l, k_u, i_l - 1, i_u, self.nwx, main_grid.IDlookup['Hy'], main_grid.Hy, self.Ez, 1, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + + # bottom and top + cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 1, i_l, i_u, j_l, j_u + 1, k_l - 1, k_u, self.nwz, main_grid.IDlookup['Hy'], main_grid.Hy, self.Ex, 3, -1, 1, 1, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + cython_update_magnetic_os(main_grid.updatecoeffsH, main_grid.ID, 1, i_l, i_u + 1, j_l, j_u, k_l - 1, k_u, self.nwz, main_grid.IDlookup['Hx'], main_grid.Hx, self.Ey, 3, 1, -1, 0, self.ratio, self.is_os_sep, self.n_boundary_cells, main_grid.nthreads) + + def __str__(self): + + self.memory_estimate_basic() + + s = '\n' + s += Fore.CYAN + s += 'Sub Grid HSG\n' + s += 'Name: {}\n'.format(self.name) + s += 'dx, dy, dz: {}m {}m {}m\n'.format(self.dx, self.dy, self.dz) + s += 'dt: {}s\n'.format(self.dt) + s += 'Memory Estimate: {}\n'.format(human_size(self.memoryusage)) + s += 'Position: ({}m, {}m, {}m), ({}m, {}m, {}m)\n'.format(self.x1, + self.y1, + self.z1, + self.x2, + self.y2, + self.z2) + s += 'Main Grid Indices: lower left({}, {}, {}), upper right({}, {}, {})\n'.format(self.i0, self.j0, self.k0, self.i1, self.j1, self.k1) + s += 'Total Cells: {} {} {}\n'.format(self.nx, self.ny, self.nz) + s += 'Working Region Cells: {} {} {}\n'.format(self.nwx, + self.nwy, + self.nwz) + for h in self.hertziandipoles: + s += 'Hertizian dipole: {} {} {}\n'.format(h.xcoord, + h.ycoord, + h.zcoord) + s += str([x for x in self.waveforms + if x.ID == h.waveformID][0]) + '\n' + for r in self.rxs: + s += 'Receiver: {} {} {}\n'.format(r.xcoord, r.ycoord, r.zcoord) + + for tl in self.transmissionlines: + s += 'Transmission Line: {} {} {}\n'.format(tl.xcoord, tl.ycoord, tl.zcoord) + s += str([x for x in self.waveforms + if x.ID == tl.waveformID][0]) + '\n' + s += Style.RESET_ALL + return s diff --git a/gprMax/subgrids/solver.py b/gprMax/subgrids/updates.py similarity index 51% rename from gprMax/subgrids/solver.py rename to gprMax/subgrids/updates.py index 41ee6138..128d876e 100644 --- a/gprMax/subgrids/solver.py +++ b/gprMax/subgrids/updates.py @@ -1,241 +1,167 @@ -from ..cython.fields_updates_normal import update_electric -from ..cython.fields_updates_normal import update_magnetic -from ..fields_outputs import store_outputs -from ..utilities import get_terminal_width -from ..exceptions import GeneralError - -from .subgrid_hsg import SubGridHSG -from .precursor_nodes import PrecursorNodes as PrecursorNodesHSG -from .precursor_nodes_filtered import PrecursorNodes as PrecursorNodesFilteredHSG - -from tqdm import tqdm -from time import perf_counter - -import os -import sys - -from ..updates import CPUUpdates - - - -def create_updates(G): - """Return the solver for the given subgrids.""" - updaters = [] - - for sg in G.subgrids: - print(sg) - sg_type = type(sg) - if sg_type == SubGridHSG and sg.filter: - precursors = PrecursorNodesFilteredHSG(G, sg) - elif sg_type == SubGridHSG and not sg.filter: - precursors = PrecursorNodesHSG(G, sg) - else: - raise GeneralError(str(sg) + ' is not a subgrid type') - - sgu = SubgridUpdater(sg, precursors, G) - updaters.append(sgu) - - updates = SubgridUpdates(G, updaters) - return updates - -class SubgridUpdates(CPUUpdates): - - def __init__(self, G, updaters): - super().__init__(G) - self.updaters = updaters - - def hsg_1(self): - """Method to update the subgrids over the first phase.""" - for sg_updater in self.updaters: - sg_updater.hsg_1() - - def hsg_2(self): - """Method to update the subgrids over the second phase.""" - for sg_updater in self.updaters: - sg_updater.hsg_2() - -class SubGridSolver: - """Solver for subgridding simulations.""" - - """Class to call the various update methods required for an HSG-Subgrid simulation. - Multiple subgrids can be updated by adding more subgrid_updater objects to the subgrid_updater - array. - """ - - def __init__(self, G, updates, hsg=True): - """ - Args: - G (G): Grid class instance - holds essential parameters - describing the model. - updates: (list): list of subgrid_updaters used for updating - the subgrids - hsg (bool): HSG methods for subgrids will not be called if False. - """ - self.G = G - self.updates = updates - self.hsg = hsg - - def store_snapshots(self): - """Store any snapshots.""" - for snap in self.G.snapshots: - if snap.time == self.G.iteration + 1: - snap.store(self.G) - - def solve(self, iterations): - """Run timestepping.""" - tsolvestart = perf_counter() - self.iterations = iterations - # for time step in range(self.G.iterations): - - # The main grid FDTD loop - for iteration in self.iterations: - self.updates.grid.iteration = iteration - self.updates.store_outputs() - #self.updates.store_snapshots(iteration) - self.updates.update_magnetic() - self.updates.update_magnetic_pml() - self.updates.update_magnetic_sources(iteration) - self.updates.hsg_2() - self.updates.update_electric_a() - self.updates.update_electric_pml() - self.updates.update_electric_sources(iteration) - self.updates.hsg_1() - self.updates.update_electric_b() - - # Keep track of the index. Required for saving output correctly - self.G.iteration = iteration - - # Return the elapsed time - tsolve = perf_counter() - tsolvestart - - return tsolve - - def write_snapshots(self, iteration): - # Write any snapshots to file - for i, snap in enumerate(self.G.snapshots): - if snap.time == iteration + 1: - snapiters = 36 * (((snap.xf - snap.xs) / snap.dx) * ((snap.yf - snap.ys) / snap.dy) * ((snap.zf - snap.zs) / snap.dz)) - pbar = tqdm(total=snapiters, leave=False, unit='byte', unit_scale=True, desc=' Writing snapshot file {} of {}, {}'.format(i + 1, len(self.G.snapshots), os.path.split(snap.filename)[1]), ncols=get_terminal_width() - 1, file=sys.stdout, disable=self.G.tqdmdisable) - - # Use this call to print out main grid and subgrids - snap.write_vtk_imagedata(self.G.Ex, self.G.Ey, self.G.Ez, self.G.Hx, self.G.Hy, self.G.Hz, self.G, pbar, sub_grids=self.G.subgrids) - - # Use this call to print out the standard grid without subgrid - # snap.write_vtk_imagedata(self.G.Ex, self.G.Ey, self.G.Ez, self.G.Hx, self.G.Hy, self.G.Hz, self.G, pbar) - - # Use this call to print out only the subgrid - use in combination with commented code in .multi_cmds/snapshots.py - # snap.write_vtk_imagedata_fast(self.grid) - pbar.close() - - -class SubgridUpdater(CPUUpdates): - """Class to handle updating the electric and magnetic fields of an HSG - subgrid. The IS, OS, subgrid region and the electric/magnetic sources are updated - using the precursor regions. - """ - - def __init__(self, subgrid, precursors, G): - """ - Args: - subgrid (SubGrid3d): Subgrid to be updated - precursors (PrecursorNodes): Precursor nodes associated with - the subgrid - G (class): Grid class instance - holds essential parameters - describing the model. - """ - super().__init__(subgrid) - self.precursors = precursors - self.G = G - self.source_iteration = 0 - - def hsg_1(self): - """This is the first half of the subgrid update. Takes the time step - up to the main grid magnetic update""" - G = self.G - sub_grid = self.grid - precursors = self.precursors - - precursors.update_electric() - - upper_m = int(sub_grid.ratio / 2 - 0.5) - - for m in range(1, upper_m + 1): - - # STD update, interpolate inc. field in time, apply correction - self.store_outputs() - self.update_electric_a() - self.update_electric_pml() - precursors.interpolate_magnetic_in_time(int(m + sub_grid.ratio / 2 - 0.5)) - sub_grid.update_electric_is(precursors) - self.update_electric_b() - - self.update_sub_grid_electric_sources() - - # STD update, interpolate inc. field in time, apply correction - self.update_magnetic() - self.update_magnetic_pml() - precursors.interpolate_electric_in_time(m) - sub_grid.update_magnetic_is(precursors) - self.update_sub_grid_magnetic_sources() - - self.store_outputs() - self.update_electric_a() - self.update_electric_pml() - precursors.calc_exact_magnetic_in_time() - sub_grid.update_electric_is(precursors) - self.update_electric_b() - self.update_sub_grid_electric_sources() - sub_grid.update_electric_os(G) - - def hsg_2(self): - """This is the first half of the subgrid update. Takes the time step - up to the main grid electric update""" - G = self.G - sub_grid = self.grid - precursors = self.precursors - - precursors.update_magnetic() - - upper_m = int(sub_grid.ratio / 2 - 0.5) - - for m in range(1, upper_m + 1): - - self.update_magnetic() - self.update_magnetic_pml() - - precursors.interpolate_electric_in_time(int(m + sub_grid.ratio / 2 - 0.5)) - sub_grid.update_magnetic_is(precursors) - self.update_sub_grid_magnetic_sources() - - self.store_outputs() - self.update_electric_a() - self.update_electric_pml() - - precursors.interpolate_magnetic_in_time(m) - sub_grid.update_electric_is(precursors) - self.update_electric_b() - - self.update_sub_grid_electric_sources() - - self.update_magnetic() - self.update_magnetic_pml() - precursors.calc_exact_electric_in_time() - sub_grid.update_magnetic_is(precursors) - self.update_sub_grid_magnetic_sources() - sub_grid.update_magnetic_os(G) - - def update_sub_grid_electric_sources(self): - """Update any electric sources in the subgrid""" - sg = self.grid - for source in sg.voltagesources + sg.transmissionlines + sg.hertziandipoles: - source.update_electric(self.source_iteration, sg.updatecoeffsE, sg.ID, - sg.Ex, sg.Ey, sg.Ez, sg) - self.source_iteration += 1 - self.grid.iteration = self.source_iteration - - def update_sub_grid_magnetic_sources(self): - """Update any magnetic sources in the subgrid""" - sg = self.grid - for source in sg.transmissionlines + sg.magneticdipoles: - source.update_magnetic(self.source_iteration, sg.updatecoeffsH, sg.ID, - sg.Hx, sg.Hy, sg.Hz, sg) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +from ..cython.fields_updates_normal import update_electric +from ..cython.fields_updates_normal import update_magnetic +from ..fields_outputs import store_outputs +from ..utilities import get_terminal_width +from ..exceptions import GeneralError + +from .subgrid_hsg import SubGridHSG +from .precursor_nodes import PrecursorNodes as PrecursorNodesHSG +from .precursor_nodes_filtered import PrecursorNodes as PrecursorNodesFilteredHSG + +from tqdm import tqdm +from time import perf_counter + +import os +import sys + +from ..updates import CPUUpdates + +def create_updates(G): + """Return the solver for the given subgrids.""" + updaters = [] + + for sg in G.subgrids: + print(sg) + sg_type = type(sg) + if sg_type == SubGridHSG and sg.filter: + precursors = PrecursorNodesFilteredHSG(G, sg) + elif sg_type == SubGridHSG and not sg.filter: + precursors = PrecursorNodesHSG(G, sg) + else: + raise GeneralError(str(sg) + ' is not a subgrid type') + + sgu = SubgridUpdater(sg, precursors, G) + updaters.append(sgu) + + updates = SubgridUpdates(G, updaters) + return updates + +class SubgridUpdates(CPUUpdates): + + def __init__(self, G, updaters): + super().__init__(G) + self.updaters = updaters + + def hsg_1(self): + """Method to update the subgrids over the first phase.""" + for sg_updater in self.updaters: + sg_updater.hsg_1() + + def hsg_2(self): + """Method to update the subgrids over the second phase.""" + for sg_updater in self.updaters: + sg_updater.hsg_2() + + +class SubgridUpdater(CPUUpdates): + """Class to handle updating the electric and magnetic fields of an HSG + subgrid. The IS, OS, subgrid region and the electric/magnetic sources are updated + using the precursor regions. + """ + + def __init__(self, subgrid, precursors, G): + """ + Args: + subgrid (SubGrid3d): Subgrid to be updated + precursors (PrecursorNodes): Precursor nodes associated with + the subgrid - contain interpolated fields + G (class): Grid class instance - holds essential parameters + describing the model. + """ + super().__init__(subgrid) + self.precursors = precursors + self.G = G + self.source_iteration = 0 + + def hsg_1(self): + """This is the first half of the subgrid update. Takes the time step + up to the main grid magnetic update""" + G = self.G + sub_grid = self.grid + precursors = self.precursors + + precursors.update_electric() + + upper_m = int(sub_grid.ratio / 2 - 0.5) + + for m in range(1, upper_m + 1): + + # STD update, interpolate inc. field in time, apply correction + self.store_outputs() + self.update_electric_a() + self.update_electric_pml() + precursors.interpolate_magnetic_in_time(int(m + sub_grid.ratio / 2 - 0.5)) + sub_grid.update_electric_is(precursors) + self.update_electric_b() + + self.update_electric_sources() + + # STD update, interpolate inc. field in time, apply correction + self.update_magnetic() + self.update_magnetic_pml() + precursors.interpolate_electric_in_time(m) + sub_grid.update_magnetic_is(precursors) + self.update_magnetic_sources() + + self.store_outputs() + self.update_electric_a() + self.update_electric_pml() + precursors.calc_exact_magnetic_in_time() + sub_grid.update_electric_is(precursors) + self.update_electric_b() + self.update_electric_sources() + sub_grid.update_electric_os(G) + + def hsg_2(self): + """This is the first half of the subgrid update. Takes the time step + up to the main grid electric update""" + G = self.G + sub_grid = self.grid + precursors = self.precursors + + precursors.update_magnetic() + + upper_m = int(sub_grid.ratio / 2 - 0.5) + + for m in range(1, upper_m + 1): + + self.update_magnetic() + self.update_magnetic_pml() + + precursors.interpolate_electric_in_time(int(m + sub_grid.ratio / 2 - 0.5)) + sub_grid.update_magnetic_is(precursors) + self.update_magnetic_sources() + + self.store_outputs() + self.update_electric_a() + self.update_electric_pml() + + precursors.interpolate_magnetic_in_time(m) + sub_grid.update_electric_is(precursors) + self.update_electric_b() + + self.update_electric_sources() + + self.update_magnetic() + self.update_magnetic_pml() + precursors.calc_exact_electric_in_time() + sub_grid.update_magnetic_is(precursors) + self.update_magnetic_sources() + sub_grid.update_magnetic_os(G) diff --git a/gprMax/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py index 8ed9d738..eb8d2902 100644 --- a/gprMax/subgrids/user_objects.py +++ b/gprMax/subgrids/user_objects.py @@ -1,205 +1,195 @@ -from .subgrid_hsg import SubGridHSG as SubGridHSGUser -from .multi import ReferenceRx as ReferenceRxUser -from ..exceptions import CmdInputError -from ..cmds_multiple import UserObjectMulti -from ..cmds_geometry.cmds_geometry import UserObjectGeometry -from ..cmds_multiple import Rx -from gprMax import config - -from copy import copy - -import numpy as np - - -class SubGridBase(UserObjectMulti): - """Class to allow UserObjectMulti and UserObjectGeometry to be nested in SubGrid type user objects.""" - - def __init__(self, **kwargs): - """Constructor.""" - super().__init__(**kwargs) - self.children_multiple = [] - self.children_geometry = [] - - def add(self, node): - """Function to add other user objects. Geometry and multi only.""" - if isinstance(node, UserObjectMulti): - self.children_multiple.append(node) - elif isinstance(node, UserObjectGeometry): - self.children_geometry.append(node) - else: - raise Exception(str(node) + ' This Object can not be added to a sub grid') - - def check_filters(self, grid): - """Check the filter of other grids - Only allow filters all on or filters all off.""" - if grid.subgrids: - f = grid.subgrids[0] - if f != self.kwargs['filter']: - raise CmdInputError(self.__str__() + "Filters should be on or off. Set Filter on or off for all subgrids") - - def set_discretisation(self, sg, grid): - """Set the spatial discretisation.""" - sg.dx = grid.dx / sg.ratio - sg.dy = grid.dy / sg.ratio - sg.dz = grid.dz / sg.ratio - sg.dl = np.array([sg.dx, sg.dy, sg.dz]) - - def set_main_grid_indices(self, sg, grid, uip, p1, p2): - """Set subgrid indices related to main grid placement.""" - # Main grid indices of the sub grid. These are dummy indices. They are - # not user internal except for printing to the user - sg.i0_u, sg.j0_u, sg.k0_u = p1 - sg.i1_u, sg.j1_u, sg.k1_u = p2 - - # The actual sub gridded area (IS index) is 4 cells in - sg.i0, sg.j0, sg.k0 = np.add([sg.i0_u, sg.j0_u, sg.k0_u], sg.is_os_sep) - sg.i1, sg.j1, sg.k1 = np.subtract([sg.i1_u, sg.j1_u, sg.k1_u], sg.is_os_sep) - - # Main grid indices of the sub grid. These are dummy indices. They are - # not user internal except for printing to the user - sg.x1_u, sg.y1_u, sg.z1_u = uip.round_to_grid(p1) - sg.x2_u, sg.y2_u, sg.z2_u = uip.round_to_grid(p2) - - sg.x1, sg.y1, sg.z1 = np.add([sg.x1_u, sg.y1_u, sg.z1_u], sg.is_os_sep * sg.dx) - sg.x2, sg.y2, sg.z2 = np.subtract([sg.x2_u, sg.y2_u, sg.z2_u], sg.is_os_sep * sg.dx) - - def set_name(self, sg): - sg.name = self.kwargs['id'] - - def set_working_region_cells(self, sg): - """Number of cells in each dimension for the working region.""" - sg.nwx = (sg.i1 - sg.i0) * sg.ratio - sg.nwy = (sg.j1 - sg.j0) * sg.ratio - sg.nwz = (sg.k1 - sg.k0) * sg.ratio - - def set_total_cells(self, sg): - """Number of cells in each dimension for the whole region.""" - sg.nx = 2 * sg.n_boundary_cells_x + sg.nwx - sg.ny = 2 * sg.n_boundary_cells_y + sg.nwy - sg.nz = 2 * sg.n_boundary_cells_z + sg.nwz - - def set_iterations(self, sg, main): - """Set number of iterations that will take place in the subgrid.""" - sg.iterations = main.iterations * sg.ratio - - def setup(self, sg, grid, uip): - """"Common setup to both all subgrid types.""" - p1 = self.kwargs['p1'] - p2 = self.kwargs['p2'] - - p1, p2 = uip.check_box_points(p1, p2, self.__str__()) - - self.check_filters(grid) - - self.set_discretisation(sg, grid) - - # Set the temporal discretisation - sg.calculate_dt() - - # ensure stability - sg.round_time_step() - - # set the indices related to the subgrids main grid placement - self.set_main_grid_indices(sg, grid, uip, p1, p2) - - """ - try: - uip.check_box_points([sg.i0, sg.j0, sg.k0], - [sg.i1, sg.j1, sg.k1], cmd_str) - except CmdInputError: - es_f = 'The subgrid should extend at least {} cells' - es = es_f.format(sg.is_os_sep * 2) - raise CmdInputError(cmd_str, es) - """ - - self.set_working_region_cells(sg) - self.set_total_cells(sg) - self.set_iterations(sg, grid) - self.set_name(sg) - - # Copy a reference for the main grid to the sub grid - sg.parent_grid = grid - - sg.timewindow = grid.timewindow - - # Copy a subgrid reference to self so that children.create(grid, uip) can access - # the correct grid - self.subgrid = sg - - # Copy over built in materials - sg.materials = [copy(m) for m in grid.materials if m.numID in range(0, grid.n_built_in_materials + 1)] - # use same number of threads - sg.nthreads = grid.nthreads - - # Dont mix and match different subgrids - for sg_made in grid.subgrids: - if type(sg) != type(sg_made): - raise CmdInputError(self.__str__() + ' Please only use one type of subgrid') - - # Reference the sub grid under the main grid to which it belongs. - grid.subgrids.append(sg) - - -class SubGridHSG(SubGridBase): - """HSG User Object.""" - def __init__(self, - p1=None, - p2=None, - ratio=3, - ID='', - is_os_sep=3, - pml_separation=4, - subgrid_pml_thickness=6, - interpolation='linear', - loss_mechanism=False, - loss_factor=False, - filter=True, - **kwargs): - """Constructor.""" - - pml_separation = ratio // 2 + 2 - - # copy over the optional parameters - kwargs['p1'] = p1 - kwargs['p2'] = p2 - kwargs['ratio'] = ratio - kwargs['ID'] = ID - kwargs['is_os_sep'] = is_os_sep - kwargs['pml_separation'] = pml_separation - kwargs['subgrid_pml_thickness'] = subgrid_pml_thickness - kwargs['interpolation'] = interpolation - kwargs['filter'] = filter - kwargs['loss_mechanism'] = loss_mechanism - kwargs['loss_factor'] = loss_factor - - super().__init__(**kwargs) - self.order = 18 - self.hash = '#subgrid_hsg' - - def create(self, grid, uip): - sg = SubGridHSGUser(**self.kwargs) - self.setup(sg, grid, uip) - if config.is_messages(): - print(sg) - return sg - - - -class ReferenceRx(Rx): - """ReferenceRx User Object.""" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.hash = '#rx_reference' - self.constructor = ReferenceRxUser - - def create(self, grid, uip): - - r = super().create(grid, uip) - - try: - ratio = self.kwargs['ratio'] - r.ratio = ratio - r.offset = ratio // 2 - - except KeyError: - raise CmdInputError("'{}' has an no ratio parameter".format(self.__str__())) +from .subgrid_hsg import SubGridHSG as SubGridHSGUser +from .multi import ReferenceRx as ReferenceRxUser +from ..exceptions import CmdInputError +from ..cmds_multiple import UserObjectMulti +from ..cmds_geometry.cmds_geometry import UserObjectGeometry +from ..cmds_multiple import Rx +from gprMax import config + +from copy import copy + +import numpy as np + + +class SubGridBase(UserObjectMulti): + """Class to allow UserObjectMulti and UserObjectGeometry to be nested in SubGrid type user objects.""" + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__(**kwargs) + self.children_multiple = [] + self.children_geometry = [] + + def add(self, node): + """Function to add other user objects. Geometry and multi only.""" + if isinstance(node, UserObjectMulti): + self.children_multiple.append(node) + elif isinstance(node, UserObjectGeometry): + self.children_geometry.append(node) + else: + raise Exception(str(node) + ' This Object can not be added to a sub grid') + + def check_filters(self, grid): + """Check the filter of other grids - Only allow filters all on or filters all off.""" + if grid.subgrids: + f = grid.subgrids[0] + if f != self.kwargs['filter']: + raise CmdInputError(self.__str__() + "Filters should be on or off. Set Filter on or off for all subgrids") + + def set_discretisation(self, sg, grid): + """Set the spatial discretisation.""" + sg.dx = grid.dx / sg.ratio + sg.dy = grid.dy / sg.ratio + sg.dz = grid.dz / sg.ratio + sg.dl = np.array([sg.dx, sg.dy, sg.dz]) + + def set_main_grid_indices(self, sg, grid, uip, p1, p2): + """Set subgrid indices related to main grid placement.""" + # location of the IS + sg.i0, sg.j0, sg.k0 = p1 + sg.i1, sg.j1, sg.k1 = p2 + + sg.x1, sg.y1, sg.z1 = uip.round_to_grid(p1) + sg.x2, sg.y2, sg.z2 = uip.round_to_grid(p2) + + def set_name(self, sg): + sg.name = self.kwargs['id'] + + def set_working_region_cells(self, sg): + """Number of cells in each dimension for the working region.""" + sg.nwx = (sg.i1 - sg.i0) * sg.ratio + sg.nwy = (sg.j1 - sg.j0) * sg.ratio + sg.nwz = (sg.k1 - sg.k0) * sg.ratio + + def set_total_cells(self, sg): + """Number of cells in each dimension for the whole region.""" + sg.nx = 2 * sg.n_boundary_cells_x + sg.nwx + sg.ny = 2 * sg.n_boundary_cells_y + sg.nwy + sg.nz = 2 * sg.n_boundary_cells_z + sg.nwz + + def set_iterations(self, sg, main): + """Set number of iterations that will take place in the subgrid.""" + sg.iterations = main.iterations * sg.ratio + + def setup(self, sg, grid, uip): + """"Common setup to both all subgrid types.""" + p1 = self.kwargs['p1'] + p2 = self.kwargs['p2'] + + p1, p2 = uip.check_box_points(p1, p2, self.__str__()) + + self.check_filters(grid) + + self.set_discretisation(sg, grid) + + # Set the temporal discretisation + sg.calculate_dt() + + # ensure stability + sg.round_time_step() + + # set the indices related to the subgrids main grid placement + self.set_main_grid_indices(sg, grid, uip, p1, p2) + + """ + try: + uip.check_box_points([sg.i0, sg.j0, sg.k0], + [sg.i1, sg.j1, sg.k1], cmd_str) + except CmdInputError: + es_f = 'The subgrid should extend at least {} cells' + es = es_f.format(sg.is_os_sep * 2) + raise CmdInputError(cmd_str, es) + """ + + self.set_working_region_cells(sg) + self.set_total_cells(sg) + self.set_iterations(sg, grid) + self.set_name(sg) + + # Copy a reference for the main grid to the sub grid + sg.parent_grid = grid + + sg.timewindow = grid.timewindow + + # Copy a subgrid reference to self so that children.create(grid, uip) can access + # the correct grid + self.subgrid = sg + + # Copy over built in materials + sg.materials = [copy(m) for m in grid.materials if m.numID in range(0, grid.n_built_in_materials + 1)] + # use same number of threads + sg.nthreads = grid.nthreads + + # Dont mix and match different subgrids + for sg_made in grid.subgrids: + if type(sg) != type(sg_made): + raise CmdInputError(self.__str__() + ' Please only use one type of subgrid') + + # Reference the sub grid under the main grid to which it belongs. + grid.subgrids.append(sg) + + +class SubGridHSG(SubGridBase): + """HSG User Object.""" + def __init__(self, + p1=None, + p2=None, + ratio=3, + ID='', + is_os_sep=3, + pml_separation=4, + subgrid_pml_thickness=6, + interpolation='linear', + loss_mechanism=False, + loss_factor=False, + filter=True, + **kwargs): + """Constructor.""" + + pml_separation = ratio // 2 + 2 + + # copy over the optional parameters + kwargs['p1'] = p1 + kwargs['p2'] = p2 + kwargs['ratio'] = ratio + kwargs['ID'] = ID + kwargs['is_os_sep'] = is_os_sep + kwargs['pml_separation'] = pml_separation + kwargs['subgrid_pml_thickness'] = subgrid_pml_thickness + kwargs['interpolation'] = interpolation + kwargs['filter'] = filter + kwargs['loss_mechanism'] = loss_mechanism + kwargs['loss_factor'] = loss_factor + + super().__init__(**kwargs) + self.order = 18 + self.hash = '#subgrid_hsg' + + def create(self, grid, uip): + sg = SubGridHSGUser(**self.kwargs) + self.setup(sg, grid, uip) + if config.is_messages(): + print(sg) + return sg + + + +class ReferenceRx(Rx): + """ReferenceRx User Object.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.hash = '#rx_reference' + self.constructor = ReferenceRxUser + + def create(self, grid, uip): + + r = super().create(grid, uip) + + try: + ratio = self.kwargs['ratio'] + r.ratio = ratio + r.offset = ratio // 2 + + except KeyError: + raise CmdInputError("'{}' has an no ratio parameter".format(self.__str__())) diff --git a/gprMax/templates/fields_updates_dispersive_template b/gprMax/templates/fields_updates_dispersive_template index 88626be5..9906ced5 100644 --- a/gprMax/templates/fields_updates_dispersive_template +++ b/gprMax/templates/fields_updates_dispersive_template @@ -1,314 +1,314 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -import numpy as np -cimport numpy as np -from cython.parallel import prange - -cdef extern from "complex.h" nogil: - double creal(double complex z) - float crealf(float complex z) - - -############################################################### -# Electric field updates - dispersive materials - multipole A # -############################################################### - -{% for item in functions %} -cpdef void {{ item.name_a }}( - int nx, - int ny, - int nz, - int nthreads, - int maxpoles, - {{ item.field_type }}[:, ::1] updatecoeffsE, - {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, - np.uint32_t[:, :, :, ::1] ID, - {{ item.dispersive_type }}[:, :, :, ::1] Tx, - {{ item.dispersive_type }}[:, :, :, ::1] Ty, - {{ item.dispersive_type }}[:, :, :, ::1] Tz, - {{ item.field_type }}[:, :, ::1] Ex, - {{ item.field_type }}[:, :, ::1] Ey, - {{ item.field_type }}[:, :, ::1] Ez, - {{ item.field_type }}[:, :, ::1] Hx, - {{ item.field_type }}[:, :, ::1] Hy, - {{ item.field_type }}[:, :, ::1] Hz - ): - """This function updates the electric field components when dispersive materials (with multiple poles) are present. - - Args: - nx, ny, nz (int): Grid size in cells - nthreads (int): Number of threads to use - maxpoles (int): Maximum number of poles - updatecoeffs, T, ID, E, H (memoryviews): Access to update coeffients, temporary, ID and field component arrays - """ - - cdef Py_ssize_t i, j, k, pole - cdef int material - cdef float phi = 0 - - # Ex component - if ny != 1 or nz != 1: - for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(1, nz): - material = ID[0, i, j, k] - phi = 0 - for pole in range(maxpoles): - {% if 'complex' in item.dispersive_type %} - phi = phi + {{ item.real_part }}(updatecoeffsdispersive[material, pole * 3]) * {{ item.real_part }}(Tx[pole, i, j, k]) - {% else %} - phi = phi + updatecoeffsdispersive[material, pole * 3] * Tx[pole, i, j, k] - {% endif %} - Tx[pole, i, j, k] = updatecoeffsdispersive[material, 1 + (pole * 3)] * Tx[pole, i, j, k] + updatecoeffsdispersive[material, 2 + (pole * 3)] * Ex[i, j, k] - Ex[i, j, k] = updatecoeffsE[material, 0] * Ex[i, j, k] + updatecoeffsE[material, 2] * (Hz[i, j, k] - Hz[i, j - 1, k]) - updatecoeffsE[material, 3] * (Hy[i, j, k] - Hy[i, j, k - 1]) - updatecoeffsE[material, 4] * phi - - # Ey component - if nx != 1 or nz != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(0, ny): - for k in range(1, nz): - material = ID[1, i, j, k] - phi = 0 - for pole in range(maxpoles): - {% if 'complex' in item.dispersive_type %} - phi = phi + {{ item.real_part }}(updatecoeffsdispersive[material, pole * 3]) * {{ item.real_part }}(Ty[pole, i, j, k]) - {% else %} - phi = phi + updatecoeffsdispersive[material, pole * 3] * Ty[pole, i, j, k] - {% endif %} - Ty[pole, i, j, k] = updatecoeffsdispersive[material, 1 + (pole * 3)] * Ty[pole, i, j, k] + updatecoeffsdispersive[material, 2 + (pole * 3)] * Ey[i, j, k] - Ey[i, j, k] = updatecoeffsE[material, 0] * Ey[i, j, k] + updatecoeffsE[material, 3] * (Hx[i, j, k] - Hx[i, j, k - 1]) - updatecoeffsE[material, 1] * (Hz[i, j, k] - Hz[i - 1, j, k]) - updatecoeffsE[material, 4] * phi - - # Ez component - if nx != 1 or ny != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(0, nz): - material = ID[2, i, j, k] - phi = 0 - for pole in range(maxpoles): - {% if 'complex' in item.dispersive_type %} - phi = phi + {{ item.real_part }}(updatecoeffsdispersive[material, pole * 3]) * {{ item.real_part }}(Tz[pole, i, j, k]) - {% else %} - phi = phi + updatecoeffsdispersive[material, pole * 3] * Tz[pole, i, j, k] - {% endif %} - Tz[pole, i, j, k] = updatecoeffsdispersive[material, 1 + (pole * 3)] * Tz[pole, i, j, k] + updatecoeffsdispersive[material, 2 + (pole * 3)] * Ez[i, j, k] - Ez[i, j, k] = updatecoeffsE[material, 0] * Ez[i, j, k] + updatecoeffsE[material, 1] * (Hy[i, j, k] - Hy[i - 1, j, k]) - updatecoeffsE[material, 2] * (Hx[i, j, k] - Hx[i, j - 1, k]) - updatecoeffsE[material, 4] * phi -{% endfor %} - - -############################################################### -# Electric field updates - dispersive materials - multipole B # -############################################################### - -{% for item in functions %} -cpdef void {{ item.name_b }}( - int nx, - int ny, - int nz, - int nthreads, - int maxpoles, - {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, - np.uint32_t[:, :, :, ::1] ID, - {{ item.dispersive_type }}[:, :, :, ::1] Tx, - {{ item.dispersive_type }}[:, :, :, ::1] Ty, - {{ item.dispersive_type }}[:, :, :, ::1] Tz, - {{ item.field_type }}[:, :, ::1] Ex, - {{ item.field_type }}[:, :, ::1] Ey, - {{ item.field_type }}[:, :, ::1] Ez - ): - """This function updates a temporary dispersive material array when disperisive materials (with multiple poles) are present. - - Args: - nx, ny, nz (int): Grid size in cells - nthreads (int): Number of threads to use - maxpoles (int): Maximum number of poles - updatecoeffs, T, ID, E (memoryviews): Access to update coeffients, temporary, ID and field component arrays - """ - - cdef Py_ssize_t i, j, k, pole - cdef int material - - # Ex component - if ny != 1 or nz != 1: - for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(1, nz): - material = ID[0, i, j, k] - for pole in range(maxpoles): - Tx[pole, i, j, k] = Tx[pole, i, j, k] - updatecoeffsdispersive[material, 2 + (pole * 3)] * Ex[i, j, k] - - # Ey component - if nx != 1 or nz != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(0, ny): - for k in range(1, nz): - material = ID[1, i, j, k] - for pole in range(maxpoles): - Ty[pole, i, j, k] = Ty[pole, i, j, k] - updatecoeffsdispersive[material, 2 + (pole * 3)] * Ey[i, j, k] - - # Ez component - if nx != 1 or ny != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(0, nz): - material = ID[2, i, j, k] - for pole in range(maxpoles): - Tz[pole, i, j, k] = Tz[pole, i, j, k] - updatecoeffsdispersive[material, 2 + (pole * 3)] * Ez[i, j, k] - -{% endfor %} - - -################################################################# -# Electric field updates - dispersive materials - single pole A # -################################################################# - -{% for item in functions %} -cpdef void {{ item.name_a_1 }}( - int nx, - int ny, - int nz, - int nthreads, - int maxpoles, - {{ item.field_type }}[:, ::1] updatecoeffsE, - {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, - np.uint32_t[:, :, :, ::1] ID, - {{ item.dispersive_type }}[:, :, :, ::1] Tx, - {{ item.dispersive_type }}[:, :, :, ::1] Ty, - {{ item.dispersive_type }}[:, :, :, ::1] Tz, - {{ item.field_type }}[:, :, ::1] Ex, - {{ item.field_type }}[:, :, ::1] Ey, - {{ item.field_type }}[:, :, ::1] Ez, - {{ item.field_type }}[:, :, ::1] Hx, - {{ item.field_type }}[:, :, ::1] Hy, - {{ item.field_type }}[:, :, ::1] Hz - ): - """This function updates the electric field components when dispersive materials (with 1 pole) are present. - - Args: - nx, ny, nz (int): Grid size in cells - nthreads (int): Number of threads to use - maxpoles (int): Maximum number of poles - updatecoeffs, T, ID, E, H (memoryviews): Access to update coeffients, temporary, ID and field component arrays - """ - - cdef Py_ssize_t i, j, k - cdef int material - cdef float phi = 0 - - # Ex component - if ny != 1 or nz != 1: - for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(1, nz): - material = ID[0, i, j, k] - {% if 'complex' in item.dispersive_type %} - phi = {{ item.real_part }}(updatecoeffsdispersive[material, 0]) * {{ item.real_part }}(Tx[0, i, j, k]) - {% else %} - phi = updatecoeffsdispersive[material, 0] * Tx[0, i, j, k] - {% endif %} - Tx[0, i, j, k] = updatecoeffsdispersive[material, 1] * Tx[0, i, j, k] + updatecoeffsdispersive[material, 2] * Ex[i, j, k] - Ex[i, j, k] = updatecoeffsE[material, 0] * Ex[i, j, k] + updatecoeffsE[material, 2] * (Hz[i, j, k] - Hz[i, j - 1, k]) - updatecoeffsE[material, 3] * (Hy[i, j, k] - Hy[i, j, k - 1]) - updatecoeffsE[material, 4] * phi - - # Ey component - if nx != 1 or nz != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(0, ny): - for k in range(1, nz): - material = ID[1, i, j, k] - {% if 'complex' in item.dispersive_type %} - phi = {{ item.real_part }}(updatecoeffsdispersive[material, 0]) * {{ item.real_part }}(Ty[0, i, j, k]) - {% else %} - phi = updatecoeffsdispersive[material, 0] * Ty[0, i, j, k] - {% endif %} - Ty[0, i, j, k] = updatecoeffsdispersive[material, 1] * Ty[0, i, j, k] + updatecoeffsdispersive[material, 2] * Ey[i, j, k] - Ey[i, j, k] = updatecoeffsE[material, 0] * Ey[i, j, k] + updatecoeffsE[material, 3] * (Hx[i, j, k] - Hx[i, j, k - 1]) - updatecoeffsE[material, 1] * (Hz[i, j, k] - Hz[i - 1, j, k]) - updatecoeffsE[material, 4] * phi - - # Ez component - if nx != 1 or ny != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(0, nz): - material = ID[2, i, j, k] - {% if 'complex' in item.dispersive_type %} - phi = {{ item.real_part }}(updatecoeffsdispersive[material, 0]) * {{ item.real_part }}(Tz[0, i, j, k]) - {% else %} - phi = updatecoeffsdispersive[material, 0] * Tz[0, i, j, k] - {% endif %} - Tz[0, i, j, k] = updatecoeffsdispersive[material, 1] * Tz[0, i, j, k] + updatecoeffsdispersive[material, 2] * Ez[i, j, k] - Ez[i, j, k] = updatecoeffsE[material, 0] * Ez[i, j, k] + updatecoeffsE[material, 1] * (Hy[i, j, k] - Hy[i - 1, j, k]) - updatecoeffsE[material, 2] * (Hx[i, j, k] - Hx[i, j - 1, k]) - updatecoeffsE[material, 4] * phi - -{% endfor %} - - -################################################################# -# Electric field updates - dispersive materials - single pole B # -################################################################# - -{% for item in functions %} -cpdef void {{ item.name_b_1 }}( - int nx, - int ny, - int nz, - int nthreads, - int maxpoles, - {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, - np.uint32_t[:, :, :, ::1] ID, - {{ item.dispersive_type }}[:, :, :, ::1] Tx, - {{ item.dispersive_type }}[:, :, :, ::1] Ty, - {{ item.dispersive_type }}[:, :, :, ::1] Tz, - {{ item.field_type }}[:, :, ::1] Ex, - {{ item.field_type }}[:, :, ::1] Ey, - {{ item.field_type }}[:, :, ::1] Ez - ): - """This function updates a temporary dispersive material array when disperisive materials (with 1 pole) are present. - - Args: - nx, ny, nz (int): Grid size in cells - nthreads (int): Number of threads to use - maxpoles (int): Maximum number of poles - updatecoeffs, T, ID, E (memoryviews): Access to update coeffients, temporary, ID and field component arrays - """ - - cdef Py_ssize_t i, j, k - cdef int material - - # Ex component - if ny != 1 or nz != 1: - for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(1, nz): - material = ID[0, i, j, k] - Tx[0, i, j, k] = Tx[0, i, j, k] - updatecoeffsdispersive[material, 2] * Ex[i, j, k] - - # Ey component - if nx != 1 or nz != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(0, ny): - for k in range(1, nz): - material = ID[1, i, j, k] - Ty[0, i, j, k] = Ty[0, i, j, k] - updatecoeffsdispersive[material, 2] * Ey[i, j, k] - - # Ez component - if nx != 1 or ny != 1: - for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): - for j in range(1, ny): - for k in range(0, nz): - material = ID[2, i, j, k] - Tz[0, i, j, k] = Tz[0, i, j, k] - updatecoeffsdispersive[material, 2] * Ez[i, j, k] -{% endfor %} +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +import numpy as np +cimport numpy as np +from cython.parallel import prange + +cdef extern from "complex.h" nogil: + double creal(double complex z) + float crealf(float complex z) + + +############################################################### +# Electric field updates - dispersive materials - multipole A # +############################################################### + +{% for item in functions %} +cpdef void {{ item.name_a }}( + int nx, + int ny, + int nz, + int nthreads, + int maxpoles, + {{ item.field_type }}[:, ::1] updatecoeffsE, + {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, + np.uint32_t[:, :, :, ::1] ID, + {{ item.dispersive_type }}[:, :, :, ::1] Tx, + {{ item.dispersive_type }}[:, :, :, ::1] Ty, + {{ item.dispersive_type }}[:, :, :, ::1] Tz, + {{ item.field_type }}[:, :, ::1] Ex, + {{ item.field_type }}[:, :, ::1] Ey, + {{ item.field_type }}[:, :, ::1] Ez, + {{ item.field_type }}[:, :, ::1] Hx, + {{ item.field_type }}[:, :, ::1] Hy, + {{ item.field_type }}[:, :, ::1] Hz + ): + """This function updates the electric field components when dispersive materials (with multiple poles) are present. + + Args: + nx, ny, nz (int): Grid size in cells + nthreads (int): Number of threads to use + maxpoles (int): Maximum number of poles + updatecoeffs, T, ID, E, H (memoryviews): Access to update coeffients, temporary, ID and field component arrays + """ + + cdef Py_ssize_t i, j, k, pole + cdef int material + cdef float phi = 0 + + # Ex component + if ny != 1 or nz != 1: + for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(1, nz): + material = ID[0, i, j, k] + phi = 0 + for pole in range(maxpoles): + {% if 'complex' in item.dispersive_type %} + phi = phi + {{ item.real_part }}(updatecoeffsdispersive[material, pole * 3]) * {{ item.real_part }}(Tx[pole, i, j, k]) + {% else %} + phi = phi + updatecoeffsdispersive[material, pole * 3] * Tx[pole, i, j, k] + {% endif %} + Tx[pole, i, j, k] = updatecoeffsdispersive[material, 1 + (pole * 3)] * Tx[pole, i, j, k] + updatecoeffsdispersive[material, 2 + (pole * 3)] * Ex[i, j, k] + Ex[i, j, k] = updatecoeffsE[material, 0] * Ex[i, j, k] + updatecoeffsE[material, 2] * (Hz[i, j, k] - Hz[i, j - 1, k]) - updatecoeffsE[material, 3] * (Hy[i, j, k] - Hy[i, j, k - 1]) - updatecoeffsE[material, 4] * phi + + # Ey component + if nx != 1 or nz != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(0, ny): + for k in range(1, nz): + material = ID[1, i, j, k] + phi = 0 + for pole in range(maxpoles): + {% if 'complex' in item.dispersive_type %} + phi = phi + {{ item.real_part }}(updatecoeffsdispersive[material, pole * 3]) * {{ item.real_part }}(Ty[pole, i, j, k]) + {% else %} + phi = phi + updatecoeffsdispersive[material, pole * 3] * Ty[pole, i, j, k] + {% endif %} + Ty[pole, i, j, k] = updatecoeffsdispersive[material, 1 + (pole * 3)] * Ty[pole, i, j, k] + updatecoeffsdispersive[material, 2 + (pole * 3)] * Ey[i, j, k] + Ey[i, j, k] = updatecoeffsE[material, 0] * Ey[i, j, k] + updatecoeffsE[material, 3] * (Hx[i, j, k] - Hx[i, j, k - 1]) - updatecoeffsE[material, 1] * (Hz[i, j, k] - Hz[i - 1, j, k]) - updatecoeffsE[material, 4] * phi + + # Ez component + if nx != 1 or ny != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(0, nz): + material = ID[2, i, j, k] + phi = 0 + for pole in range(maxpoles): + {% if 'complex' in item.dispersive_type %} + phi = phi + {{ item.real_part }}(updatecoeffsdispersive[material, pole * 3]) * {{ item.real_part }}(Tz[pole, i, j, k]) + {% else %} + phi = phi + updatecoeffsdispersive[material, pole * 3] * Tz[pole, i, j, k] + {% endif %} + Tz[pole, i, j, k] = updatecoeffsdispersive[material, 1 + (pole * 3)] * Tz[pole, i, j, k] + updatecoeffsdispersive[material, 2 + (pole * 3)] * Ez[i, j, k] + Ez[i, j, k] = updatecoeffsE[material, 0] * Ez[i, j, k] + updatecoeffsE[material, 1] * (Hy[i, j, k] - Hy[i - 1, j, k]) - updatecoeffsE[material, 2] * (Hx[i, j, k] - Hx[i, j - 1, k]) - updatecoeffsE[material, 4] * phi +{% endfor %} + + +############################################################### +# Electric field updates - dispersive materials - multipole B # +############################################################### + +{% for item in functions %} +cpdef void {{ item.name_b }}( + int nx, + int ny, + int nz, + int nthreads, + int maxpoles, + {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, + np.uint32_t[:, :, :, ::1] ID, + {{ item.dispersive_type }}[:, :, :, ::1] Tx, + {{ item.dispersive_type }}[:, :, :, ::1] Ty, + {{ item.dispersive_type }}[:, :, :, ::1] Tz, + {{ item.field_type }}[:, :, ::1] Ex, + {{ item.field_type }}[:, :, ::1] Ey, + {{ item.field_type }}[:, :, ::1] Ez + ): + """This function updates a temporary dispersive material array when disperisive materials (with multiple poles) are present. + + Args: + nx, ny, nz (int): Grid size in cells + nthreads (int): Number of threads to use + maxpoles (int): Maximum number of poles + updatecoeffs, T, ID, E (memoryviews): Access to update coeffients, temporary, ID and field component arrays + """ + + cdef Py_ssize_t i, j, k, pole + cdef int material + + # Ex component + if ny != 1 or nz != 1: + for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(1, nz): + material = ID[0, i, j, k] + for pole in range(maxpoles): + Tx[pole, i, j, k] = Tx[pole, i, j, k] - updatecoeffsdispersive[material, 2 + (pole * 3)] * Ex[i, j, k] + + # Ey component + if nx != 1 or nz != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(0, ny): + for k in range(1, nz): + material = ID[1, i, j, k] + for pole in range(maxpoles): + Ty[pole, i, j, k] = Ty[pole, i, j, k] - updatecoeffsdispersive[material, 2 + (pole * 3)] * Ey[i, j, k] + + # Ez component + if nx != 1 or ny != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(0, nz): + material = ID[2, i, j, k] + for pole in range(maxpoles): + Tz[pole, i, j, k] = Tz[pole, i, j, k] - updatecoeffsdispersive[material, 2 + (pole * 3)] * Ez[i, j, k] + +{% endfor %} + + +################################################################# +# Electric field updates - dispersive materials - single pole A # +################################################################# + +{% for item in functions %} +cpdef void {{ item.name_a_1 }}( + int nx, + int ny, + int nz, + int nthreads, + int maxpoles, + {{ item.field_type }}[:, ::1] updatecoeffsE, + {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, + np.uint32_t[:, :, :, ::1] ID, + {{ item.dispersive_type }}[:, :, :, ::1] Tx, + {{ item.dispersive_type }}[:, :, :, ::1] Ty, + {{ item.dispersive_type }}[:, :, :, ::1] Tz, + {{ item.field_type }}[:, :, ::1] Ex, + {{ item.field_type }}[:, :, ::1] Ey, + {{ item.field_type }}[:, :, ::1] Ez, + {{ item.field_type }}[:, :, ::1] Hx, + {{ item.field_type }}[:, :, ::1] Hy, + {{ item.field_type }}[:, :, ::1] Hz + ): + """This function updates the electric field components when dispersive materials (with 1 pole) are present. + + Args: + nx, ny, nz (int): Grid size in cells + nthreads (int): Number of threads to use + maxpoles (int): Maximum number of poles + updatecoeffs, T, ID, E, H (memoryviews): Access to update coeffients, temporary, ID and field component arrays + """ + + cdef Py_ssize_t i, j, k + cdef int material + cdef float phi = 0 + + # Ex component + if ny != 1 or nz != 1: + for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(1, nz): + material = ID[0, i, j, k] + {% if 'complex' in item.dispersive_type %} + phi = {{ item.real_part }}(updatecoeffsdispersive[material, 0]) * {{ item.real_part }}(Tx[0, i, j, k]) + {% else %} + phi = updatecoeffsdispersive[material, 0] * Tx[0, i, j, k] + {% endif %} + Tx[0, i, j, k] = updatecoeffsdispersive[material, 1] * Tx[0, i, j, k] + updatecoeffsdispersive[material, 2] * Ex[i, j, k] + Ex[i, j, k] = updatecoeffsE[material, 0] * Ex[i, j, k] + updatecoeffsE[material, 2] * (Hz[i, j, k] - Hz[i, j - 1, k]) - updatecoeffsE[material, 3] * (Hy[i, j, k] - Hy[i, j, k - 1]) - updatecoeffsE[material, 4] * phi + + # Ey component + if nx != 1 or nz != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(0, ny): + for k in range(1, nz): + material = ID[1, i, j, k] + {% if 'complex' in item.dispersive_type %} + phi = {{ item.real_part }}(updatecoeffsdispersive[material, 0]) * {{ item.real_part }}(Ty[0, i, j, k]) + {% else %} + phi = updatecoeffsdispersive[material, 0] * Ty[0, i, j, k] + {% endif %} + Ty[0, i, j, k] = updatecoeffsdispersive[material, 1] * Ty[0, i, j, k] + updatecoeffsdispersive[material, 2] * Ey[i, j, k] + Ey[i, j, k] = updatecoeffsE[material, 0] * Ey[i, j, k] + updatecoeffsE[material, 3] * (Hx[i, j, k] - Hx[i, j, k - 1]) - updatecoeffsE[material, 1] * (Hz[i, j, k] - Hz[i - 1, j, k]) - updatecoeffsE[material, 4] * phi + + # Ez component + if nx != 1 or ny != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(0, nz): + material = ID[2, i, j, k] + {% if 'complex' in item.dispersive_type %} + phi = {{ item.real_part }}(updatecoeffsdispersive[material, 0]) * {{ item.real_part }}(Tz[0, i, j, k]) + {% else %} + phi = updatecoeffsdispersive[material, 0] * Tz[0, i, j, k] + {% endif %} + Tz[0, i, j, k] = updatecoeffsdispersive[material, 1] * Tz[0, i, j, k] + updatecoeffsdispersive[material, 2] * Ez[i, j, k] + Ez[i, j, k] = updatecoeffsE[material, 0] * Ez[i, j, k] + updatecoeffsE[material, 1] * (Hy[i, j, k] - Hy[i - 1, j, k]) - updatecoeffsE[material, 2] * (Hx[i, j, k] - Hx[i, j - 1, k]) - updatecoeffsE[material, 4] * phi + +{% endfor %} + + +################################################################# +# Electric field updates - dispersive materials - single pole B # +################################################################# + +{% for item in functions %} +cpdef void {{ item.name_b_1 }}( + int nx, + int ny, + int nz, + int nthreads, + int maxpoles, + {{ item.dispersive_type }}[:, ::1] updatecoeffsdispersive, + np.uint32_t[:, :, :, ::1] ID, + {{ item.dispersive_type }}[:, :, :, ::1] Tx, + {{ item.dispersive_type }}[:, :, :, ::1] Ty, + {{ item.dispersive_type }}[:, :, :, ::1] Tz, + {{ item.field_type }}[:, :, ::1] Ex, + {{ item.field_type }}[:, :, ::1] Ey, + {{ item.field_type }}[:, :, ::1] Ez + ): + """This function updates a temporary dispersive material array when disperisive materials (with 1 pole) are present. + + Args: + nx, ny, nz (int): Grid size in cells + nthreads (int): Number of threads to use + maxpoles (int): Maximum number of poles + updatecoeffs, T, ID, E (memoryviews): Access to update coeffients, temporary, ID and field component arrays + """ + + cdef Py_ssize_t i, j, k + cdef int material + + # Ex component + if ny != 1 or nz != 1: + for i in prange(0, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(1, nz): + material = ID[0, i, j, k] + Tx[0, i, j, k] = Tx[0, i, j, k] - updatecoeffsdispersive[material, 2] * Ex[i, j, k] + + # Ey component + if nx != 1 or nz != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(0, ny): + for k in range(1, nz): + material = ID[1, i, j, k] + Ty[0, i, j, k] = Ty[0, i, j, k] - updatecoeffsdispersive[material, 2] * Ey[i, j, k] + + # Ez component + if nx != 1 or ny != 1: + for i in prange(1, nx, nogil=True, schedule='static', num_threads=nthreads): + for j in range(1, ny): + for k in range(0, nz): + material = ID[2, i, j, k] + Tz[0, i, j, k] = Tz[0, i, j, k] - updatecoeffsdispersive[material, 2] * Ez[i, j, k] +{% endfor %} diff --git a/gprMax/updates.py b/gprMax/updates.py index 03e5d5bf..2ab9957b 100644 --- a/gprMax/updates.py +++ b/gprMax/updates.py @@ -1,185 +1,186 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -from importlib import import_module -from gprMax.fields_outputs import store_outputs -import gprMax.config as config -from gprMax.cython.fields_updates_normal import update_electric -from gprMax.cython.fields_updates_normal import update_magnetic - - -class CPUUpdates: - - def __init__(self, G): - self.grid = G - self.dispersive_update_a = None - self.dispersive_update_b = None - - def store_outputs(self): - # Store field component values for every receiver and transmission line - store_outputs(self.grid) - - def store_snapshots(self, iteration): - # Store any snapshots - for snap in self.grid.snapshots: - if snap.time == iteration + 1: - snap.store(self.grid) - - def update_magnetic(self): - # Update magnetic field components - update_magnetic(self.grid.nx, - self.grid.ny, - self.grid.nz, - config.hostinfo['ompthreads'], - self.grid.updatecoeffsH, - self.grid.ID, - self.grid.Ex, - self.grid.Ey, - self.grid.Ez, - self.grid.Hx, - self.grid.Hy, - self.grid.Hz) - - def update_magnetic_pml(self): - # Update magnetic field components with the PML correction - for pml in self.grid.pmls: - pml.update_magnetic(self.grid) - - def update_magnetic_sources(self, iteration): - # Update magnetic field components from sources - for source in self.grid.transmissionlines + self.grid.magneticdipoles: - source.update_magnetic(iteration, - self.grid.updatecoeffsH, - self.grid.ID, - self.grid.Hx, - self.grid.Hy, - self.grid.Hz, - self.grid) - - def update_electric_a(self): - # Update electric field components - # All materials are non-dispersive so do standard update - if config.materials['maxpoles'] == 0: - update_electric(self.grid.nx, - self.grid.ny, - self.grid.nz, - config.hostinfo['ompthreads'], - self.grid.updatecoeffsE, - self.grid.ID, - self.grid.Ex, - self.grid.Ey, - self.grid.Ez, - self.grid.Hx, - self.grid.Hy, - self.grid.Hz) - - # If there are any dispersive materials do 1st part of dispersive update - # (it is split into two parts as it requires present and updated electric field values). - else: - self.dispersive_update_a(self.grid.nx, - self.grid.ny, - self.grid.nz, - config.hostinfo['ompthreads'], - config.materials['maxpoles'], - self.grid.updatecoeffsE, - self.grid.updatecoeffsdispersive, - self.grid.ID, - self.grid.Tx, - self.grid.Ty, - self.grid.Tz, - self.grid.Ex, - self.grid.Ey, - self.grid.Ez, - self.grid.Hx, - self.grid.Hy, - self.grid.Hz) - - def update_electric_pml(self): - # Update electric field components with the PML correction - for pml in self.grid.pmls: - pml.update_electric(self.grid) - - def update_electric_sources(self, iteration): - # Update electric field components from sources (update any Hertzian dipole sources last) - for source in self.grid.voltagesources + self.grid.transmissionlines + self.grid.hertziandipoles: - source.update_electric(iteration, self.grid.updatecoeffsE, self.grid.ID, self.grid.Ex, self.grid.Ey, self.grid.Ez, self.grid) - - def update_electric_b(self): - # If there are any dispersive materials do 2nd part of dispersive update - # (it is split into two parts as it requires present and updated electric - # 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(self.grid.nx, - self.grid.ny, - self.grid.nz, - config.hostinfo['ompthreads'], - config.materials['maxpoles'], - self.grid.updatecoeffsdispersive, - self.grid.ID, - self.grid.Tx, - self.grid.Ty, - self.grid.Tz, - self.grid.Ex, - self.grid.Ey, - self.grid.Ez) - - def adapt_dispersive_config(self, config): - - if config.materials['maxpoles'] > 1: - poles = 'multi' - - else: - poles = '1' - - if config.precision == 'single': - type = 'float' - - else: - type = 'double' - - if config.materials['dispersivedtype'] == config.dtypes['complex']: - dispersion = 'complex' - else: - dispersion = 'real' - - class Props(): - pass - - props = Props() - props.poles = poles - props.precision = type - props.dispersion_type = dispersion - - return props - - def set_dispersive_updates(self, props): - """Function to set dispersive update functions based on model.""" - - update_f = 'update_electric_dispersive_{}pole_{}_{}_{}' - disp_a = update_f.format(props.poles, 'A', props.precision, props.dispersion_type) - disp_b = update_f.format(props.poles, 'B', props.precision, props.dispersion_type) - - disp_a_f = getattr(import_module('gprMax.cython.fields_updates_dispersive'), disp_a) - disp_b_f = getattr(import_module('gprMax.cython.fields_updates_dispersive'), disp_b) - - self.dispersive_update_a = disp_a_f - self.dispersive_update_b = disp_b_f - - -class GPUUpdates: - pass +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +from importlib import import_module +from gprMax.fields_outputs import store_outputs +import gprMax.config as config +from gprMax.cython.fields_updates_normal import update_electric +from gprMax.cython.fields_updates_normal import update_magnetic + + +class CPUUpdates: + + def __init__(self, G): + self.grid = G + self.dispersive_update_a = None + self.dispersive_update_b = None + + def store_outputs(self): + # Store field component values for every receiver and transmission line + store_outputs(self.grid) + + def store_snapshots(self, iteration): + # Store any snapshots + for snap in self.grid.snapshots: + if snap.time == iteration + 1: + snap.store(self.grid) + + def update_magnetic(self): + # Update magnetic field components + update_magnetic(self.grid.nx, + self.grid.ny, + self.grid.nz, + config.hostinfo['ompthreads'], + self.grid.updatecoeffsH, + self.grid.ID, + self.grid.Ex, + self.grid.Ey, + self.grid.Ez, + self.grid.Hx, + self.grid.Hy, + self.grid.Hz) + + def update_magnetic_pml(self): + # Update magnetic field components with the PML correction + for pml in self.grid.pmls: + pml.update_magnetic(self.grid) + + def update_magnetic_sources(self): + # Update magnetic field components from sources + for source in self.grid.transmissionlines + self.grid.magneticdipoles: + source.update_magnetic(self.grid.iteration, + self.grid.updatecoeffsH, + self.grid.ID, + self.grid.Hx, + self.grid.Hy, + self.grid.Hz, + self.grid) + + def update_electric_a(self): + # Update electric field components + # All materials are non-dispersive so do standard update + if config.materials['maxpoles'] == 0: + update_electric(self.grid.nx, + self.grid.ny, + self.grid.nz, + config.hostinfo['ompthreads'], + self.grid.updatecoeffsE, + self.grid.ID, + self.grid.Ex, + self.grid.Ey, + self.grid.Ez, + self.grid.Hx, + self.grid.Hy, + self.grid.Hz) + + # If there are any dispersive materials do 1st part of dispersive update + # (it is split into two parts as it requires present and updated electric field values). + else: + self.dispersive_update_a(self.grid.nx, + self.grid.ny, + self.grid.nz, + config.hostinfo['ompthreads'], + config.materials['maxpoles'], + self.grid.updatecoeffsE, + self.grid.updatecoeffsdispersive, + self.grid.ID, + self.grid.Tx, + self.grid.Ty, + self.grid.Tz, + self.grid.Ex, + self.grid.Ey, + self.grid.Ez, + self.grid.Hx, + self.grid.Hy, + self.grid.Hz) + + def update_electric_pml(self): + # Update electric field components with the PML correction + for pml in self.grid.pmls: + pml.update_electric(self.grid) + + def update_electric_sources(self): + # Update electric field components from sources (update any Hertzian dipole sources last) + for source in self.grid.voltagesources + self.grid.transmissionlines + self.grid.hertziandipoles: + source.update_electric(self.grid.iteration, self.grid.updatecoeffsE, self.grid.ID, self.grid.Ex, self.grid.Ey, self.grid.Ez, self.grid) + self.grid.iteration += 1 + + def update_electric_b(self): + # If there are any dispersive materials do 2nd part of dispersive update + # (it is split into two parts as it requires present and updated electric + # 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(self.grid.nx, + self.grid.ny, + self.grid.nz, + config.hostinfo['ompthreads'], + config.materials['maxpoles'], + self.grid.updatecoeffsdispersive, + self.grid.ID, + self.grid.Tx, + self.grid.Ty, + self.grid.Tz, + self.grid.Ex, + self.grid.Ey, + self.grid.Ez) + + def adapt_dispersive_config(self, config): + + if config.materials['maxpoles'] > 1: + poles = 'multi' + + else: + poles = '1' + + if config.precision == 'single': + type = 'float' + + else: + type = 'double' + + if config.materials['dispersivedtype'] == config.dtypes['complex']: + dispersion = 'complex' + else: + dispersion = 'real' + + class Props(): + pass + + props = Props() + props.poles = poles + props.precision = type + props.dispersion_type = dispersion + + return props + + def set_dispersive_updates(self, props): + """Function to set dispersive update functions based on model.""" + + update_f = 'update_electric_dispersive_{}pole_{}_{}_{}' + disp_a = update_f.format(props.poles, 'A', props.precision, props.dispersion_type) + disp_b = update_f.format(props.poles, 'B', props.precision, props.dispersion_type) + + disp_a_f = getattr(import_module('gprMax.cython.fields_updates_dispersive'), disp_a) + disp_b_f = getattr(import_module('gprMax.cython.fields_updates_dispersive'), disp_b) + + self.dispersive_update_a = disp_a_f + self.dispersive_update_b = disp_b_f + + +class GPUUpdates: + pass diff --git a/gprMax/utilities.py b/gprMax/utilities.py index 945a67de..2bd597ef 100644 --- a/gprMax/utilities.py +++ b/gprMax/utilities.py @@ -1,430 +1,430 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -from contextlib import contextmanager -import codecs -import decimal as d -import os -import platform -import psutil -import re -import subprocess -from shutil import get_terminal_size -import sys -import textwrap -from time import perf_counter - -from colorama import init -from colorama import Fore -from colorama import Style -init() -import numpy as np - -from .exceptions import GeneralError - - - -def get_terminal_width(): - """Get/set width of terminal being used. - - Returns: - terminalwidth (int): Terminal width - """ - - terminalwidth = get_terminal_size()[0] - if terminalwidth == 0: - terminalwidth = 100 - - return terminalwidth - - -def logo(version): - """Print gprMax logo, version, and licencing/copyright information. - - Args: - version (str): Version number. - """ - - description = '\n=== Electromagnetic modelling software based on the Finite-Difference Time-Domain (FDTD) method' - copyright = 'Copyright (C) 2015-2019: The University of Edinburgh' - authors = 'Authors: Craig Warren and Antonis Giannopoulos' - licenseinfo1 = 'gprMax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n' - licenseinfo2 = 'gprMax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.' - licenseinfo3 = 'You should have received a copy of the GNU General Public License along with gprMax. If not, see www.gnu.org/licenses.' - - logo = """ www.gprmax.com __ __ - __ _ _ __ _ __| \/ | __ ___ __ - / _` | '_ \| '__| |\/| |/ _` \ \/ / - | (_| | |_) | | | | | | (_| |> < - \__, | .__/|_| |_| |_|\__,_/_/\_\\ - |___/|_| - v""" + version - - 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=' ')) - - -@contextmanager -def open_path_file(path_or_file): - """Accepts either a path as a string or a file object and returns a file - object (http://stackoverflow.com/a/6783680). - - Args: - path_or_file: path as a string or a file object. - - Returns: - f (object): File object. - """ - - if isinstance(path_or_file, str): - f = file_to_close = codecs.open(path_or_file, 'r', encoding='utf-8') - else: - f = path_or_file - file_to_close = None - - try: - yield f - finally: - if file_to_close: - file_to_close.close() - - -def round_value(value, decimalplaces=0): - """Rounding function. - - Args: - value (float): Number to round. - decimalplaces (int): Number of decimal places of float to represent rounded value. - - Returns: - rounded (int/float): Rounded value. - """ - - # Rounds to nearest integer (half values are rounded downwards) - if decimalplaces == 0: - rounded = int(d.Decimal(value).quantize(d.Decimal('1'), rounding=d.ROUND_HALF_DOWN)) - - # Rounds down to nearest float represented by number of decimal places - else: - precision = '1.{places}'.format(places='0' * decimalplaces) - rounded = float(d.Decimal(value).quantize(d.Decimal(precision), rounding=d.ROUND_FLOOR)) - - return rounded - - -def round32(value): - """Rounds up to nearest multiple of 32.""" - return int(32 * np.ceil(float(value) / 32)) - - -def fft_power(waveform, dt): - """Calculate a FFT of the given waveform of amplitude values; - converted to decibels and shifted so that maximum power is 0dB - - Args: - waveform (ndarray): time domain waveform - dt (float): time step - - Returns: - freqs (ndarray): frequency bins - power (ndarray): power - """ - - # Calculate magnitude of frequency spectra of waveform (ignore warning from taking a log of any zero values) - with np.errstate(divide='ignore'): - power = 10 * np.log10(np.abs(np.fft.fft(waveform))**2) - - # Replace any NaNs or Infs from zero division - power[np.invert(np.isfinite(power))] = 0 - - # Frequency bins - freqs = np.fft.fftfreq(power.size, d=dt) - - # Shift powers so that frequency with maximum power is at zero decibels - power -= np.amax(power) - - return freqs, power - - -def human_size(size, a_kilobyte_is_1024_bytes=False): - """Convert a file size to human-readable form. - - Args: - size (int): file size in bytes. - a_kilobyte_is_1024_bytes (boolean) - true for multiples of 1024, false for multiples of 1000. - - Returns: - Human-readable (string). - """ - - suffixes = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']} - - if size < 0: - raise ValueError('Number must be non-negative.') - - multiple = 1024 if a_kilobyte_is_1024_bytes else 1000 - for suffix in suffixes[multiple]: - size /= multiple - if size < multiple: - return '{:.3g}{}'.format(size, suffix) - - raise ValueError('Number is too large.') - - -def get_host_info(): - """Get information about the machine, CPU, RAM, and OS. - - Returns: - hostinfo (dict): Manufacturer and model of machine; description of CPU - type, speed, cores; RAM; name and version of operating system. - """ - - # Default to 'unknown' if any of the detection fails - manufacturer = model = cpuID = sockets = threadspercore = 'unknown' - - # Windows - if sys.platform == 'win32': - # Manufacturer/model - try: - manufacturer = subprocess.check_output("wmic csproduct get vendor", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - manufacturer = manufacturer.split('\n') - if len(manufacturer) > 1: - manufacturer = manufacturer[1] - else: - manufacturer = manufacturer[0] - model = subprocess.check_output("wmic computersystem get model", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - model = model.split('\n') - if len(model) > 1: - model = model[1] - else: - model = model[0] - except subprocess.CalledProcessError: - pass - machineID = manufacturer + ' ' + model - - # CPU information - try: - allcpuinfo = subprocess.check_output("wmic cpu get Name", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - allcpuinfo = allcpuinfo.split('\n') - sockets = 0 - for line in allcpuinfo: - if 'CPU' in line: - cpuID = line.strip() - sockets += 1 - except subprocess.CalledProcessError: - pass - - # Hyperthreading - if psutil.cpu_count(logical=False) != psutil.cpu_count(logical=True): - hyperthreading = True - else: - hyperthreading = False - - # OS version - if platform.machine().endswith('64'): - osbit = ' (64-bit)' - else: - osbit = ' (32-bit)' - osversion = 'Windows ' + platform.release() + osbit - - # Mac OS X/macOS - elif sys.platform == 'darwin': - # Manufacturer/model - manufacturer = 'Apple' - try: - model = subprocess.check_output("sysctl -n hw.model", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - except subprocess.CalledProcessError: - pass - machineID = manufacturer + ' ' + model - - # CPU information - try: - sockets = subprocess.check_output("sysctl -n hw.packages", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - sockets = int(sockets) - cpuID = subprocess.check_output("sysctl -n machdep.cpu.brand_string", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - cpuID = ' '.join(cpuID.split()) - except subprocess.CalledProcessError: - pass - - # Hyperthreading - if psutil.cpu_count(logical=False) != psutil.cpu_count(logical=True): - hyperthreading = True - else: - hyperthreading = False - - # OS version - if int(platform.mac_ver()[0].split('.')[1]) < 12: - osversion = 'Mac OS X (' + platform.mac_ver()[0] + ')' - else: - osversion = 'macOS (' + platform.mac_ver()[0] + ')' - - # Linux - elif sys.platform == 'linux': - # Manufacturer/model - try: - manufacturer = subprocess.check_output("cat /sys/class/dmi/id/sys_vendor", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - model = subprocess.check_output("cat /sys/class/dmi/id/product_name", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - except subprocess.CalledProcessError: - pass - machineID = manufacturer + ' ' + model - - # CPU information - try: - cpuIDinfo = subprocess.check_output("cat /proc/cpuinfo", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - for line in cpuIDinfo.split('\n'): - if re.search('model name', line): - cpuID = re.sub('.*model name.*:', '', line, 1).strip() - allcpuinfo = subprocess.check_output("lscpu", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() - for line in allcpuinfo.split('\n'): - if 'Socket(s)' in line: - sockets = int(line.strip()[-1]) - if 'Thread(s) per core' in line: - threadspercore = int(line.strip()[-1]) - except subprocess.CalledProcessError: - pass - - # Hyperthreading - if threadspercore == 2: - hyperthreading = True - else: - hyperthreading = False - - # OS version - osrelease = subprocess.check_output("cat /proc/sys/kernel/osrelease", shell=True).decode('utf-8').strip() - osversion = 'Linux (' + osrelease + ', ' + platform.linux_distribution()[0] + ')' - - # Dictionary of host information - hostinfo = {} - hostinfo['hostname'] = platform.node() - hostinfo['machineID'] = machineID.strip() - hostinfo['sockets'] = sockets - hostinfo['cpuID'] = cpuID - hostinfo['osversion'] = osversion - hostinfo['hyperthreading'] = hyperthreading - hostinfo['logicalcores'] = psutil.cpu_count() - try: - # Get number of physical CPU cores, i.e. avoid hyperthreading with OpenMP - hostinfo['physicalcores'] = psutil.cpu_count(logical=False) - except ValueError: - hostinfo['physicalcores'] = hostinfo['logicalcores'] - # Handle case where cpu_count returns None on some machines - if not hostinfo['physicalcores']: - hostinfo['physicalcores'] = hostinfo['logicalcores'] - hostinfo['ram'] = psutil.virtual_memory().total - hostinfo['ompthreads'] = 1 - - return hostinfo - - -class GPU(object): - """GPU information.""" - - def __init__(self, deviceID): - """ - Args: - deviceID (int): Device ID for GPU. - """ - - self.deviceID = deviceID - self.name = None - self.pcibusID = None - self.constmem = None - self.totalmem = None - # Threads per block for main field updates - self.tpb = (256, 1, 1) - # Blocks per grid for main field updates (set in grid.py) - self.bpg = None - - def get_gpu_info(self, drv): - """Set information about GPU. - - Args: - drv (object): PyCuda driver. - """ - - self.name = drv.Device(self.deviceID).name() - self.pcibusID = drv.Device(self.deviceID).pci_bus_id() - self.constmem = drv.Device(self.deviceID).total_constant_memory - self.totalmem = drv.Device(self.deviceID).total_memory() - - -def detect_check_gpus(deviceIDs): - """Get information about Nvidia GPU(s). - - Args: - deviceIDs (list): List of integers of device IDs. - - Returns: - gpus (list): Detected GPU(s) object(s). - """ - - try: - import pycuda.driver as drv - except ImportError: - raise ImportError('To use gprMax in GPU mode the pycuda package must be installed, and you must have a NVIDIA CUDA-Enabled GPU (https://developer.nvidia.com/cuda-gpus).') - drv.init() - - # Check and list any CUDA-Enabled GPUs - if drv.Device.count() == 0: - raise GeneralError('No NVIDIA CUDA-Enabled GPUs detected (https://developer.nvidia.com/cuda-gpus)') - elif 'CUDA_VISIBLE_DEVICES' in os.environ: - deviceIDsavail = os.environ.get('CUDA_VISIBLE_DEVICES') - deviceIDsavail = [int(s) for s in deviceIDsavail.split(',')] - else: - deviceIDsavail = range(drv.Device.count()) - - # If no device ID is given use default of 0 - if not deviceIDs: - deviceIDs = [0] - - # 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)) - - # Gather information about selected/detected GPUs - gpus = [] - allgpustext = [] - for ID in deviceIDsavail: - gpu = GPU(deviceID=ID) - 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))) - - return gpus, allgpustext - - -def timer(): - """Function to return the current process wide time in fractional seconds.""" - return perf_counter() - -class Printer(): - - def __init__(self, config): - self.printing = config.is_messages() - - def print(self, str): - if self.printing: - print(str) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +from contextlib import contextmanager +import codecs +import decimal as d +import os +import platform +import psutil +import re +import subprocess +from shutil import get_terminal_size +import sys +import textwrap +from time import perf_counter + +from colorama import init +from colorama import Fore +from colorama import Style +init() +import numpy as np + +from .exceptions import GeneralError + + + +def get_terminal_width(): + """Get/set width of terminal being used. + + Returns: + terminalwidth (int): Terminal width + """ + + terminalwidth = get_terminal_size()[0] + if terminalwidth == 0: + terminalwidth = 100 + + return terminalwidth + + +def logo(version): + """Print gprMax logo, version, and licencing/copyright information. + + Args: + version (str): Version number. + """ + + description = '\n=== Electromagnetic modelling software based on the Finite-Difference Time-Domain (FDTD) method' + copyright = 'Copyright (C) 2015-2019: The University of Edinburgh' + authors = 'Authors: Craig Warren and Antonis Giannopoulos' + licenseinfo1 = 'gprMax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n' + licenseinfo2 = 'gprMax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.' + licenseinfo3 = 'You should have received a copy of the GNU General Public License along with gprMax. If not, see www.gnu.org/licenses.' + + logo = """ www.gprmax.com __ __ + __ _ _ __ _ __| \/ | __ ___ __ + / _` | '_ \| '__| |\/| |/ _` \ \/ / + | (_| | |_) | | | | | | (_| |> < + \__, | .__/|_| |_| |_|\__,_/_/\_\\ + |___/|_| + v""" + version + + 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=' ')) + + +@contextmanager +def open_path_file(path_or_file): + """Accepts either a path as a string or a file object and returns a file + object (http://stackoverflow.com/a/6783680). + + Args: + path_or_file: path as a string or a file object. + + Returns: + f (object): File object. + """ + + if isinstance(path_or_file, str): + f = file_to_close = codecs.open(path_or_file, 'r', encoding='utf-8') + else: + f = path_or_file + file_to_close = None + + try: + yield f + finally: + if file_to_close: + file_to_close.close() + + +def round_value(value, decimalplaces=0): + """Rounding function. + + Args: + value (float): Number to round. + decimalplaces (int): Number of decimal places of float to represent rounded value. + + Returns: + rounded (int/float): Rounded value. + """ + + # Rounds to nearest integer (half values are rounded downwards) + if decimalplaces == 0: + rounded = int(d.Decimal(value).quantize(d.Decimal('1'), rounding=d.ROUND_HALF_DOWN)) + + # Rounds down to nearest float represented by number of decimal places + else: + precision = '1.{places}'.format(places='0' * decimalplaces) + rounded = float(d.Decimal(value).quantize(d.Decimal(precision), rounding=d.ROUND_FLOOR)) + + return rounded + + +def round32(value): + """Rounds up to nearest multiple of 32.""" + return int(32 * np.ceil(float(value) / 32)) + + +def fft_power(waveform, dt): + """Calculate a FFT of the given waveform of amplitude values; + converted to decibels and shifted so that maximum power is 0dB + + Args: + waveform (ndarray): time domain waveform + dt (float): time step + + Returns: + freqs (ndarray): frequency bins + power (ndarray): power + """ + + # Calculate magnitude of frequency spectra of waveform (ignore warning from taking a log of any zero values) + with np.errstate(divide='ignore'): + power = 10 * np.log10(np.abs(np.fft.fft(waveform))**2) + + # Replace any NaNs or Infs from zero division + power[np.invert(np.isfinite(power))] = 0 + + # Frequency bins + freqs = np.fft.fftfreq(power.size, d=dt) + + # Shift powers so that frequency with maximum power is at zero decibels + power -= np.amax(power) + + return freqs, power + + +def human_size(size, a_kilobyte_is_1024_bytes=False): + """Convert a file size to human-readable form. + + Args: + size (int): file size in bytes. + a_kilobyte_is_1024_bytes (boolean) - true for multiples of 1024, false for multiples of 1000. + + Returns: + Human-readable (string). + """ + + suffixes = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']} + + if size < 0: + raise ValueError('Number must be non-negative.') + + multiple = 1024 if a_kilobyte_is_1024_bytes else 1000 + for suffix in suffixes[multiple]: + size /= multiple + if size < multiple: + return '{:.3g}{}'.format(size, suffix) + + raise ValueError('Number is too large.') + + +def get_host_info(): + """Get information about the machine, CPU, RAM, and OS. + + Returns: + hostinfo (dict): Manufacturer and model of machine; description of CPU + type, speed, cores; RAM; name and version of operating system. + """ + + # Default to 'unknown' if any of the detection fails + manufacturer = model = cpuID = sockets = threadspercore = 'unknown' + + # Windows + if sys.platform == 'win32': + # Manufacturer/model + try: + manufacturer = subprocess.check_output("wmic csproduct get vendor", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + manufacturer = manufacturer.split('\n') + if len(manufacturer) > 1: + manufacturer = manufacturer[1] + else: + manufacturer = manufacturer[0] + model = subprocess.check_output("wmic computersystem get model", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + model = model.split('\n') + if len(model) > 1: + model = model[1] + else: + model = model[0] + except subprocess.CalledProcessError: + pass + machineID = manufacturer + ' ' + model + + # CPU information + try: + allcpuinfo = subprocess.check_output("wmic cpu get Name", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + allcpuinfo = allcpuinfo.split('\n') + sockets = 0 + for line in allcpuinfo: + if 'CPU' in line: + cpuID = line.strip() + sockets += 1 + except subprocess.CalledProcessError: + pass + + # Hyperthreading + if psutil.cpu_count(logical=False) != psutil.cpu_count(logical=True): + hyperthreading = True + else: + hyperthreading = False + + # OS version + if platform.machine().endswith('64'): + osbit = ' (64-bit)' + else: + osbit = ' (32-bit)' + osversion = 'Windows ' + platform.release() + osbit + + # Mac OS X/macOS + elif sys.platform == 'darwin': + # Manufacturer/model + manufacturer = 'Apple' + try: + model = subprocess.check_output("sysctl -n hw.model", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + except subprocess.CalledProcessError: + pass + machineID = manufacturer + ' ' + model + + # CPU information + try: + sockets = subprocess.check_output("sysctl -n hw.packages", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + sockets = int(sockets) + cpuID = subprocess.check_output("sysctl -n machdep.cpu.brand_string", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + cpuID = ' '.join(cpuID.split()) + except subprocess.CalledProcessError: + pass + + # Hyperthreading + if psutil.cpu_count(logical=False) != psutil.cpu_count(logical=True): + hyperthreading = True + else: + hyperthreading = False + + # OS version + if int(platform.mac_ver()[0].split('.')[1]) < 12: + osversion = 'Mac OS X (' + platform.mac_ver()[0] + ')' + else: + osversion = 'macOS (' + platform.mac_ver()[0] + ')' + + # Linux + elif sys.platform == 'linux': + # Manufacturer/model + try: + manufacturer = subprocess.check_output("cat /sys/class/dmi/id/sys_vendor", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + model = subprocess.check_output("cat /sys/class/dmi/id/product_name", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + except subprocess.CalledProcessError: + pass + machineID = manufacturer + ' ' + model + + # CPU information + try: + cpuIDinfo = subprocess.check_output("cat /proc/cpuinfo", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + for line in cpuIDinfo.split('\n'): + if re.search('model name', line): + cpuID = re.sub('.*model name.*:', '', line, 1).strip() + allcpuinfo = subprocess.check_output("lscpu", shell=True, stderr=subprocess.STDOUT).decode('utf-8').strip() + for line in allcpuinfo.split('\n'): + if 'Socket(s)' in line: + sockets = int(line.strip()[-1]) + if 'Thread(s) per core' in line: + threadspercore = int(line.strip()[-1]) + except subprocess.CalledProcessError: + pass + + # Hyperthreading + if threadspercore == 2: + hyperthreading = True + else: + hyperthreading = False + + # OS version + osrelease = subprocess.check_output("cat /proc/sys/kernel/osrelease", shell=True).decode('utf-8').strip() + osversion = 'Linux (' + osrelease + ', ' + platform.linux_distribution()[0] + ')' + + # Dictionary of host information + hostinfo = {} + hostinfo['hostname'] = platform.node() + hostinfo['machineID'] = machineID.strip() + hostinfo['sockets'] = sockets + hostinfo['cpuID'] = cpuID + hostinfo['osversion'] = osversion + hostinfo['hyperthreading'] = hyperthreading + hostinfo['logicalcores'] = psutil.cpu_count() + try: + # Get number of physical CPU cores, i.e. avoid hyperthreading with OpenMP + hostinfo['physicalcores'] = psutil.cpu_count(logical=False) + except ValueError: + hostinfo['physicalcores'] = hostinfo['logicalcores'] + # Handle case where cpu_count returns None on some machines + if not hostinfo['physicalcores']: + hostinfo['physicalcores'] = hostinfo['logicalcores'] + hostinfo['ram'] = psutil.virtual_memory().total + hostinfo['ompthreads'] = 1 + + return hostinfo + + +class GPU(object): + """GPU information.""" + + def __init__(self, deviceID): + """ + Args: + deviceID (int): Device ID for GPU. + """ + + self.deviceID = deviceID + self.name = None + self.pcibusID = None + self.constmem = None + self.totalmem = None + # Threads per block for main field updates + self.tpb = (256, 1, 1) + # Blocks per grid for main field updates (set in grid.py) + self.bpg = None + + def get_gpu_info(self, drv): + """Set information about GPU. + + Args: + drv (object): PyCuda driver. + """ + + self.name = drv.Device(self.deviceID).name() + self.pcibusID = drv.Device(self.deviceID).pci_bus_id() + self.constmem = drv.Device(self.deviceID).total_constant_memory + self.totalmem = drv.Device(self.deviceID).total_memory() + + +def detect_check_gpus(deviceIDs): + """Get information about Nvidia GPU(s). + + Args: + deviceIDs (list): List of integers of device IDs. + + Returns: + gpus (list): Detected GPU(s) object(s). + """ + + try: + import pycuda.driver as drv + except ImportError: + raise ImportError('To use gprMax in GPU mode the pycuda package must be installed, and you must have a NVIDIA CUDA-Enabled GPU (https://developer.nvidia.com/cuda-gpus).') + drv.init() + + # Check and list any CUDA-Enabled GPUs + if drv.Device.count() == 0: + raise GeneralError('No NVIDIA CUDA-Enabled GPUs detected (https://developer.nvidia.com/cuda-gpus)') + elif 'CUDA_VISIBLE_DEVICES' in os.environ: + deviceIDsavail = os.environ.get('CUDA_VISIBLE_DEVICES') + deviceIDsavail = [int(s) for s in deviceIDsavail.split(',')] + else: + deviceIDsavail = range(drv.Device.count()) + + # If no device ID is given use default of 0 + if not deviceIDs: + deviceIDs = [0] + + # 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)) + + # Gather information about selected/detected GPUs + gpus = [] + allgpustext = [] + for ID in deviceIDsavail: + gpu = GPU(deviceID=ID) + 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))) + + return gpus, allgpustext + + +def timer(): + """Function to return the current process wide time in fractional seconds.""" + return perf_counter() + +class Printer(): + + def __init__(self, config): + self.printing = config.is_messages() + + def print(self, str): + if self.printing: + print(str) diff --git a/setup.py b/setup.py index dbb72b8b..e4e17861 100644 --- a/setup.py +++ b/setup.py @@ -1,270 +1,270 @@ -# 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 General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . - -try: - from setuptools import setup, Extension -except ImportError: - from distutils.core import setup - from distutils.extension import Extension - -try: - import numpy as np -except ImportError: - raise ImportError('gprMax requires the NumPy package.') - -import glob -import os -import pathlib -import re -import shutil -import sys -from jinja2 import Environment, PackageLoader, 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. - """ - env = Environment( - loader=PackageLoader(__name__, 'gprMax/templates'), - ) - - template = env.get_template('fields_updates_dispersive_template') - - # Render dispersive template for different types - r = template.render( - functions=[ - # templates for Double precision and dispersive materials with - # real susceptibility functions - { - 'name_a': 'update_electric_dispersive_multipole_A_double_real', - 'name_b': 'update_electric_dispersive_multipole_B_double_real', - 'name_a_1': 'update_electric_dispersive_1pole_A_double_real', - 'name_b_1': 'update_electric_dispersive_1pole_B_double_real', - 'field_type': 'double', - 'dispersive_type': 'double' - }, - # templates for Float precision and dispersive materials with - # real susceptibility functions - { - 'name_a': 'update_electric_dispersive_multipole_A_float_real', - 'name_b': 'update_electric_dispersive_multipole_B_float_real', - 'name_a_1': 'update_electric_dispersive_1pole_A_float_real', - 'name_b_1': 'update_electric_dispersive_1pole_B_float_real', - 'field_type': 'float', - 'dispersive_type': 'float' - }, - # templates for Double precision and dispersive materials with - # complex susceptibility functions - { - 'name_a': 'update_electric_dispersive_multipole_A_double_complex', - 'name_b': 'update_electric_dispersive_multipole_B_double_complex', - 'name_a_1': 'update_electric_dispersive_1pole_A_double_complex', - 'name_b_1': 'update_electric_dispersive_1pole_B_double_complex', - 'field_type': 'double', - 'dispersive_type': 'double complex', - # c function to take real part of complex double type - 'real_part': 'creal' - }, - # templates for Float precision and dispersive materials with - # complex susceptibility functions - { - 'name_a': 'update_electric_dispersive_multipole_A_float_complex', - 'name_b': 'update_electric_dispersive_multipole_B_float_complex', - 'name_a_1': 'update_electric_dispersive_1pole_A_float_complex', - 'name_b_1': 'update_electric_dispersive_1pole_B_float_complex', - 'field_type': 'float', - 'dispersive_type': 'float complex', - # c function to take real part of complex double type - 'real_part': 'crealf' - }] - ) - - with open('gprMax/cython/fields_updates_dispersive.pyx', 'w') as f: - f.write(r) - -# Generate Cython file for dispersive materials update functions -build_dispersive_material_templates() - -# Importing _version__.py before building can cause issues. -with open('gprMax/_version.py', 'r') as fd: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', - fd.read(), re.MULTILINE).group(1) - -# Parse package name from init file. Importing __init__.py / gprMax will break as gprMax depends on compiled .pyx files. -with open('gprMax/__init__.py', 'r') as fd: - packagename = re.search(r'^__name__\s*=\s*[\'"]([^\'"]*)[\'"]', - fd.read(), re.MULTILINE).group(1) - -packages = [packagename, 'tests', 'tools', 'user_libs'] - -# Parse long_description from README.rst file. -with open('README.rst','r') as fd: - long_description = fd.read() - -# Python version -if sys.version_info[:2] < (3, 6): - sys.exit('\nExited: Requires Python 3.6 or newer!\n') - -# Process 'build' command line argument -if 'build' in sys.argv: - print("Running 'build_ext --inplace'") - sys.argv.remove('build') - sys.argv.append('build_ext') - sys.argv.append('--inplace') - -# Process '--no-cython' command line argument - either Cythonize or just compile the .c files -if '--no-cython' in sys.argv: - USE_CYTHON = False - sys.argv.remove('--no-cython') -else: - USE_CYTHON = True - -# Build a list of all the files that need to be Cythonized looking in gprMax directory -cythonfiles = [] -for root, dirs, files in os.walk(os.path.join(os.getcwd(), packagename), topdown=True): - for file in files: - if file.endswith('.pyx'): - cythonfiles.append(os.path.relpath(os.path.join(root, file))) - -# Process 'cleanall' command line argument - cleanup Cython files -if 'cleanall' in sys.argv: - USE_CYTHON = False - for file in cythonfiles: - filebase = os.path.splitext(file)[0] - # Remove Cython C files - if os.path.isfile(filebase + '.c'): - try: - os.remove(filebase + '.c') - print('Removed: {}'.format(filebase + '.c')) - except OSError: - print('Could not remove: {}'.format(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))) - except OSError: - print('Could not remove: {}'.format(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)) - # Now do a normal clean - sys.argv[1] = 'clean' # this is what distutils understands - -# Set compiler options -# Windows -if sys.platform == 'win32': - compile_args = ['/O2', '/openmp', '/w'] # No static linking as no static version of OpenMP library; /w disables warnings - linker_args = [] - extra_objects = [] -# macOS - needs gcc (usually via HomeBrew) because the default compiler LLVM (clang) does not support OpenMP -# - with gcc -fopenmp option implies -pthread -elif sys.platform == 'darwin': - gccpath = glob.glob('/usr/local/bin/gcc-[4-9]*') - if gccpath: - # Use newest gcc found - os.environ['CC'] = gccpath[-1].split(os.sep)[-1] - rpath = '/usr/local/opt/gcc/lib/gcc/' + gccpath[-1].split(os.sep)[-1][-1] + '/' - else: - raise('Cannot find gcc 4-9 in /usr/local/bin. gprMax requires gcc to be installed - easily done through the Homebrew package manager (http://brew.sh). Note: gcc with OpenMP support is required.') - compile_args = ['-O3', '-w', '-fopenmp', '-march=native'] # Sometimes worth testing with '-fstrict-aliasing', '-fno-common' - linker_args = ['-fopenmp', '-Wl,-rpath,' + rpath] - extra_objects = [] -# Linux -elif sys.platform == 'linux': - compile_args = ['-O3', '-w', '-fopenmp', '-march=native'] - linker_args = ['-fopenmp'] - extra_objects = [] - -# Build a list of all the extensions -extensions = [] -for file in cythonfiles: - tmp = os.path.splitext(file) - if USE_CYTHON: - fileext = tmp[1] - else: - fileext = '.c' - extension = Extension(tmp[0].replace(os.sep, '.'), - [tmp[0] + fileext], - language='c', - include_dirs=[np.get_include()], - extra_compile_args=compile_args, - extra_link_args=linker_args, - extra_objects=extra_objects) - extensions.append(extension) - -# Cythonize (build .c files) -if USE_CYTHON: - from Cython.Build import cythonize - extensions = cythonize(extensions, - compiler_directives={ - 'boundscheck': False, - 'wraparound': False, - 'initializedcheck': False, - 'embedsignature': True, - 'language_level': 3 - }, - annotate=False) - -# SetupTools Required to make package -import setuptools - -setup(name=packagename, - version=version, - author='Craig Warren and Antonis Giannopoulos', - url='http://www.gprmax.com', - description='Electromagnetic Modelling Software based on the Finite-Difference Time-Domain (FDTD) method', - long_description=long_description, - long_description_content_type="text/x-rst", - license='GPLv3+', - classifiers=[ - 'Environment :: Console', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Operating System :: MacOS', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Cython', - 'Programming Language :: Python :: 3', - 'Topic :: Scientific/Engineering' - ], - #requirements - python_requires=">3.6", - install_requires=[ - "colorama", - "cython", - "h5py", - "jupyter", - "matplotlib", - "numpy", - "psutil", - "scipy", - "terminaltables", - "tqdm", - ], - ext_modules=extensions, - packages=packages, - include_package_data=True, - include_dirs=[np.get_include()]) +# 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 General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . + +try: + from setuptools import setup, Extension +except ImportError: + from distutils.core import setup + from distutils.extension import Extension + +try: + import numpy as np +except ImportError: + raise ImportError('gprMax requires the NumPy package.') + +import glob +import os +import pathlib +import re +import shutil +import sys +from jinja2 import Environment, PackageLoader, 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. + """ + env = Environment( + loader=PackageLoader(__name__, 'gprMax/templates'), + ) + + template = env.get_template('fields_updates_dispersive_template') + + # Render dispersive template for different types + r = template.render( + functions=[ + # templates for Double precision and dispersive materials with + # real susceptibility functions + { + 'name_a': 'update_electric_dispersive_multipole_A_double_real', + 'name_b': 'update_electric_dispersive_multipole_B_double_real', + 'name_a_1': 'update_electric_dispersive_1pole_A_double_real', + 'name_b_1': 'update_electric_dispersive_1pole_B_double_real', + 'field_type': 'double', + 'dispersive_type': 'double' + }, + # templates for Float precision and dispersive materials with + # real susceptibility functions + { + 'name_a': 'update_electric_dispersive_multipole_A_float_real', + 'name_b': 'update_electric_dispersive_multipole_B_float_real', + 'name_a_1': 'update_electric_dispersive_1pole_A_float_real', + 'name_b_1': 'update_electric_dispersive_1pole_B_float_real', + 'field_type': 'float', + 'dispersive_type': 'float' + }, + # templates for Double precision and dispersive materials with + # complex susceptibility functions + { + 'name_a': 'update_electric_dispersive_multipole_A_double_complex', + 'name_b': 'update_electric_dispersive_multipole_B_double_complex', + 'name_a_1': 'update_electric_dispersive_1pole_A_double_complex', + 'name_b_1': 'update_electric_dispersive_1pole_B_double_complex', + 'field_type': 'double', + 'dispersive_type': 'double complex', + # c function to take real part of complex double type + 'real_part': 'creal' + }, + # templates for Float precision and dispersive materials with + # complex susceptibility functions + { + 'name_a': 'update_electric_dispersive_multipole_A_float_complex', + 'name_b': 'update_electric_dispersive_multipole_B_float_complex', + 'name_a_1': 'update_electric_dispersive_1pole_A_float_complex', + 'name_b_1': 'update_electric_dispersive_1pole_B_float_complex', + 'field_type': 'float', + 'dispersive_type': 'float complex', + # c function to take real part of complex double type + 'real_part': 'crealf' + }] + ) + + with open('gprMax/cython/fields_updates_dispersive.pyx', 'w') as f: + f.write(r) + +# Generate Cython file for dispersive materials update functions +build_dispersive_material_templates() + +# Importing _version__.py before building can cause issues. +with open('gprMax/_version.py', 'r') as fd: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', + fd.read(), re.MULTILINE).group(1) + +# Parse package name from init file. Importing __init__.py / gprMax will break as gprMax depends on compiled .pyx files. +with open('gprMax/__init__.py', 'r') as fd: + packagename = re.search(r'^__name__\s*=\s*[\'"]([^\'"]*)[\'"]', + fd.read(), re.MULTILINE).group(1) + +packages = [packagename, 'tests', 'tools', 'user_libs'] + +# Parse long_description from README.rst file. +with open('README.rst','r') as fd: + long_description = fd.read() + +# Python version +if sys.version_info[:2] < (3, 6): + sys.exit('\nExited: Requires Python 3.6 or newer!\n') + +# Process 'build' command line argument +if 'build' in sys.argv: + print("Running 'build_ext --inplace'") + sys.argv.remove('build') + sys.argv.append('build_ext') + sys.argv.append('--inplace') + +# Process '--no-cython' command line argument - either Cythonize or just compile the .c files +if '--no-cython' in sys.argv: + USE_CYTHON = False + sys.argv.remove('--no-cython') +else: + USE_CYTHON = True + +# Build a list of all the files that need to be Cythonized looking in gprMax directory +cythonfiles = [] +for root, dirs, files in os.walk(os.path.join(os.getcwd(), packagename), topdown=True): + for file in files: + if file.endswith('.pyx'): + cythonfiles.append(os.path.relpath(os.path.join(root, file))) + +# Process 'cleanall' command line argument - cleanup Cython files +if 'cleanall' in sys.argv: + USE_CYTHON = False + for file in cythonfiles: + filebase = os.path.splitext(file)[0] + # Remove Cython C files + if os.path.isfile(filebase + '.c'): + try: + os.remove(filebase + '.c') + print('Removed: {}'.format(filebase + '.c')) + except OSError: + print('Could not remove: {}'.format(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))) + except OSError: + print('Could not remove: {}'.format(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)) + # Now do a normal clean + sys.argv[1] = 'clean' # this is what distutils understands + +# Set compiler options +# Windows +if sys.platform == 'win32': + compile_args = ['/O2', '/openmp', '/w'] # No static linking as no static version of OpenMP library; /w disables warnings + linker_args = [] + extra_objects = [] +# macOS - needs gcc (usually via HomeBrew) because the default compiler LLVM (clang) does not support OpenMP +# - with gcc -fopenmp option implies -pthread +elif sys.platform == 'darwin': + gccpath = glob.glob('/usr/local/bin/gcc-[4-9]*') + if gccpath: + # Use newest gcc found + os.environ['CC'] = gccpath[-1].split(os.sep)[-1] + rpath = '/usr/local/opt/gcc/lib/gcc/' + gccpath[-1].split(os.sep)[-1][-1] + '/' + else: + raise('Cannot find gcc 4-9 in /usr/local/bin. gprMax requires gcc to be installed - easily done through the Homebrew package manager (http://brew.sh). Note: gcc with OpenMP support is required.') + compile_args = ['-O3', '-w', '-fopenmp', '-march=native'] # Sometimes worth testing with '-fstrict-aliasing', '-fno-common' + linker_args = ['-fopenmp', '-Wl,-rpath,' + rpath] + extra_objects = [] +# Linux +elif sys.platform == 'linux': + compile_args = ['-O3', '-w', '-fopenmp', '-march=native'] + linker_args = ['-fopenmp'] + extra_objects = [] + +# Build a list of all the extensions +extensions = [] +for file in cythonfiles: + tmp = os.path.splitext(file) + if USE_CYTHON: + fileext = tmp[1] + else: + fileext = '.c' + extension = Extension(tmp[0].replace(os.sep, '.'), + [tmp[0] + fileext], + language='c', + include_dirs=[np.get_include()], + extra_compile_args=compile_args, + extra_link_args=linker_args, + extra_objects=extra_objects) + extensions.append(extension) + +# Cythonize (build .c files) +if USE_CYTHON: + from Cython.Build import cythonize + extensions = cythonize(extensions, + compiler_directives={ + 'boundscheck': False, + 'wraparound': False, + 'initializedcheck': False, + 'embedsignature': True, + 'language_level': 3 + }, + annotate=False) + +# SetupTools Required to make package +import setuptools + +setup(name=packagename, + version=version, + author='Craig Warren and Antonis Giannopoulos', + url='http://www.gprmax.com', + description='Electromagnetic Modelling Software based on the Finite-Difference Time-Domain (FDTD) method', + long_description=long_description, + long_description_content_type="text/x-rst", + license='GPLv3+', + classifiers=[ + 'Environment :: Console', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Operating System :: MacOS', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Cython', + 'Programming Language :: Python :: 3', + 'Topic :: Scientific/Engineering' + ], + #requirements + python_requires=">3.6", + install_requires=[ + "colorama", + "cython", + "h5py", + "jupyter", + "matplotlib", + "numpy", + "psutil", + "scipy", + "terminaltables", + "tqdm", + ], + ext_modules=extensions, + packages=packages, + include_package_data=True, + include_dirs=[np.get_include()])