From b5c6045eafefc72eefa666641782e84390657170 Mon Sep 17 00:00:00 2001 From: nmannall Date: Fri, 6 Dec 2024 16:14:19 +0000 Subject: [PATCH 01/20] Add new abstract classes for UserObjects --- gprMax/user_objects/rotatable.py | 43 ++++++++++ gprMax/user_objects/user_objects.py | 127 ++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 gprMax/user_objects/rotatable.py create mode 100644 gprMax/user_objects/user_objects.py diff --git a/gprMax/user_objects/rotatable.py b/gprMax/user_objects/rotatable.py new file mode 100644 index 00000000..d29f689e --- /dev/null +++ b/gprMax/user_objects/rotatable.py @@ -0,0 +1,43 @@ +from abc import ABC, abstractmethod +from typing import Optional, Tuple + + +class Rotatable(ABC): + """Stores parameters and defines an interface for rotatable objects. + + Attributes: + axis (str): Defines the axis about which to perform the + rotation. Must have value "x", "y", or "z". Default x. + angle (int): Specifies the angle of rotation (degrees). + Default 0. + origin (tuple | None): Optional point about which to perform the + rotation (x, y, z). Default None. + do_rotate (bool): True if the object should be rotated. False + otherwise. Default False. + """ + + def __init__(self): + self.axis = "x" + self.angle = 0 + self.origin = None + self.do_rotate = False + + def rotate(self, axis: str, angle: int, origin: Optional[Tuple[float, float, float]] = None): + """Sets parameters for rotation. + + Args: + axis: Defines the axis about which to perform the rotation. + Must have value "x", "y", or "z". + angle: Specifies the angle of rotation (degrees). + origin: Optional point about which to perform the rotation + (x, y, z). Default None. + """ + self.axis = axis + self.angle = angle + self.origin = origin + self.do_rotate = True + + @abstractmethod + def _do_rotate(self): + """Performs the rotation.""" + pass diff --git a/gprMax/user_objects/user_objects.py b/gprMax/user_objects/user_objects.py new file mode 100644 index 00000000..7bddeb02 --- /dev/null +++ b/gprMax/user_objects/user_objects.py @@ -0,0 +1,127 @@ +from abc import ABC, abstractmethod +from typing import List, Union + +from gprMax import config +from gprMax.grid.fdtd_grid import FDTDGrid +from gprMax.grid.mpi_grid import MPIGrid +from gprMax.model import Model +from gprMax.subgrids.grid import SubGridBaseGrid +from gprMax.user_inputs import MainGridUserInput, SubgridUserInput + + +class UserObject(ABC): + """User defined object. + + Attributes: + order (int): Specifies the order user objects should be + constructed in. + hash (str): gprMax hash command used to create the user object + in an input file. + kwargs (dict): Keyword arguments used to construct the user + object. + autotranslate (bool): TODO + is_single_use (bool): True if the object can only appear once in a + given model. False otherwise. Default True. + is_geometry_object (bool): True if the object adds geometry to the + model. False otherwise. Default False. + """ + + @property + @abstractmethod + def order(self) -> int: + pass + + @property + @abstractmethod + def hash(self) -> str: + pass + + @property + def is_single_use(self) -> bool: + return True + + @property + def is_geometry_object(self) -> bool: + return False + + def __init__(self, **kwargs) -> None: + self.kwargs = kwargs + self.autotranslate = True + + def __str__(self) -> str: + """Readable user object as per hash commands.""" + args: List[str] = [] + for value in self.kwargs.values(): + if isinstance(value, (tuple, list)): + for element in value: + args.append(str(element)) + else: + args.append(str(value)) + + return f"{self.hash}: {' '.join(args)}" + + def _params_str(self) -> str: + """Readable string of parameters given to object.""" + return f"{self.hash}: {str(self.kwargs)}" + + def _create_uip(self, grid: FDTDGrid) -> Union[SubgridUserInput, MainGridUserInput]: + """Returns a point checker class based on the grid supplied. + + Args: + grid: Grid to get a UserInput object for. + + Returns: + uip: UserInput object for the grid provided. + """ + + # If autotranslate is set as True globally, local object + # configuration trumps. I.e. User can turn off autotranslate for + # specific objects. + if ( + isinstance(grid, SubGridBaseGrid) + and config.sim_config.args.autotranslate + and self.autotranslate + ): + return SubgridUserInput(grid) + else: + return MainGridUserInput(grid) + + +class MultiUserObject(UserObject): + """User defined object that can occur multiple times.""" + + @property + def is_single_use(self) -> bool: + return False + + +class ModelUserObject(UserObject): + """User defined object to add to the model.""" + + @abstractmethod + def build(self, model: Model): + pass + + +class GridUserObject(MultiUserObject): + """User defined object to add to a grid.""" + + @abstractmethod + def build(self, grid: FDTDGrid): + pass + + +class OutputUserObject(MultiUserObject): + """User defined object that controls the output of data.""" + + @abstractmethod + def build(self, model: Model, grid: FDTDGrid): + pass + + +class GeometryUserObject(GridUserObject): + """User defined object that adds geometry to a grid.""" + + @property + def is_geometry_object(self) -> bool: + return True From 0869010fc68caa6f6aac51eff6352782b0e81bf9 Mon Sep 17 00:00:00 2001 From: nmannall Date: Mon, 9 Dec 2024 17:27:59 +0000 Subject: [PATCH 02/20] Move cmds_singleuse.py to new user_objects folder --- gprMax/__init__.py | 8 +++---- gprMax/hash_cmds_singleuse.py | 26 +++++++++++++++++---- gprMax/scene.py | 2 +- gprMax/{ => user_objects}/cmds_singleuse.py | 4 ++-- 4 files changed, 28 insertions(+), 12 deletions(-) rename gprMax/{ => user_objects}/cmds_singleuse.py (96%) diff --git a/gprMax/__init__.py b/gprMax/__init__.py index b97fa2dc..28dfb434 100644 --- a/gprMax/__init__.py +++ b/gprMax/__init__.py @@ -46,7 +46,10 @@ from .cmds_multiuse import ( VoltageSource, Waveform, ) -from .cmds_singleuse import ( +from .gprMax import run as run +from .scene import Scene +from .subgrids.user_objects import SubGridHSG +from .user_objects.cmds_singleuse import ( Discretisation, Domain, OMPThreads, @@ -58,8 +61,5 @@ from .cmds_singleuse import ( TimeWindow, Title, ) -from .gprMax import run as run -from .scene import Scene -from .subgrids.user_objects import SubGridHSG __name__ = "gprMax" diff --git a/gprMax/hash_cmds_singleuse.py b/gprMax/hash_cmds_singleuse.py index 2ff9eaf5..08b57b97 100644 --- a/gprMax/hash_cmds_singleuse.py +++ b/gprMax/hash_cmds_singleuse.py @@ -18,9 +18,18 @@ import logging -from .cmds_singleuse import (Discretisation, Domain, OMPThreads, OutputDir, - PMLProps, RxSteps, SrcSteps, - TimeStepStabilityFactor, TimeWindow, Title) +from .user_objects.cmds_singleuse import ( + Discretisation, + Domain, + OMPThreads, + OutputDir, + PMLProps, + RxSteps, + SrcSteps, + TimeStepStabilityFactor, + TimeWindow, + Title, +) logger = logging.getLogger(__name__) @@ -54,7 +63,9 @@ def process_singlecmds(singlecmds): if singlecmds[cmd] is not None: tmp = tuple(int(x) for x in singlecmds[cmd].split()) if len(tmp) != 1: - logger.exception(f"{cmd} requires exactly one parameter to specify the number of CPU OpenMP threads to use") + logger.exception( + f"{cmd} requires exactly one parameter to specify the number of CPU OpenMP threads to use" + ) raise ValueError omp_threads = OMPThreads(n=tmp[0]) @@ -144,7 +155,12 @@ def process_singlecmds(singlecmds): pml_props = PMLProps(thickness=int(tmp[0])) else: pml_props = PMLProps( - x0=int(tmp[0]), y0=int(tmp[1]), z0=int(tmp[2]), xmax=int(tmp[3]), ymax=int(tmp[4]), zmax=int(tmp[5]) + x0=int(tmp[0]), + y0=int(tmp[1]), + z0=int(tmp[2]), + xmax=int(tmp[3]), + ymax=int(tmp[4]), + zmax=int(tmp[5]), ) scene_objects.append(pml_props) diff --git a/gprMax/scene.py b/gprMax/scene.py index 5ca94d95..ee81af3a 100644 --- a/gprMax/scene.py +++ b/gprMax/scene.py @@ -25,13 +25,13 @@ from gprMax.cmds_geometry.add_surface_water import AddSurfaceWater from gprMax.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.cmds_geometry.fractal_box import FractalBox from gprMax.cmds_multiuse import UserObjectMulti -from gprMax.cmds_singleuse import Discretisation, Domain, TimeWindow, UserObjectSingle from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_built_in_materials from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.subgrids.user_objects import SubGridBase as SubGridUserBase from gprMax.user_inputs import MainGridUserInput, SubgridUserInput +from gprMax.user_objects.cmds_singleuse import Discretisation, Domain, TimeWindow, UserObjectSingle logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_singleuse.py b/gprMax/user_objects/cmds_singleuse.py similarity index 96% rename from gprMax/cmds_singleuse.py rename to gprMax/user_objects/cmds_singleuse.py index e39bdf29..31611578 100644 --- a/gprMax/cmds_singleuse.py +++ b/gprMax/user_objects/cmds_singleuse.py @@ -25,8 +25,8 @@ from gprMax.grid.mpi_grid import MPIGrid from gprMax.model import Model from gprMax.user_inputs import MainGridUserInput -from .pml import PML -from .utilities.host_info import set_omp_threads +from ..pml import PML +from ..utilities.host_info import set_omp_threads logger = logging.getLogger(__name__) From f9b73406ca453250222635694a2895d978c4271f Mon Sep 17 00:00:00 2001 From: nmannall Date: Mon, 9 Dec 2024 17:33:52 +0000 Subject: [PATCH 03/20] Refactor single use commands --- gprMax/grid/fdtd_grid.py | 12 +- gprMax/model.py | 4 +- gprMax/user_objects/cmds_singleuse.py | 967 +++++++++++++++----------- gprMax/user_objects/user_objects.py | 5 + 4 files changed, 571 insertions(+), 417 deletions(-) diff --git a/gprMax/grid/fdtd_grid.py b/gprMax/grid/fdtd_grid.py index 60004e7f..de8bea10 100644 --- a/gprMax/grid/fdtd_grid.py +++ b/gprMax/grid/fdtd_grid.py @@ -362,7 +362,7 @@ class FDTDGrid: logger.info(f"Materials [{self.name}]:\n{materialstable.table}\n") def _update_positions( - self, items: Iterable[Union[Source, Rx]], step_size: List[int], step_number: int + self, items: Iterable[Union[Source, Rx]], step_size: npt.NDArray[np.int32], step_number: int ) -> None: """Update the grid positions of the provided items. @@ -387,11 +387,11 @@ class FDTDGrid: or item.zcoord + step_size[2] * config.sim_config.model_end > self.nz ): raise ValueError - item.xcoord = item.xcoordorigin + step_number * step_size[0] - item.ycoord = item.ycoordorigin + step_number * step_size[1] - item.zcoord = item.zcoordorigin + step_number * step_size[2] + item.coord = item.coordorigin + step_number * step_size - def update_simple_source_positions(self, step_size: List[int], step: int = 0) -> None: + def update_simple_source_positions( + self, step_size: npt.NDArray[np.int32], step: int = 0 + ) -> None: """Update the positions of sources in the grid. Move hertzian dipole and magnetic dipole sources. Transmission @@ -414,7 +414,7 @@ class FDTDGrid: logger.exception("Source(s) will be stepped to a position outside the domain.") raise ValueError from e - def update_receiver_positions(self, step_size: List[int], step: int = 0) -> None: + def update_receiver_positions(self, step_size: npt.NDArray[np.int32], step: int = 0) -> None: """Update the positions of receivers in the grid. Args: diff --git a/gprMax/model.py b/gprMax/model.py index 82add10c..18a837ba 100644 --- a/gprMax/model.py +++ b/gprMax/model.py @@ -59,8 +59,8 @@ class Model: self.iterations = 0 # Total number of iterations self.timewindow = 0.0 - self.srcsteps: List[int] = [0, 0, 0] - self.rxsteps: List[int] = [0, 0, 0] + self.srcsteps = np.zeros(3, dtype=np.int32) + self.rxsteps = np.zeros(3, dtype=np.int32) self.G = self._create_grid() self.subgrids: List[SubGridBaseGrid] = [] diff --git a/gprMax/user_objects/cmds_singleuse.py b/gprMax/user_objects/cmds_singleuse.py index 31611578..edc0fcce 100644 --- a/gprMax/user_objects/cmds_singleuse.py +++ b/gprMax/user_objects/cmds_singleuse.py @@ -1,409 +1,558 @@ -# Copyright (C) 2015-2024: The University of Edinburgh, United Kingdom -# Authors: Craig Warren, Antonis Giannopoulos, and John Hartley -# -# This file is part of gprMax. -# -# gprMax is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# gprMax is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with gprMax. If not, see . -import logging -from abc import ABC, abstractmethod - -import numpy as np - -import gprMax.config as config -from gprMax.grid.mpi_grid import MPIGrid -from gprMax.model import Model -from gprMax.user_inputs import MainGridUserInput - -from ..pml import PML -from ..utilities.host_info import set_omp_threads - -logger = logging.getLogger(__name__) - - -class Properties: - pass - - -class UserObjectSingle(ABC): - """Object that can only occur a single time in a model.""" - - def __init__(self, **kwargs): - # Each single command has an order to specify the order in which - # the commands are constructed, e.g. discretisation must be - # created before the domain - self.order = 0 - self.kwargs = kwargs - self.props = Properties() - self.autotranslate = True - - for k, v in kwargs.items(): - setattr(self.props, k, v) - - @abstractmethod - def build(self, model: Model, uip: MainGridUserInput): - pass - - # TODO: Check if this is actually needed - def rotate(self, axis, angle, origin=None): - pass - - -class Title(UserObjectSingle): - """Includes a title for your model. - - Attributes: - name: string for model title. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 1 - - def build(self, model, uip): - try: - title = self.kwargs["name"] - model.title = title - logger.info(f"Model title: {model.title}") - except KeyError: - pass - - -class Discretisation(UserObjectSingle): - """Specifies the discretization of space in the x, y, and z directions. - - Attributes: - p1: tuple of floats to specify spatial discretisation in x, y, z direction. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 2 - - def build(self, model, uip): - try: - model.dl = np.array(self.kwargs["p1"], dtype=np.float64) - except KeyError: - logger.exception(f"{self.__str__()} discretisation requires a point") - raise - - if model.dl[0] <= 0: - logger.exception( - f"{self.__str__()} discretisation requires the " - f"x-direction spatial step to be greater than zero" - ) - raise ValueError - if model.dl[1] <= 0: - logger.exception( - f"{self.__str__()} discretisation requires the " - f"y-direction spatial step to be greater than zero" - ) - raise ValueError - if model.dl[2] <= 0: - logger.exception( - f"{self.__str__()} discretisation requires the " - f"z-direction spatial step to be greater than zero" - ) - raise ValueError - - logger.info(f"Spatial discretisation: {model.dl[0]:g} x {model.dl[1]:g} x {model.dl[2]:g}m") - - -class Domain(UserObjectSingle): - """Specifies the size of the model. - - Attributes: - p1: tuple of floats specifying extent of model domain (x, y, z). - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 3 - - def build(self, model, uip): - try: - model.nx, model.ny, model.nz = uip.discretise_point(self.kwargs["p1"]) - # TODO: Remove when distribute full build for MPI - if isinstance(model.G, MPIGrid): - model.G.nx = model.nx - model.G.ny = model.ny - model.G.nz = model.nz - - except KeyError: - logger.exception(f"{self.__str__()} please specify a point") - raise - - if model.nx == 0 or model.ny == 0 or model.nz == 0: - logger.exception(f"{self.__str__()} requires at least one cell in every dimension") - raise ValueError - - logger.info( - f"Domain size: {self.kwargs['p1'][0]:g} x {self.kwargs['p1'][1]:g} x " - + f"{self.kwargs['p1'][2]:g}m ({model.nx:d} x {model.ny:d} x {model.nz:d} = " - + f"{(model.nx * model.ny * model.nz):g} cells)" - ) - - # Calculate time step at CFL limit; switch off appropriate PMLs for 2D - G = model.G - if model.nx == 1: - config.get_model_config().mode = "2D TMx" - G.pmls["thickness"]["x0"] = 0 - G.pmls["thickness"]["xmax"] = 0 - elif model.ny == 1: - config.get_model_config().mode = "2D TMy" - G.pmls["thickness"]["y0"] = 0 - G.pmls["thickness"]["ymax"] = 0 - elif model.nz == 1: - config.get_model_config().mode = "2D TMz" - G.pmls["thickness"]["z0"] = 0 - G.pmls["thickness"]["zmax"] = 0 - else: - config.get_model_config().mode = "3D" - G.calculate_dt() - - logger.info(f"Mode: {config.get_model_config().mode}") - - # Sub-grids cannot be used with 2D models. There would typically be - # minimal performance benefit with sub-gridding and 2D models. - if "2D" in config.get_model_config().mode and config.sim_config.general["subgrid"]: - logger.exception("Sub-gridding cannot be used with 2D models") - raise ValueError - - logger.info(f"Time step (at CFL limit): {G.dt:g} secs") - - -class TimeStepStabilityFactor(UserObjectSingle): - """Factor by which to reduce the time step from the CFL limit. - - Attributes: - f: float for factor to multiply time step. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 4 - - def build(self, model, uip): - try: - f = self.kwargs["f"] - except KeyError: - logger.exception(f"{self.__str__()} requires exactly one parameter") - raise - - if f <= 0 or f > 1: - logger.exception( - f"{self.__str__()} requires the value of the time " - f"step stability factor to be between zero and one" - ) - raise ValueError - - model.dt_mod = f - model.dt *= model.dt_mod - - logger.info(f"Time step (modified): {model.dt:g} secs") - - -class TimeWindow(UserObjectSingle): - """Specifies the total required simulated time. - - Attributes: - time: float of required simulated time in seconds. - iterations: int of required number of iterations. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 5 - - def build(self, model, 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"]) - model.timewindow = (iterations - 1) * model.dt - model.iterations = iterations - except KeyError: - pass - - try: - tmp = float(self.kwargs["time"]) - if tmp > 0: - model.timewindow = tmp - model.iterations = int(np.ceil(tmp / model.dt)) + 1 - else: - logger.exception(self.__str__() + " must have a value greater than zero") - raise ValueError - except KeyError: - pass - - if not model.timewindow: - logger.exception(self.__str__() + " specify a time or number of iterations") - raise ValueError - - logger.info(f"Time window: {model.timewindow:g} secs ({model.iterations} iterations)") - - -class OMPThreads(UserObjectSingle): - """Controls how many OpenMP threads (usually the number of physical CPU - cores available) are used when running the model. - - Attributes: - n: int for number of threads. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 6 - - def build(self, model, uip): - try: - n = self.kwargs["n"] - except KeyError: - logger.exception( - f"{self.__str__()} requires exactly one parameter " - f"to specify the number of CPU OpenMP threads to use" - ) - raise - if n < 1: - logger.exception( - f"{self.__str__()} requires the value to be an " f"integer not less than one" - ) - raise ValueError - - config.get_model_config().ompthreads = set_omp_threads(n) - - -class PMLProps(UserObjectSingle): - """Specifies the formulation used and thickness (number of cells) of PML - that are used on the six sides of the model domain. Current options are - to use the Higher Order RIPML (HORIPML) - https://doi.org/10.1109/TAP.2011.2180344, - or Multipole RIPML (MRIPML) - https://doi.org/10.1109/TAP.2018.2823864. - - Attributes: - formulation: string specifying formulation to be used for all PMLs - either 'HORIPML' or 'MRIPML'. - thickness or x0, y0, z0, xmax, ymax, zmax: ints for thickness of PML - on all 6 sides or individual - sides of the model domain. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 7 - - def build(self, model, uip): - G = model.G - try: - G.pmls["formulation"] = self.kwargs["formulation"] - if G.pmls["formulation"] not in PML.formulations: - logger.exception( - self.__str__() - + f" requires the value to be " - + f"one of {' '.join(PML.formulations)}" - ) - except KeyError: - pass - - try: - thickness = self.kwargs["thickness"] - for key in G.pmls["thickness"].keys(): - G.pmls["thickness"][key] = int(thickness) - - except KeyError: - try: - G.pmls["thickness"]["x0"] = int(self.kwargs["x0"]) - G.pmls["thickness"]["y0"] = int(self.kwargs["y0"]) - G.pmls["thickness"]["z0"] = int(self.kwargs["z0"]) - G.pmls["thickness"]["xmax"] = int(self.kwargs["xmax"]) - G.pmls["thickness"]["ymax"] = int(self.kwargs["ymax"]) - G.pmls["thickness"]["zmax"] = int(self.kwargs["zmax"]) - except KeyError: - logger.exception(f"{self.__str__()} requires either one or six parameter(s)") - raise - - if ( - 2 * G.pmls["thickness"]["x0"] >= G.nx - or 2 * G.pmls["thickness"]["y0"] >= G.ny - or 2 * G.pmls["thickness"]["z0"] >= G.nz - or 2 * G.pmls["thickness"]["xmax"] >= G.nx - or 2 * G.pmls["thickness"]["ymax"] >= G.ny - or 2 * G.pmls["thickness"]["zmax"] >= G.nz - ): - logger.exception(f"{self.__str__()} has too many cells for the domain size") - raise ValueError - - -class SrcSteps(UserObjectSingle): - """Moves the location of all simple sources. - - Attributes: - p1: tuple of float increments (x,y,z) to move all simple sources. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 8 - - def build(self, model, uip): - try: - model.srcsteps = uip.discretise_point(self.kwargs["p1"]) - except KeyError: - logger.exception(f"{self.__str__()} requires exactly three parameters") - raise - - logger.info( - f"Simple sources will step {model.srcsteps[0] * model.dx:g}m, " - f"{model.srcsteps[1] * model.dy:g}m, {model.srcsteps[2] * model.dz:g}m " - "for each model run." - ) - - -class RxSteps(UserObjectSingle): - """Moves the location of all receivers. - - Attributes: - p1: tuple of float increments (x,y,z) to move all receivers. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 9 - - def build(self, model, uip): - try: - model.rxsteps = uip.discretise_point(self.kwargs["p1"]) - except KeyError: - logger.exception(f"{self.__str__()} requires exactly three parameters") - raise - - logger.info( - f"All receivers will step {model.rxsteps[0] * model.dx:g}m, " - f"{model.rxsteps[1] * model.dy:g}m, {model.rxsteps[2] * model.dz:g}m " - "for each model run." - ) - - -class OutputDir(UserObjectSingle): - """Controls the directory where output file(s) will be stored. - - Attributes: - dir: string of file path to directory. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 10 - - def build(self, grid, uip): - config.get_model_config().set_output_file_path(self.kwargs["dir"]) +# Copyright (C) 2015-2024: The University of Edinburgh, United Kingdom +# Authors: Craig Warren, Antonis Giannopoulos, and John Hartley +# +# This file is part of gprMax. +# +# gprMax is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# gprMax is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with gprMax. If not, see . +import logging +from typing import Optional, Tuple, Union + +import numpy as np +import numpy.typing as npt + +from gprMax import config +from gprMax.grid.mpi_grid import MPIGrid +from gprMax.model import Model +from gprMax.pml import PML +from gprMax.user_objects.user_objects import ModelUserObject +from gprMax.utilities.host_info import set_omp_threads + +logger = logging.getLogger(__name__) + + +class Title(ModelUserObject): + """Title of the model. + + Attributes: + title (str): Model title. + """ + + @property + def order(self): + return 1 + + @property + def hash(self): + return "#title" + + def __init__(self, name: str): + """Create a Title user object. + + Args: + name: Title of the model. + """ + super().__init__(name=name) + self.title = name + + def build(self, model: Model): + model.title = self.title + logger.info(f"Model title: {model.title}") + + +class Discretisation(ModelUserObject): + """Spatial discretisation of the model in the x, y, and z dimensions. + + Attributes: + discretisation (np.array): Spatial discretisation of the model + (x, y, z) + """ + + @property + def order(self): + return 2 + + @property + def hash(self): + return "#dx_dy_dz" + + def __init__(self, p1: Tuple[float, float, float]): + """Create a Discretisation user object. + + Args: + p1: Spatial discretisation in the x, y, and z dimensions. + """ + super().__init__(p1=p1) + self.discretisation = p1 + + def build(self, model: Model): + if any(self.discretisation) <= 0: + raise ValueError( + f"{self} discretisation requires the spatial step to be" + " greater than zero in all dimensions" + ) + + model.dl = np.array(self.discretisation, dtype=np.float64) + logger.info(f"Spatial discretisation: {model.dl[0]:g} x {model.dl[1]:g} x {model.dl[2]:g}m") + + +class Domain(ModelUserObject): + """Size of the model. + + Attributes: + domain_size (tuple): Extent of the model domain (x, y, z). + """ + + @property + def order(self): + return 3 + + @property + def hash(self): + return "#domain" + + def __init__(self, p1: Tuple[float, float, float]): + """Create a Domain user object. + + Args: + p1: Model extent in the x, y, and z dimensions. + """ + super().__init__(p1=p1) + self.domain_size = p1 + + def build(self, model: Model): + uip = self._create_uip(model.G) + model.nx, model.ny, model.nz = uip.discretise_point(self.domain_size) + # TODO: Remove when distribute full build for MPI + if isinstance(model.G, MPIGrid): + model.G.nx = model.nx + model.G.ny = model.ny + model.G.nz = model.nz + + if model.nx == 0 or model.ny == 0 or model.nz == 0: + raise ValueError(f"{self} requires at least one cell in every dimension") + + logger.info( + f"Domain size: {self.domain_size[0]:g} x {self.domain_size[1]:g} x " + + f"{self.domain_size[2]:g}m ({model.nx:d} x {model.ny:d} x {model.nz:d} = " + + f"{(model.nx * model.ny * model.nz):g} cells)" + ) + + # Set mode and switch off appropriate PMLs for 2D models + grid = model.G + if model.nx == 1: + config.get_model_config().mode = "2D TMx" + grid.pmls["thickness"]["x0"] = 0 + grid.pmls["thickness"]["xmax"] = 0 + elif model.ny == 1: + config.get_model_config().mode = "2D TMy" + grid.pmls["thickness"]["y0"] = 0 + grid.pmls["thickness"]["ymax"] = 0 + elif model.nz == 1: + config.get_model_config().mode = "2D TMz" + grid.pmls["thickness"]["z0"] = 0 + grid.pmls["thickness"]["zmax"] = 0 + else: + config.get_model_config().mode = "3D" + + logger.info(f"Mode: {config.get_model_config().mode}") + + # Sub-grids cannot be used with 2D models. There would typically be + # minimal performance benefit with sub-gridding and 2D models. + if "2D" in config.get_model_config().mode and config.sim_config.general["subgrid"]: + raise ValueError("Sub-gridding cannot be used with 2D models") + + # Calculate time step at CFL limit + grid.calculate_dt() + + logger.info(f"Time step (at CFL limit): {grid.dt:g} secs") + + +class TimeStepStabilityFactor(ModelUserObject): + """Factor by which to reduce the time step from the CFL limit. + + Attributes: + stability_factor (flaot): Factor to multiply time step by. + """ + + @property + def order(self): + return 4 + + @property + def hash(self): + return "#time_step_stability_factor" + + def __init__(self, f: float): + """Create a TimeStepStabilityFactor user object. + + Args: + f: Factor to multiply the model time step by. + """ + super().__init__(f=f) + self.stability_factor = f + + def build(self, model: Model): + if self.stability_factor <= 0 or self.stability_factor > 1: + raise ValueError( + f"{self} requires the value of the time step stability" + " factor to be between zero and one" + ) + + model.dt_mod = self.stability_factor + model.dt *= model.dt_mod + + logger.info(f"Time step (modified): {model.dt:g} secs") + + +class TimeWindow(ModelUserObject): + """Specifies the total required simulated time. + + Either time or iterations must be specified. If both are specified, + time takes precedence. + + Attributes: + time: float of required simulated time in seconds. + iterations: int of required number of iterations. + """ + + @property + def order(self): + return 5 + + @property + def hash(self): + return "#time_window" + + def __init__(self, time: Optional[float] = None, iterations: Optional[int] = None): + """Create a TimeWindow user object. + + Args: + time: Optional simulation time in seconds. Default None. + iterations: Optional number of iterations. Default None. + """ + super().__init__(time=time, iterations=iterations) + self.time = time + self.iterations = iterations + + def build(self, model: Model): + if self.time is not None: + if self.time > 0: + model.timewindow = self.time + model.iterations = np.ceil(self.time / model.dt, dtype=np.int32) + 1 + else: + raise ValueError(f"{self} must have a value greater than zero") + elif self.iterations is not None: + # 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 + model.timewindow = (self.iterations - 1) * model.dt + model.iterations = self.iterations + else: + raise ValueError(f"{self} specify a time or number of iterations") + + if self.time is not None and self.iterations is not None: + logger.warning( + f"{self._params_str()} Time and iterations were both specified, using 'time'" + ) + + logger.info(f"Time window: {model.timewindow:g} secs ({model.iterations} iterations)") + + +class OMPThreads(ModelUserObject): + """Set the number of OpenMP threads to use when running the model. + + Usually this should match the number of physical CPU cores + available. + + Attributes: + omp_threads (int): Number of OpenMP threads. + """ + + @property + def order(self): + return 6 + + @property + def hash(self): + return "#num_threads" + + def __init__(self, n: int): + """Create an OMPThreads user object. + + Args: + n: Number of OpenMP threads. + """ + super().__init__(n=n) + self.omp_threads = n + + def build(self, model: Model): + if self.omp_threads < 1: + raise ValueError(f"{self} requires the value to be an integer not less than one") + + config.get_model_config().ompthreads = set_omp_threads(self.omp_threads) + + logger.info(f"Simulation will use {config.get_model_config().ompthreads} OpenMP threads") + + +class PMLFormulation(ModelUserObject): + """Set the formulation of the PMLs. + + Current options are to use the Higher Order RIPML (HORIPML) - + https://doi.org/10.1109/TAP.2011.2180344, or Multipole RIPML + (MRIPML) - https://doi.org/10.1109/TAP.2018.2823864. + + Attributes: + formulation (str): Formulation to be used for all PMLs. Either + 'HORIPML' or 'MRIPML'. + """ + + @property + def order(self): + return 7 + + @property + def hash(self): + return "#pml_formulation" + + def __init__(self, formulation: str): + """Create a PMLFormulation user object. + + Args: + formulation: Formulation to be used for all PMLs. Either + 'HORIPML' or 'MRIPML'. + """ + super().__init__(formulation=formulation) + self.formulation = formulation + + def build(self, model: Model): + if self.formulation not in PML.formulations: + logger.exception(f"{self} requires the value to be one of {' '.join(PML.formulations)}") + + model.G.pmls["formulation"] = self.formulation + + logger.info(f"PML formulation set to {model.G.pmls['formulation']}") + + +class PMLThickness(ModelUserObject): + """Set the thickness of the PMLs. + + The thickness can be set globally, or individually for each of the + six sides of the model domain. Either thickness must be set, or all + of x0, y0, z0, xmax, ymax, zmax. + + Attributes: + thickness (int | Tuple[int]): Thickness of the PML on all 6 + sides or individual sides of the model domain. + """ + + @property + def order(self): + return 7 + + @property + def hash(self): + return "#pml_cells" + + def __init__(self, thickness: Union[int, Tuple[int, int, int, int, int, int]]): + """Create a PMLThickness user object. + + Args: + thickness: Thickness of the PML on all 6 sides or individual + sides of the model domain. + """ + super().__init__(thickness=thickness) + self.thickness = thickness + + def build(self, model: Model): + grid = model.G + + if isinstance(self.thickness, int) or len(self.thickness) == 1: + for key in grid.pmls["thickness"].keys(): + grid.pmls["thickness"][key] = int(self.thickness) + elif len(self.thickness) == 6: + grid.pmls["thickness"]["x0"] = int(self.thickness[0]) + grid.pmls["thickness"]["y0"] = int(self.thickness[1]) + grid.pmls["thickness"]["z0"] = int(self.thickness[2]) + grid.pmls["thickness"]["xmax"] = int(self.thickness[3]) + grid.pmls["thickness"]["ymax"] = int(self.thickness[4]) + grid.pmls["thickness"]["zmax"] = int(self.thickness[5]) + else: + raise ValueError(f"{self} requires either one or six parameter(s)") + + # Check each PML does not take up more than half the grid + if ( + 2 * grid.pmls["thickness"]["x0"] >= grid.nx + or 2 * grid.pmls["thickness"]["y0"] >= grid.ny + or 2 * grid.pmls["thickness"]["z0"] >= grid.nz + or 2 * grid.pmls["thickness"]["xmax"] >= grid.nx + or 2 * grid.pmls["thickness"]["ymax"] >= grid.ny + or 2 * grid.pmls["thickness"]["zmax"] >= grid.nz + ): + raise ValueError(f"{self} has too many cells for the domain size") + + logger.info( + f"PML thickness: x0={model.G.pmls['x0']}, y0={model.G.pmls['y0']}," + f" z0={model.G.pmls['z0']}, xmax={model.G.pmls['xmax']}," + f" ymax={model.G.pmls['yxmax']}, zmax={model.G.pmls['zmax']}" + ) + + +class PMLProps(ModelUserObject): + """Specify the formulation and thickness of the PMLs. + + A PML can be set on each of the six sides of the model domain. + Current options are to use the Higher Order RIPML (HORIPML) - + https://doi.org/10.1109/TAP.2011.2180344, or Multipole RIPML + (MRIPML) - https://doi.org/10.1109/TAP.2018.2823864. + + Deprecated: PMLProps is deprecated and may be removed in future + releases of gprMax. Use the new PMLFormulation and PMLThickness + user objects instead. + + Attributes: + pml_formulation (PMLFormulation): User object to set the PML + formulation. + pml_thickness (PMLThickness): User object to set the PML + thickness. + """ + + @property + def order(self): + return 7 + + @property + def hash(self): + return "#pml_properties" + + def __init__( + self, + formulation: str, + thickness: Optional[int] = None, + x0: Optional[int] = None, + y0: Optional[int] = None, + z0: Optional[int] = None, + xmax: Optional[int] = None, + ymax: Optional[int] = None, + zmax: Optional[int] = None, + ): + """Create a PMLProps user object. + + If 'thickness' is set, it will take precendence over any + individual thicknesses set. Additionally, if 'thickness' is not + set, the individual thickness must be set for all six sides of + the model domain. + + Deprecated: PMLProps is deprecated and may be removed in future + releases of gprMax. Use the new PMLFormulation and PMLThickness + user objects instead. + + Args: + formulation (str): Formulation to be used for all PMLs. Either + 'HORIPML' or 'MRIPML'. + thickness: Optional thickness of the PML on all 6 sides of + the model domain. Default None. + x0, y0, z0, xmax, ymax, zmax: Optional thickness of the PML + on individual sides of the model domain. Default None. + """ + super().__init__() + + logger.warning( + "PMLProps is deprecated and may be removed in future" + " releases of gprMax. Use the new PMLFormulation and" + " PMLThickness user objects instead." + ) + + self.pml_formulation = PMLFormulation(formulation) + + if thickness is not None: + self.pml_thickness = PMLThickness(thickness) + elif ( + x0 is not None + and y0 is not None + and z0 is not None + and xmax is not None + and ymax is not None + and zmax is not None + ): + self.pml_thickness = PMLThickness((x0, y0, z0, xmax, ymax, zmax)) + else: + raise ValueError("Either set thickness, or all of x0, y0, z0, xmax, ymax, zmax.") + + def build(self, model): + self.pml_formulation.build(model) + self.pml_thickness.build(model) + + +class SrcSteps(ModelUserObject): + """Move the location of all simple sources. + + Attributes: + step_size (Tuple[float]): Increment (x, y, z) to move all + simple sources by for each step. + """ + + @property + def order(self): + return 8 + + @property + def hash(self): + return "#src_steps" + + def __init__(self, p1: Tuple[float, float, float]): + """Create a SrcSteps user object. + + Args: + p1: Increment (x, y, z) to move all simple sources by for + each step. + """ + super().__init__(p1=p1) + self.step_size = p1 + + def build(self, model: Model): + uip = self._create_uip(model.G) + model.srcsteps = np.array(uip.discretise_point(self.step_size), dtype=np.int32) + + logger.info( + f"Simple sources will step {model.srcsteps[0] * model.dx:g}m, " + f"{model.srcsteps[1] * model.dy:g}m, {model.srcsteps[2] * model.dz:g}m " + "for each model run." + ) + + +class RxSteps(ModelUserObject): + """Move the location of all receivers. + + Attributes: + step_size (Tuple[float]): Increment (x, y, z) to move all + receivers by for each step. + """ + + @property + def order(self): + return 9 + + @property + def hash(self): + return "#rx_steps" + + def __init__(self, p1: Tuple[float, float, float]): + """Create a RxSteps user object. + + Args: + p1: Increment (x, y, z) to move all receivers by for each + step. + """ + super().__init__(p1=p1) + self.step_size = p1 + + def build(self, model: Model): + uip = self._create_uip(model.G) + model.rxsteps = np.array(uip.discretise_point(self.step_size), dtype=np.int32) + + logger.info( + f"All receivers will step {model.rxsteps[0] * model.dx:g}m, " + f"{model.rxsteps[1] * model.dy:g}m, {model.rxsteps[2] * model.dz:g}m " + "for each model run." + ) diff --git a/gprMax/user_objects/user_objects.py b/gprMax/user_objects/user_objects.py index 7bddeb02..e9b35e22 100644 --- a/gprMax/user_objects/user_objects.py +++ b/gprMax/user_objects/user_objects.py @@ -100,6 +100,11 @@ class ModelUserObject(UserObject): @abstractmethod def build(self, model: Model): + """Build user object and set model properties. + + Args: + model: Model to set the properties of. + """ pass From d20fc36d2986f8f752b7e75417e4038f377244a2 Mon Sep 17 00:00:00 2001 From: nmannall Date: Tue, 10 Dec 2024 10:39:42 +0000 Subject: [PATCH 04/20] Move cmds_multiuse.py to new user_objects folder --- gprMax/__init__.py | 8 ++-- gprMax/hash_cmds_multiuse.py | 2 +- gprMax/scene.py | 2 +- gprMax/subgrids/user_objects.py | 7 ++- gprMax/{ => user_objects}/cmds_multiuse.py | 51 +++++++++++----------- 5 files changed, 34 insertions(+), 36 deletions(-) rename gprMax/{ => user_objects}/cmds_multiuse.py (96%) diff --git a/gprMax/__init__.py b/gprMax/__init__.py index 28dfb434..f8e613ad 100644 --- a/gprMax/__init__.py +++ b/gprMax/__init__.py @@ -24,7 +24,10 @@ from .cmds_geometry.geometry_objects_read import GeometryObjectsRead from .cmds_geometry.plate import Plate from .cmds_geometry.sphere import Sphere from .cmds_geometry.triangle import Triangle -from .cmds_multiuse import ( +from .gprMax import run as run +from .scene import Scene +from .subgrids.user_objects import SubGridHSG +from .user_objects.cmds_multiuse import ( PMLCFS, AddDebyeDispersion, AddDrudeDispersion, @@ -46,9 +49,6 @@ from .cmds_multiuse import ( VoltageSource, Waveform, ) -from .gprMax import run as run -from .scene import Scene -from .subgrids.user_objects import SubGridHSG from .user_objects.cmds_singleuse import ( Discretisation, Domain, diff --git a/gprMax/hash_cmds_multiuse.py b/gprMax/hash_cmds_multiuse.py index c933b7c4..688411a4 100644 --- a/gprMax/hash_cmds_multiuse.py +++ b/gprMax/hash_cmds_multiuse.py @@ -18,7 +18,7 @@ import logging -from .cmds_multiuse import ( +from .user_objects.cmds_multiuse import ( PMLCFS, AddDebyeDispersion, AddDrudeDispersion, diff --git a/gprMax/scene.py b/gprMax/scene.py index ee81af3a..96b99506 100644 --- a/gprMax/scene.py +++ b/gprMax/scene.py @@ -24,13 +24,13 @@ from gprMax.cmds_geometry.add_surface_roughness import AddSurfaceRoughness from gprMax.cmds_geometry.add_surface_water import AddSurfaceWater from gprMax.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.cmds_geometry.fractal_box import FractalBox -from gprMax.cmds_multiuse import UserObjectMulti from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_built_in_materials from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.subgrids.user_objects import SubGridBase as SubGridUserBase from gprMax.user_inputs import MainGridUserInput, SubgridUserInput +from gprMax.user_objects.cmds_multiuse import UserObjectMulti from gprMax.user_objects.cmds_singleuse import Discretisation, Domain, TimeWindow, UserObjectSingle logger = logging.getLogger(__name__) diff --git a/gprMax/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py index aa7c8ee0..b7c0582e 100644 --- a/gprMax/subgrids/user_objects.py +++ b/gprMax/subgrids/user_objects.py @@ -22,14 +22,13 @@ from typing import List, Tuple, Union import numpy as np +from gprMax.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid +from gprMax.subgrids.subgrid_hsg import SubGridHSG as SubGridHSGUser from gprMax.user_inputs import MainGridUserInput - -from ..cmds_geometry.cmds_geometry import UserObjectGeometry -from ..cmds_multiuse import UserObjectMulti -from .subgrid_hsg import SubGridHSG as SubGridHSGUser +from gprMax.user_objects.cmds_multiuse import UserObjectMulti logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py similarity index 96% rename from gprMax/cmds_multiuse.py rename to gprMax/user_objects/cmds_multiuse.py index 55cc64fb..2cc34d35 100644 --- a/gprMax/cmds_multiuse.py +++ b/gprMax/user_objects/cmds_multiuse.py @@ -26,34 +26,33 @@ import numpy.typing as npt from scipy import interpolate import gprMax.config as config -from gprMax.grid.fdtd_grid import FDTDGrid -from gprMax.grid.mpi_grid import MPIGrid -from gprMax.model import Model -from gprMax.user_inputs import MainGridUserInput - -from .cmds_geometry.cmds_geometry import ( +from gprMax.cmds_geometry.cmds_geometry import ( UserObjectGeometry, rotate_2point_object, rotate_polarisation, ) -from .geometry_outputs import GeometryObjects as GeometryObjectsUser -from .geometry_outputs import MPIGeometryObjects as MPIGeometryObjectsUser -from .materials import DispersiveMaterial as DispersiveMaterialUser -from .materials import ListMaterial as ListMaterialUser -from .materials import Material as MaterialUser -from .materials import PeplinskiSoil as PeplinskiSoilUser -from .materials import RangeMaterial as RangeMaterialUser -from .pml import CFS, CFSParameter -from .receivers import Rx as RxUser -from .snapshots import MPISnapshot as MPISnapshotUser -from .snapshots import Snapshot as SnapshotUser -from .sources import HertzianDipole as HertzianDipoleUser -from .sources import MagneticDipole as MagneticDipoleUser -from .sources import TransmissionLine as TransmissionLineUser -from .sources import VoltageSource as VoltageSourceUser -from .subgrids.grid import SubGridBaseGrid -from .utilities.utilities import round_value -from .waveforms import Waveform as WaveformUser +from gprMax.geometry_outputs import GeometryObjects as GeometryObjectsUser +from gprMax.geometry_outputs import MPIGeometryObjects as MPIGeometryObjectsUser +from gprMax.grid.fdtd_grid import FDTDGrid +from gprMax.grid.mpi_grid import MPIGrid +from gprMax.materials import DispersiveMaterial as DispersiveMaterialUser +from gprMax.materials import ListMaterial as ListMaterialUser +from gprMax.materials import Material as MaterialUser +from gprMax.materials import PeplinskiSoil as PeplinskiSoilUser +from gprMax.materials import RangeMaterial as RangeMaterialUser +from gprMax.model import Model +from gprMax.pml import CFS, CFSParameter +from gprMax.receivers import Rx as RxUser +from gprMax.snapshots import MPISnapshot as MPISnapshotUser +from gprMax.snapshots import Snapshot as SnapshotUser +from gprMax.sources import HertzianDipole as HertzianDipoleUser +from gprMax.sources import MagneticDipole as MagneticDipoleUser +from gprMax.sources import TransmissionLine as TransmissionLineUser +from gprMax.sources import VoltageSource as VoltageSourceUser +from gprMax.subgrids.grid import SubGridBaseGrid +from gprMax.user_inputs import MainGridUserInput +from gprMax.utilities.utilities import round_value +from gprMax.waveforms import Waveform as WaveformUser logger = logging.getLogger(__name__) @@ -1888,9 +1887,9 @@ class GeometryView(UserObjectMulti): """ if output_type == "n": - from .geometry_outputs import GeometryViewVoxels as GeometryViewUser + from gprMax.geometry_outputs import GeometryViewVoxels as GeometryViewUser else: - from .geometry_outputs import GeometryViewLines as GeometryViewUser + from gprMax.geometry_outputs import GeometryViewLines as GeometryViewUser return GeometryViewUser From e818441730a075f0c8a9d24968aff776725f2768 Mon Sep 17 00:00:00 2001 From: nmannall Date: Tue, 10 Dec 2024 11:06:30 +0000 Subject: [PATCH 05/20] Move iterations and timewindow to FDTDGrid Grids need to know the number of iterations and the timewindow to calculate waveform values. While waveforms are global in an MPI sense, subgrids will calculate their own waveforms based on the dt value of the subgrid. --- gprMax/grid/fdtd_grid.py | 3 +++ gprMax/model.py | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/gprMax/grid/fdtd_grid.py b/gprMax/grid/fdtd_grid.py index de8bea10..0813da0f 100644 --- a/gprMax/grid/fdtd_grid.py +++ b/gprMax/grid/fdtd_grid.py @@ -61,6 +61,9 @@ class FDTDGrid: self.dl = np.ones(3, dtype=np.float64) self.dt = 0.0 + self.iterations = 0 # Total number of iterations + self.timewindow = 0.0 + # Field Arrays self.Ex: npt.NDArray[np.float32] self.Ey: npt.NDArray[np.float32] diff --git a/gprMax/model.py b/gprMax/model.py index 18a837ba..8d7012f5 100644 --- a/gprMax/model.py +++ b/gprMax/model.py @@ -56,8 +56,6 @@ class Model: self.dt_mod = 1.0 # Time step stability factor self.iteration = 0 # Current iteration number - self.iterations = 0 # Total number of iterations - self.timewindow = 0.0 self.srcsteps = np.zeros(3, dtype=np.int32) self.rxsteps = np.zeros(3, dtype=np.int32) @@ -141,6 +139,22 @@ class Model: def dt(self, value: float): self.G.dt = value + @property + def iterations(self) -> int: + return self.G.iterations + + @iterations.setter + def iterations(self, value: int): + self.G.iterations = value + + @property + def timewindow(self) -> float: + return self.G.timewindow + + @timewindow.setter + def timewindow(self, value: float): + self.G.timewindow = value + def _create_grid(self) -> FDTDGrid: """Create grid object according to solver. From b401f1d00db10757823ce9625b5181dca9394898 Mon Sep 17 00:00:00 2001 From: nmannall Date: Tue, 10 Dec 2024 14:14:18 +0000 Subject: [PATCH 06/20] Update multiuse cmds to new user object classes --- gprMax/snapshots.py | 4 +- gprMax/user_objects/cmds_multiuse.py | 620 +++++++++++--------------- gprMax/user_objects/cmds_output.py | 180 ++++++++ gprMax/user_objects/cmds_singleuse.py | 2 +- gprMax/user_objects/rotatable.py | 4 +- gprMax/user_objects/user_objects.py | 35 +- 6 files changed, 490 insertions(+), 355 deletions(-) create mode 100644 gprMax/user_objects/cmds_output.py diff --git a/gprMax/snapshots.py b/gprMax/snapshots.py index 69278fc4..48e93770 100644 --- a/gprMax/snapshots.py +++ b/gprMax/snapshots.py @@ -110,7 +110,7 @@ class Snapshot: filename: str, fileext: str, outputs: Dict[str, bool], - grid_dl: npt.NDArray[np.float32], + grid_dl: npt.NDArray[np.float64], grid_dt: float, ): """ @@ -358,7 +358,7 @@ class MPISnapshot(Snapshot): filename: str, fileext: str, outputs: Dict[str, bool], - grid_dl: npt.NDArray[np.float32], + grid_dl: npt.NDArray[np.float64], grid_dt: float, ): super().__init__( diff --git a/gprMax/user_objects/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py index 2cc34d35..2e4eaed4 100644 --- a/gprMax/user_objects/cmds_multiuse.py +++ b/gprMax/user_objects/cmds_multiuse.py @@ -19,7 +19,9 @@ import inspect import logging from abc import ABC, abstractmethod +from os import PathLike from pathlib import Path +from typing import Optional, Union import numpy as np import numpy.typing as npt @@ -31,8 +33,6 @@ from gprMax.cmds_geometry.cmds_geometry import ( rotate_2point_object, rotate_polarisation, ) -from gprMax.geometry_outputs import GeometryObjects as GeometryObjectsUser -from gprMax.geometry_outputs import MPIGeometryObjects as MPIGeometryObjectsUser from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.grid.mpi_grid import MPIGrid from gprMax.materials import DispersiveMaterial as DispersiveMaterialUser @@ -51,6 +51,8 @@ from gprMax.sources import TransmissionLine as TransmissionLineUser from gprMax.sources import VoltageSource as VoltageSourceUser from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.user_inputs import MainGridUserInput +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GridUserObject from gprMax.utilities.utilities import round_value from gprMax.waveforms import Waveform as WaveformUser @@ -105,45 +107,54 @@ class UserObjectMulti(ABC): return f"[{model.title}] " -class ExcitationFile(UserObjectMulti): - """An ASCII file that contains columns of amplitude values that specify - custom waveform shapes that can be used with sources in the model. +class ExcitationFile(GridUserObject): + """Specify file containing amplitude values of custom waveforms. + + The file should be an ASCII file, and the custom waveform shapes can + be used with sources in the model. Attributes: - filepath: string of excitation file path. - kind: string or int specifying interpolation kind passed to - scipy.interpolate.interp1d. - fill_value: float or 'extrapolate' passed to scipy.interpolate.interp1d. + filepath (str | PathLike): Excitation file path. + kind (int | str | None): Optional interpolation kind passed to + scipy.interpolate.interp1d. + fill_value (float | str | None): Optional float value or + 'extrapolate' passed to scipy.interpolate.interp1d. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 1 - self.hash = "#excitation_file" + @property + def order(self): + return 1 - def build(self, model, uip): - try: - kwargs = {} - excitationfile = self.kwargs["filepath"] - kwargs["kind"] = self.kwargs["kind"] - kwargs["fill_value"] = self.kwargs["fill_value"] + @property + def hash(self): + return "#excitation_file" - except KeyError: - try: - excitationfile = self.kwargs["filepath"] - fullargspec = inspect.getfullargspec(interpolate.interp1d) - kwargs = dict(zip(reversed(fullargspec.args), reversed(fullargspec.defaults))) - except KeyError: - logger.exception(f"{self.__str__()} requires either one or three parameter(s)") - raise + def __init__( + self, + filepath: Union[str, PathLike], + kind: Optional[Union[int, str]] = None, + fill_value: Optional[Union[float, str]] = None, + ): + """Create an ExcitationFile user object. + Args: + filepath: Excitation file path. + kind: Optional interpolation kind passed to + scipy.interpolate.interp1d. Default None. + fill_value: Optional float value or 'extrapolate' passed to + scipy.interpolate.interp1d. Default None. + """ + super().__init__(filepath=filepath, kind=kind, fill_value=fill_value) + self.filepath = filepath + self.kind = kind + self.fill_value = fill_value + + def build(self, grid: FDTDGrid): # See if file exists at specified path and if not try input file directory - excitationfile = Path(excitationfile) - # excitationfile = excitationfile.resolve() + excitationfile = Path(self.filepath) if not excitationfile.exists(): excitationfile = Path(config.sim_config.input_file_path.parent, excitationfile) - grid = uip.grid logger.info(self.grid_name(grid) + f"Excitation file: {excitationfile}") # Get waveform names @@ -161,13 +172,12 @@ class ExcitationFile(UserObjectMulti): waveformvalues = waveformvalues[:, 1:] timestr = "user-defined time array" else: - waveformtime = np.arange(0, model.timewindow + grid.dt, grid.dt) + waveformtime = np.arange(0, grid.timewindow + grid.dt, grid.dt) timestr = "simulation time array" for i, waveformID in enumerate(waveformIDs): if any(x.ID == waveformID for x in grid.waveforms): - logger.exception(f"Waveform with ID {waveformID} already exists") - raise ValueError + raise ValueError(f"Waveform with ID {waveformID} already exists") w = WaveformUser() w.ID = waveformID w.type = "user" @@ -190,38 +200,61 @@ class ExcitationFile(UserObjectMulti): ) # Interpolate waveform values - w.userfunc = interpolate.interp1d(waveformtime, singlewaveformvalues, **kwargs) + if self.kind is None and self.fill_value is None: + w.userfunc = interpolate.interp1d(waveformtime, singlewaveformvalues) + elif self.kind is not None and self.fill_value is not None: + w.userfunc = interpolate.interp1d( + waveformtime, singlewaveformvalues, kind=self.kind, fill_value=self.fill_value + ) + else: + raise ValueError(f"{self} requires either one or three parameter(s)") logger.info( self.grid_name(grid) + f"User waveform {w.ID} created using {timestr} and, if " - f"required, interpolation parameters (kind: {kwargs['kind']}, " - f"fill value: {kwargs['fill_value']})." + f"required, interpolation parameters (kind: {self.kind}, " + f"fill value: {self.fill_value})." ) grid.waveforms.append(w) -class Waveform(UserObjectMulti): - """Specifies waveforms to use with sources in the model. +class Waveform(GridUserObject): + """Create waveform to use with sources in the model. Attributes: - wave_type: string required to specify waveform type. - amp: float to scale maximum amplitude of waveform. - freq: float to specify centre frequency (Hz) of waveform. - id: string required for identifier of waveform. - user_values: optional 1D array of amplitude values to use with - user waveform. - user_time: optional 1D array of time values to use with user waveform. - kind: optional string or int, see scipy.interpolate.interp1d - https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html#scipy-interpolate-interp1d - fill_value: optional array or 'extrapolate', see scipy.interpolate.interp1d - https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html#scipy-interpolate-interp1d + wave_type (str): Waveform type. Can should be one of 'gaussian', + 'gaussiandot', 'gaussiandotnorm', 'gaussiandotdot', + 'gaussiandotdotnorm', 'ricker', 'gaussianprime', + 'gaussiandoubleprime', 'sine', 'contsine'. + amp (float): Factor to scale the maximum amplitude of the + waveform by. (For a #hertzian_dipole the units will be Amps, + for a #voltage_source or #transmission_line the units will + be Volts). + freq: Centre frequency (Hz) of the waveform. In the case of the + Gaussian waveform it is related to the pulse width. + id (str): Identifier of the waveform. + user_values: Optional 1D array of amplitude values to use with + user waveform. + user_time: Optional 1D array of time values to use with user + waveform. + kind (int | str | None): Optional string or int, see + scipy.interpolate.interp1d - https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html#scipy-interpolate-interp1d + fill_value: Optional array or 'extrapolate', see + scipy.interpolate.interp1d - https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html#scipy-interpolate-interp1d """ + @property + def order(self): + return 2 + + @property + def hash(self): + return "#waveform" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 2 - self.hash = "#waveform" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: wavetype = self.kwargs["wave_type"].lower() except KeyError: @@ -235,7 +268,6 @@ class Waveform(UserObjectMulti): ) raise ValueError - grid = uip.grid if wavetype != "user": try: amp = self.kwargs["amp"] @@ -287,7 +319,7 @@ class Waveform(UserObjectMulti): if "user_time" in self.kwargs: waveformtime = self.kwargs["user_time"] else: - waveformtime = np.arange(0, model.timewindow + grid.dt, grid.dt) + waveformtime = np.arange(0, grid.timewindow + grid.dt, grid.dt) # Set args for interpolation if given by user if "kind" in self.kwargs: @@ -309,7 +341,7 @@ class Waveform(UserObjectMulti): grid.waveforms.append(w) -class VoltageSource(UserObjectMulti): +class VoltageSource(GridUserObject, Rotatable): """Specifies a voltage source at an electric field location. Attributes: @@ -322,19 +354,18 @@ class VoltageSource(UserObjectMulti): stop: float optional to time (secs) to remove source. """ + @property + def order(self): + return 3 + + @property + def hash(self): + return "#voltage_source" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 3 - self.hash = "#voltage_source" - def rotate(self, axis, angle, origin=None): - """Sets parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self, grid): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" rot_pol_pts, self.kwargs["polarisation"] = rotate_polarisation( self.kwargs["p1"], self.kwargs["polarisation"], self.axis, self.angle, grid @@ -342,7 +373,7 @@ class VoltageSource(UserObjectMulti): rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] polarisation = self.kwargs["polarisation"].lower() @@ -352,7 +383,6 @@ class VoltageSource(UserObjectMulti): logger.exception(self.params_str() + (" requires at least six parameters.")) raise - grid = uip.grid if self.do_rotate: self._do_rotate(grid) @@ -379,6 +409,7 @@ class VoltageSource(UserObjectMulti): logger.exception(self.params_str() + (" polarisation must be z in 2D TMz mode.")) raise ValueError + uip = self._create_uip(grid) xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str()) p2 = uip.round_to_grid_static_point(p1) @@ -434,15 +465,14 @@ class VoltageSource(UserObjectMulti): ) raise ValueError v.start = start - v.stop = min(stop, model.timewindow) + v.stop = min(stop, grid.timewindow) startstop = f" start time {v.start:g} secs, finish time {v.stop:g} secs " except KeyError: v.start = 0 - v.stop = model.timewindow + v.stop = grid.timewindow startstop = " " - iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations - v.calculate_waveform_values(iterations, grid.dt) + v.calculate_waveform_values(grid.iterations, grid.dt) logger.info( f"{self.grid_name(grid)}Voltage source with polarity " @@ -455,7 +485,7 @@ class VoltageSource(UserObjectMulti): grid.voltagesources.append(v) -class HertzianDipole(UserObjectMulti): +class HertzianDipole(GridUserObject, Rotatable): """Specifies a current density term at an electric field location. The simplest excitation, often referred to as an additive or soft source. @@ -468,19 +498,18 @@ class HertzianDipole(UserObjectMulti): stop: float optional to time (secs) to remove source. """ + @property + def order(self): + return 4 + + @property + def hash(self): + return "#hertzian_dipole" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 4 - self.hash = "#hertzian_dipole" - def rotate(self, axis, angle, origin=None): - """Sets parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self, grid): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" rot_pol_pts, self.kwargs["polarisation"] = rotate_polarisation( self.kwargs["p1"], self.kwargs["polarisation"], self.axis, self.angle, grid @@ -488,7 +517,7 @@ class HertzianDipole(UserObjectMulti): rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: polarisation = self.kwargs["polarisation"].lower() p1 = self.kwargs["p1"] @@ -497,7 +526,6 @@ class HertzianDipole(UserObjectMulti): logger.exception(f"{self.params_str()} requires at least 3 parameters.") raise - grid = uip.grid if self.do_rotate: self._do_rotate(grid) @@ -524,6 +552,7 @@ class HertzianDipole(UserObjectMulti): logger.exception(self.params_str() + " polarisation must be z in 2D TMz mode.") raise ValueError + uip = self._create_uip(grid) xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str()) p2 = uip.round_to_grid_static_point(p1) @@ -574,15 +603,14 @@ class HertzianDipole(UserObjectMulti): ) raise ValueError h.start = start - h.stop = min(stop, model.timewindow) + h.stop = min(stop, grid.timewindow) startstop = f" start time {h.start:g} secs, finish time {h.stop:g} secs " except KeyError: h.start = 0 - h.stop = model.timewindow + h.stop = grid.timewindow startstop = " " - iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations - h.calculate_waveform_values(iterations, grid.dt) + h.calculate_waveform_values(grid.iterations, grid.dt) if config.get_model_config().mode == "2D": logger.info( @@ -604,7 +632,7 @@ class HertzianDipole(UserObjectMulti): grid.hertziandipoles.append(h) -class MagneticDipole(UserObjectMulti): +class MagneticDipole(GridUserObject, Rotatable): """Simulates an infinitesimal magnetic dipole. Often referred to as an additive or soft source. @@ -617,19 +645,18 @@ class MagneticDipole(UserObjectMulti): stop: float optional to time (secs) to remove source. """ + @property + def order(self): + return 5 + + @property + def hash(self): + return "#magnetic_dipole" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 5 - self.hash = "#magnetic_dipole" - def rotate(self, axis, angle, origin=None): - """Sets parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self, grid): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" rot_pol_pts, self.kwargs["polarisation"] = rotate_polarisation( self.kwargs["p1"], self.kwargs["polarisation"], self.axis, self.angle, grid @@ -637,7 +664,7 @@ class MagneticDipole(UserObjectMulti): rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: polarisation = self.kwargs["polarisation"].lower() p1 = self.kwargs["p1"] @@ -646,7 +673,6 @@ class MagneticDipole(UserObjectMulti): logger.exception(f"{self.params_str()} requires at least five parameters.") raise - grid = uip.grid if self.do_rotate: self._do_rotate(grid) @@ -673,6 +699,7 @@ class MagneticDipole(UserObjectMulti): logger.exception(self.params_str() + " polarisation must be z in 2D TMz mode.") raise ValueError + uip = self._create_uip(grid) xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str()) p2 = uip.round_to_grid_static_point(p1) @@ -724,15 +751,14 @@ class MagneticDipole(UserObjectMulti): ) raise ValueError m.start = start - m.stop = min(stop, model.timewindow) + m.stop = min(stop, grid.timewindow) startstop = f" start time {m.start:g} secs, finish time {m.stop:g} secs " except KeyError: m.start = 0 - m.stop = model.timewindow + m.stop = grid.timewindow startstop = " " - iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations - m.calculate_waveform_values(iterations, grid.dt) + m.calculate_waveform_values(grid.iterations, grid.dt) logger.info( f"{self.grid_name(grid)}Magnetic dipole with polarity " @@ -744,7 +770,7 @@ class MagneticDipole(UserObjectMulti): grid.magneticdipoles.append(m) -class TransmissionLine(UserObjectMulti): +class TransmissionLine(GridUserObject, Rotatable): """Specifies a one-dimensional transmission line model at an electric field location. @@ -758,19 +784,18 @@ class TransmissionLine(UserObjectMulti): stop: float optional to time (secs) to remove source. """ + @property + def order(self): + return 6 + + @property + def hash(self): + return "#transmission_line" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 6 - self.hash = "#transmission_line" - def rotate(self, axis, angle, origin=None): - """Sets parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self, grid): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" rot_pol_pts, self.kwargs["polarisation"] = rotate_polarisation( self.kwargs["p1"], self.kwargs["polarisation"], self.axis, self.angle, grid @@ -778,7 +803,7 @@ class TransmissionLine(UserObjectMulti): rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: polarisation = self.kwargs["polarisation"].lower() p1 = self.kwargs["p1"] @@ -788,7 +813,6 @@ class TransmissionLine(UserObjectMulti): logger.exception(f"{self.params_str()} requires at least six parameters.") raise - grid = uip.grid if self.do_rotate: self._do_rotate(grid) @@ -824,6 +848,7 @@ class TransmissionLine(UserObjectMulti): logger.exception(self.params_str() + (" polarisation must be z in " "2D TMz mode.")) raise ValueError + uip = self._create_uip(grid) xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str()) p2 = uip.round_to_grid_static_point(p1) @@ -842,8 +867,7 @@ class TransmissionLine(UserObjectMulti): ) raise ValueError - iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations - t = TransmissionLineUser(iterations, grid.dt) + t = TransmissionLineUser(grid.iterations, grid.dt) t.polarisation = polarisation t.xcoord = xcoord t.ycoord = ycoord @@ -884,14 +908,14 @@ class TransmissionLine(UserObjectMulti): ) raise ValueError t.start = start - t.stop = min(stop, model.timewindow) + t.stop = min(stop, grid.timewindow) startstop = f" start time {t.start:g} secs, finish time {t.stop:g} secs " except KeyError: t.start = 0 - t.stop = model.timewindow + t.stop = grid.timewindow startstop = " " - t.calculate_waveform_values(iterations, grid.dt) + t.calculate_waveform_values(grid.iterations, grid.dt) t.calculate_incident_V_I(grid) logger.info( @@ -905,7 +929,7 @@ class TransmissionLine(UserObjectMulti): grid.transmissionlines.append(t) -class Rx(UserObjectMulti): +class Rx(GridUserObject, Rotatable): """Specifies output points in the model. These are locations where the values of the electric and magnetic field @@ -918,20 +942,20 @@ class Rx(UserObjectMulti): selection from Ex, Ey, Ez, Hx, Hy, Hz, Ix, Iy, or Iz. """ + @property + def order(self): + return 7 + + @property + def hash(self): + return "#rx" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 7 - self.hash = "#rx" + # TODO: Can this be removed? self.constructor = RxUser - def rotate(self, axis, angle, origin=None): - """Sets parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self, grid): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" new_pt = ( self.kwargs["p1"][0] + grid.dx, @@ -952,17 +976,17 @@ class Rx(UserObjectMulti): except KeyError: pass - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] except KeyError: logger.exception(self.params_str()) raise - grid = uip.grid if self.do_rotate: self._do_rotate(grid) + uip = self._create_uip(grid) p = uip.check_src_rx_point(p1, self.params_str()) p2 = uip.round_to_grid_static_point(p1) @@ -970,8 +994,6 @@ class Rx(UserObjectMulti): r.xcoord, r.ycoord, r.zcoord = p r.xcoordorigin, r.ycoordorigin, r.zcoordorigin = p - iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations - try: r.ID = self.kwargs["id"] outputs = self.kwargs["outputs"] @@ -980,7 +1002,7 @@ class Rx(UserObjectMulti): r.ID = f"{r.__class__.__name__}({str(r.xcoord)},{str(r.ycoord)},{str(r.zcoord)})" for key in RxUser.defaultoutputs: r.outputs[key] = np.zeros( - iterations, dtype=config.sim_config.dtypes["float_or_double"] + grid.iterations, dtype=config.sim_config.dtypes["float_or_double"] ) else: outputs.sort() @@ -993,7 +1015,7 @@ class Rx(UserObjectMulti): for field in outputs: if field in allowableoutputs: r.outputs[field] = np.zeros( - iterations, dtype=config.sim_config.dtypes["float_or_double"] + grid.iterations, dtype=config.sim_config.dtypes["float_or_double"] ) else: logger.exception( @@ -1015,7 +1037,7 @@ class Rx(UserObjectMulti): return r -class RxArray(UserObjectMulti): +class RxArray(GridUserObject): """Defines multiple output points in the model. Attributes: @@ -1024,12 +1046,18 @@ class RxArray(UserObjectMulti): dl: tuple required for receiver spacing dx, dy, dz. """ + @property + def order(self): + return 8 + + @property + def hash(self): + return "#rx_array" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 8 - self.hash = "#rx_array" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -1038,6 +1066,7 @@ class RxArray(UserObjectMulti): logger.exception(f"{self.params_str()} requires exactly 9 parameters") raise + uip = self._create_uip(grid) xs, ys, zs = uip.check_src_rx_point(p1, self.params_str(), "lower") xf, yf, zf = uip.check_src_rx_point(p2, self.params_str(), "upper") p3 = uip.round_to_grid_static_point(p1) @@ -1077,7 +1106,6 @@ class RxArray(UserObjectMulti): ) raise ValueError - grid = uip.grid logger.info( f"{self.grid_name(grid)}Receiver array " f"{p3[0]:g}m, {p3[1]:g}m, {p3[2]:g}m, to " @@ -1085,8 +1113,6 @@ class RxArray(UserObjectMulti): f"{dx * grid.dx:g}m, {dy * grid.dy:g}m, {dz * grid.dz:g}m" ) - iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations - for x in range(xs, xf + 1, dx): for y in range(ys, yf + 1, dy): for z in range(zs, zf + 1, dz): @@ -1104,7 +1130,7 @@ class RxArray(UserObjectMulti): r.ID = f"{r.__class__.__name__}({str(x)},{str(y)},{str(z)})" for key in RxUser.defaultoutputs: r.outputs[key] = np.zeros( - iterations, dtype=config.sim_config.dtypes["float_or_double"] + grid.iterations, dtype=config.sim_config.dtypes["float_or_double"] ) logger.info( f" Receiver at {p5[0]:g}m, {p5[1]:g}m, " @@ -1114,7 +1140,7 @@ class RxArray(UserObjectMulti): grid.rxs.append(r) -class Snapshot(UserObjectMulti): +class Snapshot(GridUserObject): """Obtains information about the electromagnetic fields within a volume of the model at a given time instant. @@ -1135,10 +1161,17 @@ class Snapshot(UserObjectMulti): selection from Ex, Ey, Ez, Hx, Hy, or Hz. """ + # TODO: Make this an output user object + @property + def order(self): + return 9 + + @property + def hash(self): + return "#snapshot" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 9 - self.hash = "#snapshot" def _calculate_upper_bound( self, start: npt.NDArray, step: npt.NDArray, size: npt.NDArray @@ -1146,9 +1179,7 @@ class Snapshot(UserObjectMulti): # upper_bound = p2 + dl - ((snapshot_size - 1) % dl) - 1 return start + step * np.ceil(size / step) - def build(self, model, uip): - grid = uip.grid - + def build(self, grid: FDTDGrid): if isinstance(grid, SubGridBaseGrid): logger.exception(f"{self.params_str()} do not add snapshots to subgrids.") raise ValueError @@ -1161,6 +1192,7 @@ class Snapshot(UserObjectMulti): logger.exception(f"{self.params_str()} requires exactly 11 parameters.") raise + uip = self._create_uip(grid) dl = np.array(uip.discretise_static_point(dl)) try: @@ -1241,7 +1273,7 @@ class Snapshot(UserObjectMulti): logger.exception(f"{self.params_str()} time value must be greater than zero.") raise ValueError - if iterations <= 0 or iterations > model.iterations: + if iterations <= 0 or iterations > grid.iterations: logger.exception(f"{self.params_str()} time value is not valid.") raise ValueError @@ -1321,7 +1353,7 @@ class Snapshot(UserObjectMulti): grid.snapshots.append(s) -class Material(UserObjectMulti): +class Material(GridUserObject): """Specifies a material in the model described by a set of constitutive parameters. @@ -1333,12 +1365,18 @@ class Material(UserObjectMulti): id: string used as identifier for material. """ + @property + def order(self): + return 10 + + @property + def hash(self): + return "#material" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 10 - self.hash = "#material" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: er = self.kwargs["er"] se = self.kwargs["se"] @@ -1372,7 +1410,6 @@ class Material(UserObjectMulti): logger.exception(f"{self.params_str()} requires a positive value for magnetic loss.") raise ValueError - grid = uip.grid if any(x.ID == material_id for x in grid.materials): logger.exception(f"{self.params_str()} with ID {material_id} already exists") raise ValueError @@ -1390,7 +1427,7 @@ class Material(UserObjectMulti): m.er = er logger.info( - f"{self.model_name(model)}Material {m.ID} with eps_r={m.er:g}, " + f"{self.grid_name(grid)}Material {m.ID} with eps_r={m.er:g}, " f"sigma={m.se:g} S/m; mu_r={m.mr:g}, sigma*={m.sm:g} Ohm/m " f"created." ) @@ -1398,7 +1435,7 @@ class Material(UserObjectMulti): grid.materials.append(m) -class AddDebyeDispersion(UserObjectMulti): +class AddDebyeDispersion(GridUserObject): """Adds dispersive properties to already defined Material based on a multi-pole Debye formulation. @@ -1412,12 +1449,18 @@ class AddDebyeDispersion(UserObjectMulti): properties. """ + @property + def order(self): + return 11 + + @property + def hash(self): + return "#add_dispersion_debye" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 11 - self.hash = "#add_dispersion_debye" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: poles = self.kwargs["poles"] er_delta = self.kwargs["er_delta"] @@ -1432,7 +1475,6 @@ class AddDebyeDispersion(UserObjectMulti): raise ValueError # Look up requested materials in existing list of material instances - grid = uip.grid materials = [y for x in material_ids for y in grid.materials if y.ID == x] if len(materials) != len(material_ids): @@ -1474,7 +1516,7 @@ class AddDebyeDispersion(UserObjectMulti): ) -class AddLorentzDispersion(UserObjectMulti): +class AddLorentzDispersion(GridUserObject): """Adds dispersive properties to already defined Material based on a multi-pole Lorentz formulation. @@ -1489,12 +1531,18 @@ class AddLorentzDispersion(UserObjectMulti): properties. """ + @property + def order(self): + return 12 + + @property + def hash(self): + return "#add_dispersion_lorentz" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 12 - self.hash = "#add_dispersion_lorentz" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: poles = self.kwargs["poles"] er_delta = self.kwargs["er_delta"] @@ -1510,7 +1558,6 @@ class AddLorentzDispersion(UserObjectMulti): raise ValueError # Look up requested materials in existing list of material instances - grid = uip.grid materials = [y for x in material_ids for y in grid.materials if y.ID == x] if len(materials) != len(material_ids): @@ -1557,7 +1604,7 @@ class AddLorentzDispersion(UserObjectMulti): ) -class AddDrudeDispersion(UserObjectMulti): +class AddDrudeDispersion(GridUserObject): """Adds dispersive properties to already defined Material based on a multi-pole Drude formulation. @@ -1569,12 +1616,18 @@ class AddDrudeDispersion(UserObjectMulti): properties. """ + @property + def order(self): + return 13 + + @property + def hash(self): + return "#add_dispersion_drude" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 13 - self.hash = "#add_dispersion_drude" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: poles = self.kwargs["poles"] omega = self.kwargs["omega"] @@ -1589,7 +1642,6 @@ class AddDrudeDispersion(UserObjectMulti): raise ValueError # Look up requested materials in existing list of material instances - grid = uip.grid materials = [y for x in material_ids for y in grid.materials if y.ID == x] if len(materials) != len(material_ids): @@ -1633,7 +1685,7 @@ class AddDrudeDispersion(UserObjectMulti): ) -class SoilPeplinski(UserObjectMulti): +class SoilPeplinski(GridUserObject): """Mixing model for soils proposed by Peplinski et al. (http://dx.doi.org/10.1109/36.387598) @@ -1649,12 +1701,18 @@ class SoilPeplinski(UserObjectMulti): id: string used as identifier for soil. """ + @property + def order(self): + return 14 + + @property + def hash(self): + return "#soil_peplinski" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 14 - self.hash = "#soil_peplinski" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: sand_fraction = self.kwargs["sand_fraction"] clay_fraction = self.kwargs["clay_fraction"] @@ -1697,7 +1755,6 @@ class SoilPeplinski(UserObjectMulti): "fraction." ) raise ValueError - grid = uip.grid if any(x.ID == ID for x in grid.mixingmodels): logger.exception(f"{self.params_str()} with ID {ID} already exists") raise ValueError @@ -1724,7 +1781,7 @@ class SoilPeplinski(UserObjectMulti): grid.mixingmodels.append(s) -class MaterialRange(UserObjectMulti): +class MaterialRange(GridUserObject): """Creates varying material properties for stochastic models. Attributes: @@ -1739,12 +1796,18 @@ class MaterialRange(UserObjectMulti): id: string used as identifier for this variable material. """ + @property + def order(self): + return 15 + + @property + def hash(self): + return "#material_range" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 15 - self.hash = "#material_range" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: er_lower = self.kwargs["er_lower"] er_upper = self.kwargs["er_upper"] @@ -1802,7 +1865,6 @@ class MaterialRange(UserObjectMulti): logger.exception( f"{self.params_str()} requires a positive value for the upper range of magnetic loss." ) - grid = uip.grid if any(x.ID == ID for x in grid.mixingmodels): logger.exception(f"{self.params_str()} with ID {ID} already exists") raise ValueError @@ -1825,7 +1887,7 @@ class MaterialRange(UserObjectMulti): grid.mixingmodels.append(s) -class MaterialList(UserObjectMulti): +class MaterialList(GridUserObject): """Creates varying material properties for stochastic models. Attributes: @@ -1833,19 +1895,24 @@ class MaterialList(UserObjectMulti): id: string used as identifier for this variable material. """ + @property + def order(self): + return 15 + + @property + def hash(self): + return "#material_range" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 16 - self.hash = "#material_list" - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: list_of_materials = self.kwargs["list_of_materials"] ID = self.kwargs["id"] except KeyError: logger.exception(f"{self.params_str()} requires at at least 2 parameters.") raise - grid = uip.grid if any(x.ID == ID for x in grid.mixingmodels): logger.exception(f"{self.params_str()} with ID {ID} already exists") raise ValueError @@ -1859,163 +1926,7 @@ class MaterialList(UserObjectMulti): grid.mixingmodels.append(s) -class GeometryView(UserObjectMulti): - """Outputs to file(s) information about the geometry (mesh) of model. - - The geometry information is saved in Visual Toolkit (VTK) formats. - - Attributes: - p1: tuple required for lower left (x,y,z) coordinates of volume of - geometry view in metres. - p2: tuple required for upper right (x,y,z) coordinates of volume of - geometry view in metres. - dl: tuple required for spatial discretisation of geometry view in metres. - output_tuple: string required for per-cell 'n' (normal) or per-cell-edge - 'f' (fine) geometry views. - filename: string required for filename where geometry view will be - stored in the same directory as input file. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 17 - self.hash = "#geometry_view" - - def geometry_view_constructor(self, output_type): - """Selects appropriate class for geometry view dependent on geometry - view type, i.e. normal or fine. - """ - - if output_type == "n": - from gprMax.geometry_outputs import GeometryViewVoxels as GeometryViewUser - else: - from gprMax.geometry_outputs import GeometryViewLines as GeometryViewUser - - return GeometryViewUser - - def build(self, model, 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: - logger.exception(f"{self.params_str()} requires exactly eleven parameters.") - raise - - GeometryViewUser = self.geometry_view_constructor(output_type) - - try: - p3 = uip.round_to_grid_static_point(p1) - p4 = uip.round_to_grid_static_point(p2) - p1, p2 = uip.check_box_points(p1, p2, self.params_str()) - except ValueError: - logger.exception(f"{self.params_str()} point is outside the domain.") - raise - xs, ys, zs = p1 - xf, yf, zf = p2 - - grid = uip.grid - dx, dy, dz = uip.discretise_static_point(dl) - - if dx < 0 or dy < 0 or dz < 0: - logger.exception(f"{self.params_str()} the step size should not be less than zero.") - raise ValueError - if dx > grid.nx or dy > grid.ny or dz > grid.nz: - logger.exception( - f"{self.params_str()} the step size should be less than the domain size." - ) - raise ValueError - if dx < 1 or dy < 1 or dz < 1: - logger.exception( - f"{self.params_str()} the step size should not be less than the spatial discretisation." - ) - raise ValueError - if output_type not in ["n", "f"]: - logger.exception( - f"{self.params_str()} requires type to be either n (normal) or f (fine)." - ) - raise ValueError - if output_type == "f" and ( - dx * grid.dx != grid.dx or dy * grid.dy != grid.dy or dz * grid.dz != grid.dz - ): - logger.exception( - f"{self.params_str()} requires the spatial " - "discretisation for the geometry view to be the " - "same as the model for geometry view of " - "type f (fine)" - ) - raise ValueError - - g = GeometryViewUser(xs, ys, zs, xf, yf, zf, dx, dy, dz, filename, grid) - - logger.info( - f"{self.grid_name(grid)}Geometry view from {p3[0]:g}m, " - f"{p3[1]:g}m, {p3[2]:g}m, to {p4[0]:g}m, {p4[1]:g}m, " - f"{p4[2]:g}m, discretisation {dx * grid.dx:g}m, " - f"{dy * grid.dy:g}m, {dz * grid.dz:g}m, with filename " - f"base {g.filename} created." - ) - - model.geometryviews.append(g) - - -class GeometryObjectsWrite(UserObjectMulti): - """Writes geometry generated in a model to file which can be imported into - other models. - - Attributes: - p1: tuple required for lower left (x,y,z) coordinates of volume of - output in metres. - p2: tuple required for upper right (x,y,z) coordinates of volume of - output in metres. - filename: string required for filename where output will be stored in - the same directory as input file. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.order = 18 - self.hash = "#geometry_objects_write" - - def build(self, model, uip): - grid = uip.grid - if isinstance(grid, SubGridBaseGrid): - logger.exception(f"{self.params_str()} do not add geometry objects to subgrids.") - raise ValueError - try: - p1 = self.kwargs["p1"] - p2 = self.kwargs["p2"] - basefilename = self.kwargs["filename"] - except KeyError: - logger.exception(f"{self.params_str()} requires exactly seven parameters.") - raise - - p1, p2 = uip.check_box_points(p1, p2, self.params_str()) - x0, y0, z0 = p1 - x1, y1, z1 = p2 - - if isinstance(grid, MPIGrid): - geometry_object_type = MPIGeometryObjectsUser - else: - geometry_object_type = GeometryObjectsUser - - g = geometry_object_type(x0, y0, z0, x1, y1, z1, basefilename) - - logger.info( - f"Geometry objects in the volume from {p1[0] * grid.dx:g}m, " - f"{p1[1] * grid.dy:g}m, {p1[2] * grid.dz:g}m, to " - f"{p2[0] * grid.dx:g}m, {p2[1] * grid.dy:g}m, " - f"{p2[2] * grid.dz:g}m, will be written to " - f"{g.filename_hdf5}, with materials written to " - f"{g.filename_materials}" - ) - - model.geometryobjects.append(g) - - -class PMLCFS(UserObjectMulti): +class PMLCFS(GridUserObject): """Controls parameters that are used to build each order of PML. Default values are set in pml.py @@ -2040,11 +1951,18 @@ class PMLCFS(UserObjectMulti): sigmamax: float required for maximum value for the CFS sigma parameter. """ + @property + def order(self): + return 19 + + @property + def hash(self): + return "#pml_cfs" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.order = 19 - def build(self, model, uip): + def build(self, grid: FDTDGrid): try: alphascalingprofile = self.kwargs["alphascalingprofile"] alphascalingdirection = self.kwargs["alphascalingdirection"] @@ -2130,7 +2048,6 @@ class PMLCFS(UserObjectMulti): f"{cfssigma.min:g}, max: {cfssigma.max}) created." ) - grid = uip.grid grid.pmls["cfs"].append(cfs) if len(grid.pmls["cfs"]) > 2: @@ -2145,6 +2062,11 @@ class Subgrid(UserObjectMulti): def __init__(self, **kwargs): super().__init__(**kwargs) + logger.warning( + "Subgrid user object is deprecated and may be removed in" + " future releases of gprMax. Use the SubGridHSG user object" + " instead." + ) self.children_multiple = [] self.children_geometry = [] diff --git a/gprMax/user_objects/cmds_output.py b/gprMax/user_objects/cmds_output.py new file mode 100644 index 00000000..6a9ca251 --- /dev/null +++ b/gprMax/user_objects/cmds_output.py @@ -0,0 +1,180 @@ +import logging + +from gprMax.geometry_outputs import GeometryObjects as GeometryObjectsUser +from gprMax.geometry_outputs import MPIGeometryObjects as MPIGeometryObjectsUser +from gprMax.grid.fdtd_grid import FDTDGrid +from gprMax.grid.mpi_grid import MPIGrid +from gprMax.model import Model +from gprMax.subgrids.grid import SubGridBaseGrid +from gprMax.user_objects.user_objects import OutputUserObject + +logger = logging.getLogger(__name__) + + +class GeometryView(OutputUserObject): + """Outputs to file(s) information about the geometry (mesh) of model. + + The geometry information is saved in Visual Toolkit (VTK) formats. + + Attributes: + p1: tuple required for lower left (x,y,z) coordinates of volume of + geometry view in metres. + p2: tuple required for upper right (x,y,z) coordinates of volume of + geometry view in metres. + dl: tuple required for spatial discretisation of geometry view in metres. + output_tuple: string required for per-cell 'n' (normal) or per-cell-edge + 'f' (fine) geometry views. + filename: string required for filename where geometry view will be + stored in the same directory as input file. + """ + + @property + def order(self): + return 17 + + @property + def hash(self): + return "#geometry_view" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def geometry_view_constructor(self, output_type): + """Selects appropriate class for geometry view dependent on geometry + view type, i.e. normal or fine. + """ + + if output_type == "n": + from gprMax.geometry_outputs import GeometryViewVoxels as GeometryViewUser + else: + from gprMax.geometry_outputs import GeometryViewLines as GeometryViewUser + + return GeometryViewUser + + def build(self, model: Model, grid: FDTDGrid): + 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: + logger.exception(f"{self.params_str()} requires exactly eleven parameters.") + raise + + GeometryViewUser = self.geometry_view_constructor(output_type) + + uip = self._create_uip(grid) + try: + p3 = uip.round_to_grid_static_point(p1) + p4 = uip.round_to_grid_static_point(p2) + p1, p2 = uip.check_box_points(p1, p2, self.params_str()) + except ValueError: + logger.exception(f"{self.params_str()} point is outside the domain.") + raise + xs, ys, zs = p1 + xf, yf, zf = p2 + + dx, dy, dz = uip.discretise_static_point(dl) + + if dx < 0 or dy < 0 or dz < 0: + logger.exception(f"{self.params_str()} the step size should not be less than zero.") + raise ValueError + if dx > grid.nx or dy > grid.ny or dz > grid.nz: + logger.exception( + f"{self.params_str()} the step size should be less than the domain size." + ) + raise ValueError + if dx < 1 or dy < 1 or dz < 1: + logger.exception( + f"{self.params_str()} the step size should not be less than the spatial discretisation." + ) + raise ValueError + if output_type not in ["n", "f"]: + logger.exception( + f"{self.params_str()} requires type to be either n (normal) or f (fine)." + ) + raise ValueError + if output_type == "f" and ( + dx * grid.dx != grid.dx or dy * grid.dy != grid.dy or dz * grid.dz != grid.dz + ): + logger.exception( + f"{self.params_str()} requires the spatial " + "discretisation for the geometry view to be the " + "same as the model for geometry view of " + "type f (fine)" + ) + raise ValueError + + g = GeometryViewUser(xs, ys, zs, xf, yf, zf, dx, dy, dz, filename, grid) + + logger.info( + f"{self.grid_name(grid)}Geometry view from {p3[0]:g}m, " + f"{p3[1]:g}m, {p3[2]:g}m, to {p4[0]:g}m, {p4[1]:g}m, " + f"{p4[2]:g}m, discretisation {dx * grid.dx:g}m, " + f"{dy * grid.dy:g}m, {dz * grid.dz:g}m, with filename " + f"base {g.filename} created." + ) + + model.geometryviews.append(g) + + +class GeometryObjectsWrite(OutputUserObject): + """Writes geometry generated in a model to file which can be imported into + other models. + + Attributes: + p1: tuple required for lower left (x,y,z) coordinates of volume of + output in metres. + p2: tuple required for upper right (x,y,z) coordinates of volume of + output in metres. + filename: string required for filename where output will be stored in + the same directory as input file. + """ + + @property + def order(self): + return 18 + + @property + def hash(self): + return "#geometry_objects_write" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def build(self, model: Model, grid: FDTDGrid): + if isinstance(grid, SubGridBaseGrid): + logger.exception(f"{self.params_str()} do not add geometry objects to subgrids.") + raise ValueError + try: + p1 = self.kwargs["p1"] + p2 = self.kwargs["p2"] + basefilename = self.kwargs["filename"] + except KeyError: + logger.exception(f"{self.params_str()} requires exactly seven parameters.") + raise + + uip = self._create_uip(grid) + p1, p2 = uip.check_box_points(p1, p2, self.params_str()) + x0, y0, z0 = p1 + x1, y1, z1 = p2 + + # TODO: Remove these when add parallel build + if isinstance(grid, MPIGrid): + geometry_object_type = MPIGeometryObjectsUser + else: + geometry_object_type = GeometryObjectsUser + + g = geometry_object_type(x0, y0, z0, x1, y1, z1, basefilename) + + logger.info( + f"Geometry objects in the volume from {p1[0] * grid.dx:g}m, " + f"{p1[1] * grid.dy:g}m, {p1[2] * grid.dz:g}m, to " + f"{p2[0] * grid.dx:g}m, {p2[1] * grid.dy:g}m, " + f"{p2[2] * grid.dz:g}m, will be written to " + f"{g.filename_hdf5}, with materials written to " + f"{g.filename_materials}" + ) + + model.geometryobjects.append(g) diff --git a/gprMax/user_objects/cmds_singleuse.py b/gprMax/user_objects/cmds_singleuse.py index edc0fcce..4df63bff 100644 --- a/gprMax/user_objects/cmds_singleuse.py +++ b/gprMax/user_objects/cmds_singleuse.py @@ -253,7 +253,7 @@ class TimeWindow(ModelUserObject): if self.time is not None and self.iterations is not None: logger.warning( - f"{self._params_str()} Time and iterations were both specified, using 'time'" + f"{self.params_str()} Time and iterations were both specified, using 'time'" ) logger.info(f"Time window: {model.timewindow:g} secs ({model.iterations} iterations)") diff --git a/gprMax/user_objects/rotatable.py b/gprMax/user_objects/rotatable.py index d29f689e..9f6fa2c7 100644 --- a/gprMax/user_objects/rotatable.py +++ b/gprMax/user_objects/rotatable.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from typing import Optional, Tuple +from gprMax.grid.fdtd_grid import FDTDGrid + class Rotatable(ABC): """Stores parameters and defines an interface for rotatable objects. @@ -38,6 +40,6 @@ class Rotatable(ABC): self.do_rotate = True @abstractmethod - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Performs the rotation.""" pass diff --git a/gprMax/user_objects/user_objects.py b/gprMax/user_objects/user_objects.py index e9b35e22..c46ca170 100644 --- a/gprMax/user_objects/user_objects.py +++ b/gprMax/user_objects/user_objects.py @@ -3,7 +3,6 @@ from typing import List, Union from gprMax import config from gprMax.grid.fdtd_grid import FDTDGrid -from gprMax.grid.mpi_grid import MPIGrid from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.user_inputs import MainGridUserInput, SubgridUserInput @@ -60,7 +59,7 @@ class UserObject(ABC): return f"{self.hash}: {' '.join(args)}" - def _params_str(self) -> str: + def params_str(self) -> str: """Readable string of parameters given to object.""" return f"{self.hash}: {str(self.kwargs)}" @@ -115,10 +114,42 @@ class GridUserObject(MultiUserObject): def build(self, grid: FDTDGrid): pass + def grid_name(self, grid: FDTDGrid) -> str: + """Format grid name for use with logging info. + + Returns an empty string if the grid is the main grid. + + Args: + grid: Grid to get the name of. + + Returns: + grid_name: Formatted version of the grid name. + """ + if isinstance(grid, SubGridBaseGrid): + return f"[{grid.name}] " + else: + return "" + class OutputUserObject(MultiUserObject): """User defined object that controls the output of data.""" + def grid_name(self, grid: FDTDGrid) -> str: + """Format grid name for use with logging info. + + Returns an empty string if the grid is the main grid. + + Args: + grid: Grid to get the name of. + + Returns: + grid_name: Formatted version of the grid name. + """ + if isinstance(grid, SubGridBaseGrid): + return f"[{grid.name}] " + else: + return "" + @abstractmethod def build(self, model: Model, grid: FDTDGrid): pass From bad5aa7ce0200f3eb8139827a8eac4f5bb95f64c Mon Sep 17 00:00:00 2001 From: nmannall Date: Tue, 10 Dec 2024 14:46:58 +0000 Subject: [PATCH 07/20] Reorder methods for consistency --- gprMax/user_objects/user_objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gprMax/user_objects/user_objects.py b/gprMax/user_objects/user_objects.py index c46ca170..9e84d032 100644 --- a/gprMax/user_objects/user_objects.py +++ b/gprMax/user_objects/user_objects.py @@ -134,6 +134,10 @@ class GridUserObject(MultiUserObject): class OutputUserObject(MultiUserObject): """User defined object that controls the output of data.""" + @abstractmethod + def build(self, model: Model, grid: FDTDGrid): + pass + def grid_name(self, grid: FDTDGrid) -> str: """Format grid name for use with logging info. @@ -150,10 +154,6 @@ class OutputUserObject(MultiUserObject): else: return "" - @abstractmethod - def build(self, model: Model, grid: FDTDGrid): - pass - class GeometryUserObject(GridUserObject): """User defined object that adds geometry to a grid.""" From 7350532683ba8723f40400024c35c2ba9e36d5fc Mon Sep 17 00:00:00 2001 From: nmannall Date: Tue, 10 Dec 2024 14:48:18 +0000 Subject: [PATCH 08/20] Remove old UserObjectMulti class --- gprMax/user_objects/cmds_multiuse.py | 48 ---------------------------- 1 file changed, 48 deletions(-) diff --git a/gprMax/user_objects/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py index 2e4eaed4..fad7ded9 100644 --- a/gprMax/user_objects/cmds_multiuse.py +++ b/gprMax/user_objects/cmds_multiuse.py @@ -59,54 +59,6 @@ from gprMax.waveforms import Waveform as WaveformUser logger = logging.getLogger(__name__) -class UserObjectMulti(ABC): - """Object that can occur multiple times in a model.""" - - def __init__(self, **kwargs): - self.kwargs = kwargs - self.order = 0 - self.hash = None - self.autotranslate = True - self.do_rotate = False - - def __str__(self): - """Readable user string as per hash commands.""" - s = "" - for _, v in self.kwargs.items(): - if isinstance(v, (tuple, list)): - v = " ".join([str(el) for el in v]) - s += f"{str(v)} " - - return f"{self.hash}: {s[:-1]}" - - @abstractmethod - def build(self, model: Model, uip: MainGridUserInput): - """Creates object and adds it to model.""" - pass - - # TODO: Make _do_rotate not use a grid object - def rotate(self, axis, angle, origin=None): - """Rotates object (specialised for each object).""" - pass - - def params_str(self): - """Readable string of parameters given to object.""" - return f"{self.hash}: {str(self.kwargs)}" - - def grid_name(self, grid: FDTDGrid) -> str: - """Returns subgrid name for use with logging info. Returns an empty - string if the grid is the main grid. - """ - if isinstance(grid, SubGridBaseGrid): - return f"[{grid.name}] " - else: - return "" - - def model_name(self, model: Model) -> str: - """Returns model name for use with logging info.""" - return f"[{model.title}] " - - class ExcitationFile(GridUserObject): """Specify file containing amplitude values of custom waveforms. From 09a37beea42607adf667c9a42f116b161b7fca67 Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:01:36 +0000 Subject: [PATCH 09/20] Refactor scene.py to use new user object classes --- gprMax/scene.py | 226 ++++++++++++---------------- gprMax/subgrids/user_objects.py | 31 ++-- gprMax/user_objects/user_objects.py | 27 +--- 3 files changed, 126 insertions(+), 158 deletions(-) diff --git a/gprMax/scene.py b/gprMax/scene.py index 96b99506..296fa5b6 100644 --- a/gprMax/scene.py +++ b/gprMax/scene.py @@ -16,22 +16,24 @@ # You should have received a copy of the GNU General Public License # along with gprMax. If not, see . import logging -from typing import List, Optional, Union +from typing import List, Sequence -from gprMax import config from gprMax.cmds_geometry.add_grass import AddGrass from gprMax.cmds_geometry.add_surface_roughness import AddSurfaceRoughness from gprMax.cmds_geometry.add_surface_water import AddSurfaceWater -from gprMax.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.cmds_geometry.fractal_box import FractalBox from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_built_in_materials from gprMax.model import Model -from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.subgrids.user_objects import SubGridBase as SubGridUserBase -from gprMax.user_inputs import MainGridUserInput, SubgridUserInput -from gprMax.user_objects.cmds_multiuse import UserObjectMulti -from gprMax.user_objects.cmds_singleuse import Discretisation, Domain, TimeWindow, UserObjectSingle +from gprMax.user_objects.cmds_singleuse import Discretisation, Domain, TimeWindow +from gprMax.user_objects.user_objects import ( + GeometryUserObject, + GridUserObject, + ModelUserObject, + OutputUserObject, + UserObject, +) logger = logging.getLogger(__name__) @@ -39,128 +41,85 @@ logger = logging.getLogger(__name__) class Scene: """Scene stores all of the user created objects.""" - def __init__(self): - self.multiple_cmds: List[UserObjectMulti] = [] - self.single_cmds: List[UserObjectSingle] = [] - self.geometry_cmds: List[UserObjectGeometry] = [] - self.multiple_cmds: List[UserObjectMulti] = [] - self.single_cmds: List[UserObjectSingle] = [] - self.geometry_cmds: List[UserObjectGeometry] = [] - self.essential_cmds = [Domain, TimeWindow, Discretisation] + ESSENTIAL_CMDS = [Domain, TimeWindow, Discretisation] - def add(self, user_object): + def __init__(self): + self.single_use_objects: List[ModelUserObject] = [] + self.grid_objects: List[GridUserObject] = [] + self.geometry_objects: List[GeometryUserObject] = [] + self.output_objects: List[OutputUserObject] = [] + self.subgrid_objects: List[SubGridUserBase] = [] + + def add(self, user_object: UserObject): """Add the user object to the scene. Args: user_object: user object to add to the scene. For example, - :class:`gprMax.cmds_single_use.Domain` + `gprMax.user_objects.cmds_singleuse.Domain` """ - if isinstance(user_object, UserObjectMulti): - self.multiple_cmds.append(user_object) - elif isinstance(user_object, UserObjectGeometry): - self.geometry_cmds.append(user_object) - elif isinstance(user_object, UserObjectSingle): - self.single_cmds.append(user_object) + # Check for + if isinstance(user_object, SubGridUserBase): + self.subgrid_objects.append(user_object) + elif isinstance(user_object, ModelUserObject): + self.single_use_objects.append(user_object) + elif isinstance(user_object, GeometryUserObject): + self.geometry_objects.append(user_object) + elif isinstance(user_object, GridUserObject): + self.grid_objects.append(user_object) + elif isinstance(user_object, OutputUserObject): + self.output_objects.append(user_object) else: - logger.exception("This object is unknown to gprMax") - raise ValueError + raise TypeError(f"Object of type '{type(user_object)}' is unknown to gprMax") - def build_grid_obj(self, obj: UserObjectGeometry, grid: FDTDGrid): - """Builds objects in FDTDGrids. - - Args: - obj: user object - grid: FDTDGrid class describing a grid in a model. - """ - uip = create_user_input_points(grid, obj) - try: - obj.build(grid, uip) - except ValueError: - logger.exception("Error creating user input object") - raise - - def build_model_obj( - self, - obj: Union[UserObjectSingle, UserObjectMulti], - model: Model, - subgrid: Optional[FDTDGrid] = None, - ): + def build_model_objects(self, objects: Sequence[ModelUserObject], model: Model): """Builds objects in models. Args: obj: user object model: Model being built """ - - grid = model.G if subgrid is None else subgrid - uip = create_user_input_points(grid, obj) - try: - obj.build(model, uip) + for model_user_object in sorted(objects): + model_user_object.build(model) except ValueError: - logger.exception("Error creating user input object") + logger.exception(f"Error creating user object '{model_user_object}'") raise - def process_subgrid_cmds(self, model: Model): - """Process all commands in any sub-grids.""" + def build_grid_objects(self, objects: Sequence[GridUserObject], grid: FDTDGrid): + """Builds objects in FDTDGrids. - # Subgrid user objects - subgrid_cmds = [ - sg_cmd for sg_cmd in self.multiple_cmds if isinstance(sg_cmd, SubGridUserBase) - ] - subgrid_cmds = [ - sg_cmd for sg_cmd in self.multiple_cmds if isinstance(sg_cmd, SubGridUserBase) - ] + Args: + objects: user object + grid: FDTDGrid class describing a grid in a model. + """ + try: + for grid_user_object in sorted(objects): + grid_user_object.build(grid) + except ValueError: + logger.exception(f"Error creating user object '{grid_user_object}'") + raise - # 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, model, sg) - self.process_geocmds(sg_cmd.children_geometry, sg) - - def process_cmds( - self, - commands: Union[List[UserObjectMulti], List[UserObjectSingle]], - model: Model, - subgrid: Optional[SubGridBaseGrid] = None, + def build_output_objects( + self, objects: Sequence[OutputUserObject], model: Model, grid: FDTDGrid ): - """Process list of commands.""" - cmds_sorted = sorted(commands, key=lambda cmd: cmd.order) - for obj in cmds_sorted: - self.build_model_obj(obj, model, subgrid) + try: + for output_user_object in sorted(objects): + output_user_object.build(model, grid) + except ValueError: + logger.exception(f"Error creating user object '{output_user_object}'") + raise - return self - - def process_geocmds(self, commands, grid): - # Check for fractal boxes and modifications and pre-process them first - proc_cmds = [] - for obj in commands: - if isinstance(obj, (FractalBox, AddGrass, AddSurfaceRoughness, AddSurfaceWater)): - self.build_grid_obj(obj, grid) - if isinstance(obj, (FractalBox)): - proc_cmds.append(obj) - else: - proc_cmds.append(obj) - - # Process all geometry commands - for obj in proc_cmds: - self.build_grid_obj(obj, grid) - - return self - - def process_singlecmds(self, model: Model): + def process_single_use_objects(self, model: Model): # 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): + # TODO: Test this works + unique_commands = list(set(self.single_use_objects)) + if len(unique_commands) != len(self.single_use_objects): logger.exception("Duplicate single-use commands exist in the input.") raise ValueError # Check essential commands and warn user if missing - for cmd_type in self.essential_cmds: - d = any(isinstance(cmd, cmd_type) for cmd in cmds_unique) + for cmd_type in self.ESSENTIAL_CMDS: + d = any(isinstance(cmd, cmd_type) for cmd in unique_commands) if not d: logger.exception( "Your input file is missing essential commands " @@ -169,7 +128,39 @@ class Scene: ) raise ValueError - self.process_cmds(cmds_unique, model) + self.build_model_objects(unique_commands, model) + + def process_multi_use_objects(self, model: Model): + self.build_grid_objects(self.grid_objects, model.G) + self.build_output_objects(self.output_objects, model, model.G) + self.build_model_objects(self.subgrid_objects, model) + + def process_geometry_objects(self, geometry_objects: List[GeometryUserObject], grid: FDTDGrid): + # Check for fractal boxes and modifications and pre-process them first + # TODO: Can this be removed in favour of sorting geometry objects? + objects_to_be_built: List[GeometryUserObject] = [] + for obj in geometry_objects: + if isinstance(obj, (FractalBox, AddGrass, AddSurfaceRoughness, AddSurfaceWater)): + self.build_grid_objects([obj], grid) + if isinstance(obj, (FractalBox)): + objects_to_be_built.append(obj) + else: + objects_to_be_built.append(obj) + + # Process all geometry commands + self.build_grid_objects(objects_to_be_built, grid) + + def process_subgrid_objects(self, model: Model): + """Process all commands in any sub-grids.""" + # Iterate through the user command objects under the subgrid user object + for subgrid_object in self.subgrid_objects: + # 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. + subgrid = subgrid_object.subgrid + self.build_grid_objects(subgrid_object.children_grid, subgrid) + self.build_output_objects(subgrid_object.children_output, model, subgrid) + self.process_geometry_objects(subgrid_object.children_geometry, subgrid) def create_internal_objects(self, model: Model): """Calls the UserObject.build() function in the correct way - API @@ -181,36 +172,17 @@ class Scene: create_built_in_materials(model.G) # Process commands that can only have a single instance - self.process_singlecmds(model) + self.process_single_use_objects(model) - # Process main grid multiple commands - self.process_cmds(self.multiple_cmds, model) + # Process multiple commands + self.process_multi_use_objects(model) # Initialise geometry arrays for main and subgrids for grid in [model.G] + model.subgrids: grid.initialise_geometry_arrays() # Process the main grid geometry commands - self.process_geocmds(self.geometry_cmds, model.G) + self.process_geometry_objects(self.geometry_objects, model.G) # Process all the commands for subgrids - self.process_subgrid_cmds(model) - - -def create_user_input_points( - grid: FDTDGrid, user_obj: Union[UserObjectSingle, UserObjectMulti, UserObjectGeometry] -) -> Union[MainGridUserInput, SubgridUserInput]: - """Returns a point checker class based on the grid supplied.""" - - if isinstance(grid, SubGridBaseGrid): - # Local object configuration trumps. User can turn off autotranslate for - # specific objects. - if not user_obj.autotranslate and config.sim_config.args.autotranslate: - return MainGridUserInput(grid) - - if config.sim_config.args.autotranslate: - return SubgridUserInput(grid) - else: - return MainGridUserInput(grid) - else: - return MainGridUserInput(grid) + self.process_subgrid_objects(model) diff --git a/gprMax/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py index b7c0582e..dc05836c 100644 --- a/gprMax/subgrids/user_objects.py +++ b/gprMax/subgrids/user_objects.py @@ -28,29 +28,40 @@ from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.subgrids.subgrid_hsg import SubGridHSG as SubGridHSGUser from gprMax.user_inputs import MainGridUserInput -from gprMax.user_objects.cmds_multiuse import UserObjectMulti +from gprMax.user_objects.user_objects import ( + GeometryUserObject, + GridUserObject, + ModelUserObject, + OutputUserObject, + UserObject, +) logger = logging.getLogger(__name__) -class SubGridBase(UserObjectMulti): +class SubGridBase(ModelUserObject): """Allows UserObjectMulti and UserObjectGeometry to be nested in SubGrid type user objects. """ + @property + def is_single_use(self) -> bool: + return False + def __init__(self, **kwargs): super().__init__(**kwargs) - self.children_multiple: List[UserObjectMulti] = [] - self.children_geometry: List[UserObjectGeometry] = [] - self.children_multiple: List[UserObjectMulti] = [] - self.children_geometry: List[UserObjectGeometry] = [] + self.children_grid: List[GridUserObject] = [] + self.children_geometry: List[GeometryUserObject] = [] + self.children_output: List[OutputUserObject] = [] - def add(self, node: Union[UserObjectMulti, UserObjectGeometry]): + def add(self, node: UserObject): """Adds other user objects. Geometry and multi only.""" - if isinstance(node, UserObjectMulti): - self.children_multiple.append(node) - elif isinstance(node, UserObjectGeometry): + if isinstance(node, GeometryUserObject): self.children_geometry.append(node) + elif isinstance(node, GridUserObject): + self.children_grid.append(node) + elif isinstance(node, OutputUserObject): + self.children_output.append(node) else: logger.exception(f"{str(node)} this Object can not be added to a sub grid") raise ValueError diff --git a/gprMax/user_objects/user_objects.py b/gprMax/user_objects/user_objects.py index 9e84d032..50ff33c5 100644 --- a/gprMax/user_objects/user_objects.py +++ b/gprMax/user_objects/user_objects.py @@ -35,18 +35,13 @@ class UserObject(ABC): def hash(self) -> str: pass - @property - def is_single_use(self) -> bool: - return True - - @property - def is_geometry_object(self) -> bool: - return False - def __init__(self, **kwargs) -> None: self.kwargs = kwargs self.autotranslate = True + def __lt__(self, obj: "UserObject"): + return self.order < obj.order + def __str__(self) -> str: """Readable user object as per hash commands.""" args: List[str] = [] @@ -86,14 +81,6 @@ class UserObject(ABC): return MainGridUserInput(grid) -class MultiUserObject(UserObject): - """User defined object that can occur multiple times.""" - - @property - def is_single_use(self) -> bool: - return False - - class ModelUserObject(UserObject): """User defined object to add to the model.""" @@ -107,7 +94,7 @@ class ModelUserObject(UserObject): pass -class GridUserObject(MultiUserObject): +class GridUserObject(UserObject): """User defined object to add to a grid.""" @abstractmethod @@ -131,7 +118,7 @@ class GridUserObject(MultiUserObject): return "" -class OutputUserObject(MultiUserObject): +class OutputUserObject(UserObject): """User defined object that controls the output of data.""" @abstractmethod @@ -158,6 +145,4 @@ class OutputUserObject(MultiUserObject): class GeometryUserObject(GridUserObject): """User defined object that adds geometry to a grid.""" - @property - def is_geometry_object(self) -> bool: - return True + pass From 09e6fe923a7642dd2a8983217f3e0e22c32fe702 Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:08:56 +0000 Subject: [PATCH 10/20] Move cmds_geometry into user_objects folder --- gprMax/__init__.py | 28 ++++---- gprMax/hash_cmds_geometry.py | 68 ++++++++++++------- .../cmds_geometry/__init__.py | 0 .../cmds_geometry/add_grass.py | 7 +- .../cmds_geometry/add_surface_roughness.py | 5 +- .../cmds_geometry/add_surface_water.py | 5 +- .../{ => user_objects}/cmds_geometry/box.py | 4 +- .../cmds_geometry/build_templates.py | 0 .../cmds_geometry/cmds_geometry.py | 0 .../{ => user_objects}/cmds_geometry/cone.py | 5 +- .../cmds_geometry/cylinder.py | 5 +- .../cmds_geometry/cylindrical_sector.py | 5 +- .../{ => user_objects}/cmds_geometry/edge.py | 4 +- .../cmds_geometry/ellipsoid.py | 5 +- .../cmds_geometry/fractal_box.py | 0 .../cmds_geometry/geometry_objects_read.py | 6 +- .../{ => user_objects}/cmds_geometry/plate.py | 4 +- .../cmds_geometry/sphere.py | 5 +- .../cmds_geometry/triangle.py | 5 +- 19 files changed, 95 insertions(+), 66 deletions(-) rename gprMax/{ => user_objects}/cmds_geometry/__init__.py (100%) rename gprMax/{ => user_objects}/cmds_geometry/add_grass.py (96%) rename gprMax/{ => user_objects}/cmds_geometry/add_surface_roughness.py (96%) rename gprMax/{ => user_objects}/cmds_geometry/add_surface_water.py (96%) rename gprMax/{ => user_objects}/cmds_geometry/box.py (95%) rename gprMax/{ => user_objects}/cmds_geometry/build_templates.py (100%) rename gprMax/{ => user_objects}/cmds_geometry/cmds_geometry.py (100%) rename gprMax/{ => user_objects}/cmds_geometry/cone.py (98%) rename gprMax/{ => user_objects}/cmds_geometry/cylinder.py (95%) rename gprMax/{ => user_objects}/cmds_geometry/cylindrical_sector.py (96%) rename gprMax/{ => user_objects}/cmds_geometry/edge.py (94%) rename gprMax/{ => user_objects}/cmds_geometry/ellipsoid.py (98%) rename gprMax/{ => user_objects}/cmds_geometry/fractal_box.py (100%) rename gprMax/{ => user_objects}/cmds_geometry/geometry_objects_read.py (97%) rename gprMax/{ => user_objects}/cmds_geometry/plate.py (95%) rename gprMax/{ => user_objects}/cmds_geometry/sphere.py (95%) rename gprMax/{ => user_objects}/cmds_geometry/triangle.py (96%) diff --git a/gprMax/__init__.py b/gprMax/__init__.py index f8e613ad..1a1aa4c5 100644 --- a/gprMax/__init__.py +++ b/gprMax/__init__.py @@ -10,23 +10,23 @@ Electromagnetic wave propagation simulation software. import gprMax.config as config from ._version import __version__ -from .cmds_geometry.add_grass import AddGrass -from .cmds_geometry.add_surface_roughness import AddSurfaceRoughness -from .cmds_geometry.add_surface_water import AddSurfaceWater -from .cmds_geometry.box import Box -from .cmds_geometry.cone import Cone -from .cmds_geometry.cylinder import Cylinder -from .cmds_geometry.cylindrical_sector import CylindricalSector -from .cmds_geometry.edge import Edge -from .cmds_geometry.ellipsoid import Ellipsoid -from .cmds_geometry.fractal_box import FractalBox -from .cmds_geometry.geometry_objects_read import GeometryObjectsRead -from .cmds_geometry.plate import Plate -from .cmds_geometry.sphere import Sphere -from .cmds_geometry.triangle import Triangle from .gprMax import run as run from .scene import Scene from .subgrids.user_objects import SubGridHSG +from .user_objects.cmds_geometry.add_grass import AddGrass +from .user_objects.cmds_geometry.add_surface_roughness import AddSurfaceRoughness +from .user_objects.cmds_geometry.add_surface_water import AddSurfaceWater +from .user_objects.cmds_geometry.box import Box +from .user_objects.cmds_geometry.cone import Cone +from .user_objects.cmds_geometry.cylinder import Cylinder +from .user_objects.cmds_geometry.cylindrical_sector import CylindricalSector +from .user_objects.cmds_geometry.edge import Edge +from .user_objects.cmds_geometry.ellipsoid import Ellipsoid +from .user_objects.cmds_geometry.fractal_box import FractalBox +from .user_objects.cmds_geometry.geometry_objects_read import GeometryObjectsRead +from .user_objects.cmds_geometry.plate import Plate +from .user_objects.cmds_geometry.sphere import Sphere +from .user_objects.cmds_geometry.triangle import Triangle from .user_objects.cmds_multiuse import ( PMLCFS, AddDebyeDispersion, diff --git a/gprMax/hash_cmds_geometry.py b/gprMax/hash_cmds_geometry.py index 1d53b980..1cb068d3 100644 --- a/gprMax/hash_cmds_geometry.py +++ b/gprMax/hash_cmds_geometry.py @@ -20,20 +20,20 @@ import logging import numpy as np -from .cmds_geometry.add_grass import AddGrass -from .cmds_geometry.add_surface_roughness import AddSurfaceRoughness -from .cmds_geometry.add_surface_water import AddSurfaceWater -from .cmds_geometry.box import Box -from .cmds_geometry.cmds_geometry import check_averaging -from .cmds_geometry.cone import Cone -from .cmds_geometry.cylinder import Cylinder -from .cmds_geometry.cylindrical_sector import CylindricalSector -from .cmds_geometry.edge import Edge -from .cmds_geometry.ellipsoid import Ellipsoid -from .cmds_geometry.fractal_box import FractalBox -from .cmds_geometry.plate import Plate -from .cmds_geometry.sphere import Sphere -from .cmds_geometry.triangle import Triangle +from .user_objects.cmds_geometry.add_grass import AddGrass +from .user_objects.cmds_geometry.add_surface_roughness import AddSurfaceRoughness +from .user_objects.cmds_geometry.add_surface_water import AddSurfaceWater +from .user_objects.cmds_geometry.box import Box +from .user_objects.cmds_geometry.cmds_geometry import check_averaging +from .user_objects.cmds_geometry.cone import Cone +from .user_objects.cmds_geometry.cylinder import Cylinder +from .user_objects.cmds_geometry.cylindrical_sector import CylindricalSector +from .user_objects.cmds_geometry.edge import Edge +from .user_objects.cmds_geometry.ellipsoid import Ellipsoid +from .user_objects.cmds_geometry.fractal_box import FractalBox +from .user_objects.cmds_geometry.plate import Plate +from .user_objects.cmds_geometry.sphere import Sphere +from .user_objects.cmds_geometry.triangle import Triangle from .utilities.utilities import round_value logger = logging.getLogger(__name__) @@ -57,8 +57,7 @@ def process_geometrycmds(geometry): tmp = object.split() if tmp[0] == "#geometry_objects_read:": - from .cmds_geometry.geometry_objects_read import \ - GeometryObjectsRead + from .user_objects.cmds_geometry.geometry_objects_read import GeometryObjectsRead if len(tmp) != 6: logger.exception("'" + " ".join(tmp) + "'" + " requires exactly five parameters") @@ -126,7 +125,14 @@ def process_geometrycmds(geometry): # Isotropic case with user specified averaging elif len(tmp) == 13: averaging = check_averaging(tmp[12].lower()) - triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11], averaging=averaging) + triangle = Triangle( + p1=p1, + p2=p2, + p3=p3, + thickness=thickness, + material_id=tmp[11], + averaging=averaging, + ) # Uniaxial anisotropic case elif len(tmp) == 14: @@ -330,7 +336,9 @@ def process_geometrycmds(geometry): # Isotropic case with user specified averaging elif len(tmp) == 9: averaging = check_averaging(tmp[8].lower()) - ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr, material_id=tmp[7], averaging=averaging) + ellipsoid = Ellipsoid( + p1=p1, xr=xr, yr=yr, zr=zr, material_id=tmp[7], averaging=averaging + ) # Uniaxial anisotropic case elif len(tmp) == 8: @@ -346,7 +354,9 @@ def process_geometrycmds(geometry): # Default is no dielectric smoothing for a fractal box if len(tmp) < 14: - logger.exception("'" + " ".join(tmp) + "'" + " requires at least thirteen parameters") + logger.exception( + "'" + " ".join(tmp) + "'" + " requires at least thirteen parameters" + ) raise ValueError p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) @@ -402,7 +412,9 @@ def process_geometrycmds(geometry): if tmp[0] == "#add_surface_roughness:": if len(tmp) < 13: - logger.exception("'" + " ".join(tmp) + "'" + " requires at least twelve parameters") + logger.exception( + "'" + " ".join(tmp) + "'" + " requires at least twelve parameters" + ) raise ValueError p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) @@ -432,14 +444,18 @@ def process_geometrycmds(geometry): seed=int(tmp[13]), ) else: - logger.exception("'" + " ".join(tmp) + "'" + " too many parameters have been given") + logger.exception( + "'" + " ".join(tmp) + "'" + " too many parameters have been given" + ) raise ValueError scene_objects.append(asr) if tmp[0] == "#add_surface_water:": if len(tmp) != 9: - logger.exception("'" + " ".join(tmp) + "'" + " requires exactly eight parameters") + logger.exception( + "'" + " ".join(tmp) + "'" + " requires exactly eight parameters" + ) raise ValueError p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) @@ -452,7 +468,9 @@ def process_geometrycmds(geometry): if tmp[0] == "#add_grass:": if len(tmp) < 12: - logger.exception("'" + " ".join(tmp) + "'" + " requires at least eleven parameters") + logger.exception( + "'" + " ".join(tmp) + "'" + " requires at least eleven parameters" + ) raise ValueError p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3])) @@ -482,7 +500,9 @@ def process_geometrycmds(geometry): seed=int(tmp[12]), ) else: - logger.exception("'" + " ".join(tmp) + "'" + " too many parameters have been given") + logger.exception( + "'" + " ".join(tmp) + "'" + " too many parameters have been given" + ) raise ValueError scene_objects.append(grass) diff --git a/gprMax/cmds_geometry/__init__.py b/gprMax/user_objects/cmds_geometry/__init__.py similarity index 100% rename from gprMax/cmds_geometry/__init__.py rename to gprMax/user_objects/cmds_geometry/__init__.py diff --git a/gprMax/cmds_geometry/add_grass.py b/gprMax/user_objects/cmds_geometry/add_grass.py similarity index 96% rename from gprMax/cmds_geometry/add_grass.py rename to gprMax/user_objects/cmds_geometry/add_grass.py index fa263173..517b39e5 100644 --- a/gprMax/cmds_geometry/add_grass.py +++ b/gprMax/user_objects/cmds_geometry/add_grass.py @@ -20,9 +20,10 @@ import logging import numpy as np -from ..fractals import FractalSurface, Grass -from ..materials import create_grass -from ..utilities.utilities import round_value +from gprMax.fractals import FractalSurface, Grass +from gprMax.materials import create_grass +from gprMax.utilities.utilities import round_value + from .cmds_geometry import UserObjectGeometry, rotate_2point_object logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/add_surface_roughness.py b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py similarity index 96% rename from gprMax/cmds_geometry/add_surface_roughness.py rename to gprMax/user_objects/cmds_geometry/add_surface_roughness.py index a127a1ae..735def56 100644 --- a/gprMax/cmds_geometry/add_surface_roughness.py +++ b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..fractals import FractalSurface -from ..utilities.utilities import round_value +from gprMax.fractals import FractalSurface +from gprMax.utilities.utilities import round_value + from .cmds_geometry import UserObjectGeometry, rotate_2point_object logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/add_surface_water.py b/gprMax/user_objects/cmds_geometry/add_surface_water.py similarity index 96% rename from gprMax/cmds_geometry/add_surface_water.py rename to gprMax/user_objects/cmds_geometry/add_surface_water.py index 79a6af72..79ec3df5 100644 --- a/gprMax/cmds_geometry/add_surface_water.py +++ b/gprMax/user_objects/cmds_geometry/add_surface_water.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..materials import create_water -from ..utilities.utilities import round_value +from gprMax.materials import create_water +from gprMax.utilities.utilities import round_value + from .cmds_geometry import UserObjectGeometry, rotate_2point_object logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/box.py b/gprMax/user_objects/cmds_geometry/box.py similarity index 95% rename from gprMax/cmds_geometry/box.py rename to gprMax/user_objects/cmds_geometry/box.py index 564a4132..0ebf05c6 100644 --- a/gprMax/cmds_geometry/box.py +++ b/gprMax/user_objects/cmds_geometry/box.py @@ -21,9 +21,9 @@ import logging import numpy as np import gprMax.config as config +from gprMax.cython.geometry_primitives import build_box +from gprMax.materials import Material -from ..cython.geometry_primitives import build_box -from ..materials import Material from .cmds_geometry import UserObjectGeometry, check_averaging, rotate_2point_object logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/build_templates.py b/gprMax/user_objects/cmds_geometry/build_templates.py similarity index 100% rename from gprMax/cmds_geometry/build_templates.py rename to gprMax/user_objects/cmds_geometry/build_templates.py diff --git a/gprMax/cmds_geometry/cmds_geometry.py b/gprMax/user_objects/cmds_geometry/cmds_geometry.py similarity index 100% rename from gprMax/cmds_geometry/cmds_geometry.py rename to gprMax/user_objects/cmds_geometry/cmds_geometry.py diff --git a/gprMax/cmds_geometry/cone.py b/gprMax/user_objects/cmds_geometry/cone.py similarity index 98% rename from gprMax/cmds_geometry/cone.py rename to gprMax/user_objects/cmds_geometry/cone.py index 54f0e2c3..11218c86 100644 --- a/gprMax/cmds_geometry/cone.py +++ b/gprMax/user_objects/cmds_geometry/cone.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..cython.geometry_primitives import build_cone -from ..materials import Material +from gprMax.cython.geometry_primitives import build_cone +from gprMax.materials import Material + from .cmds_geometry import UserObjectGeometry, check_averaging logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/cylinder.py b/gprMax/user_objects/cmds_geometry/cylinder.py similarity index 95% rename from gprMax/cmds_geometry/cylinder.py rename to gprMax/user_objects/cmds_geometry/cylinder.py index 583645dd..8a668f3b 100644 --- a/gprMax/cmds_geometry/cylinder.py +++ b/gprMax/user_objects/cmds_geometry/cylinder.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..cython.geometry_primitives import build_cylinder -from ..materials import Material +from gprMax.cython.geometry_primitives import build_cylinder +from gprMax.materials import Material + from .cmds_geometry import UserObjectGeometry, check_averaging logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/cylindrical_sector.py b/gprMax/user_objects/cmds_geometry/cylindrical_sector.py similarity index 96% rename from gprMax/cmds_geometry/cylindrical_sector.py rename to gprMax/user_objects/cmds_geometry/cylindrical_sector.py index a9d1924d..d2f20d9d 100644 --- a/gprMax/cmds_geometry/cylindrical_sector.py +++ b/gprMax/user_objects/cmds_geometry/cylindrical_sector.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..cython.geometry_primitives import build_cylindrical_sector -from ..materials import Material +from gprMax.cython.geometry_primitives import build_cylindrical_sector +from gprMax.materials import Material + from .cmds_geometry import UserObjectGeometry, check_averaging logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/edge.py b/gprMax/user_objects/cmds_geometry/edge.py similarity index 94% rename from gprMax/cmds_geometry/edge.py rename to gprMax/user_objects/cmds_geometry/edge.py index 45acca04..ddbc2101 100644 --- a/gprMax/cmds_geometry/edge.py +++ b/gprMax/user_objects/cmds_geometry/edge.py @@ -20,8 +20,8 @@ import logging import numpy as np -from ..cython.geometry_primitives import (build_edge_x, build_edge_y, - build_edge_z) +from gprMax.cython.geometry_primitives import build_edge_x, build_edge_y, build_edge_z + from .cmds_geometry import UserObjectGeometry, rotate_2point_object logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/ellipsoid.py b/gprMax/user_objects/cmds_geometry/ellipsoid.py similarity index 98% rename from gprMax/cmds_geometry/ellipsoid.py rename to gprMax/user_objects/cmds_geometry/ellipsoid.py index 2919c41d..d78963c0 100644 --- a/gprMax/cmds_geometry/ellipsoid.py +++ b/gprMax/user_objects/cmds_geometry/ellipsoid.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..cython.geometry_primitives import build_ellipsoid -from ..materials import Material +from gprMax.cython.geometry_primitives import build_ellipsoid +from gprMax.materials import Material + from .cmds_geometry import UserObjectGeometry, check_averaging logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/fractal_box.py b/gprMax/user_objects/cmds_geometry/fractal_box.py similarity index 100% rename from gprMax/cmds_geometry/fractal_box.py rename to gprMax/user_objects/cmds_geometry/fractal_box.py diff --git a/gprMax/cmds_geometry/geometry_objects_read.py b/gprMax/user_objects/cmds_geometry/geometry_objects_read.py similarity index 97% rename from gprMax/cmds_geometry/geometry_objects_read.py rename to gprMax/user_objects/cmds_geometry/geometry_objects_read.py index 46b7c243..01526962 100644 --- a/gprMax/cmds_geometry/geometry_objects_read.py +++ b/gprMax/user_objects/cmds_geometry/geometry_objects_read.py @@ -22,10 +22,10 @@ from pathlib import Path import h5py import gprMax.config as config +from gprMax.cython.geometry_primitives import build_voxels_from_array +from gprMax.hash_cmds_file import get_user_objects +from gprMax.utilities.utilities import round_value -from ..cython.geometry_primitives import build_voxels_from_array -from ..hash_cmds_file import get_user_objects -from ..utilities.utilities import round_value from .cmds_geometry import UserObjectGeometry logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/plate.py b/gprMax/user_objects/cmds_geometry/plate.py similarity index 95% rename from gprMax/cmds_geometry/plate.py rename to gprMax/user_objects/cmds_geometry/plate.py index 90557b0e..0702a6f4 100644 --- a/gprMax/cmds_geometry/plate.py +++ b/gprMax/user_objects/cmds_geometry/plate.py @@ -20,8 +20,8 @@ import logging import numpy as np -from ..cython.geometry_primitives import (build_face_xy, build_face_xz, - build_face_yz) +from gprMax.cython.geometry_primitives import build_face_xy, build_face_xz, build_face_yz + from .cmds_geometry import UserObjectGeometry, rotate_2point_object logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/sphere.py b/gprMax/user_objects/cmds_geometry/sphere.py similarity index 95% rename from gprMax/cmds_geometry/sphere.py rename to gprMax/user_objects/cmds_geometry/sphere.py index d3577b34..300f1bb1 100644 --- a/gprMax/cmds_geometry/sphere.py +++ b/gprMax/user_objects/cmds_geometry/sphere.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..cython.geometry_primitives import build_sphere -from ..materials import Material +from gprMax.cython.geometry_primitives import build_sphere +from gprMax.materials import Material + from .cmds_geometry import UserObjectGeometry, check_averaging logger = logging.getLogger(__name__) diff --git a/gprMax/cmds_geometry/triangle.py b/gprMax/user_objects/cmds_geometry/triangle.py similarity index 96% rename from gprMax/cmds_geometry/triangle.py rename to gprMax/user_objects/cmds_geometry/triangle.py index 17d4abd7..8e9a646b 100644 --- a/gprMax/cmds_geometry/triangle.py +++ b/gprMax/user_objects/cmds_geometry/triangle.py @@ -20,8 +20,9 @@ import logging import numpy as np -from ..cython.geometry_primitives import build_triangle -from ..materials import Material +from gprMax.cython.geometry_primitives import build_triangle +from gprMax.materials import Material + from .cmds_geometry import UserObjectGeometry, check_averaging, rotate_point logger = logging.getLogger(__name__) From ed1b32dab91f95011373f890855a7cb7a0fb3d3d Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:17:32 +0000 Subject: [PATCH 11/20] Re-add OutputDir user object --- gprMax/user_objects/cmds_singleuse.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/gprMax/user_objects/cmds_singleuse.py b/gprMax/user_objects/cmds_singleuse.py index 4df63bff..821f49c7 100644 --- a/gprMax/user_objects/cmds_singleuse.py +++ b/gprMax/user_objects/cmds_singleuse.py @@ -556,3 +556,26 @@ class RxSteps(ModelUserObject): f"{model.rxsteps[1] * model.dy:g}m, {model.rxsteps[2] * model.dz:g}m " "for each model run." ) + + +class OutputDir(ModelUserObject): + """Set the directory where output file(s) will be stored. + + Attributes: + output_dir (str): File path to directory. + """ + + @property + def order(self): + return 10 + + @property + def hash(self): + return "#output_dir" + + def __init__(self, dir: str): + super().__init__(dir=dir) + self.output_dir = dir + + def build(self, model: Model): + config.get_model_config().set_output_file_path(self.output_dir) From 98c42a70ab72c62e75b717946364d9ca45f8e098 Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:19:19 +0000 Subject: [PATCH 12/20] Fix importing output user objects --- gprMax/__init__.py | 3 +-- gprMax/hash_cmds_multiuse.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gprMax/__init__.py b/gprMax/__init__.py index 1a1aa4c5..bb214c17 100644 --- a/gprMax/__init__.py +++ b/gprMax/__init__.py @@ -33,8 +33,6 @@ from .user_objects.cmds_multiuse import ( AddDrudeDispersion, AddLorentzDispersion, ExcitationFile, - GeometryObjectsWrite, - GeometryView, HertzianDipole, MagneticDipole, Material, @@ -49,6 +47,7 @@ from .user_objects.cmds_multiuse import ( VoltageSource, Waveform, ) +from .user_objects.cmds_output import GeometryObjectsWrite, GeometryView from .user_objects.cmds_singleuse import ( Discretisation, Domain, diff --git a/gprMax/hash_cmds_multiuse.py b/gprMax/hash_cmds_multiuse.py index 688411a4..3bb2ecee 100644 --- a/gprMax/hash_cmds_multiuse.py +++ b/gprMax/hash_cmds_multiuse.py @@ -24,8 +24,6 @@ from .user_objects.cmds_multiuse import ( AddDrudeDispersion, AddLorentzDispersion, ExcitationFile, - GeometryObjectsWrite, - GeometryView, HertzianDipole, MagneticDipole, Material, @@ -39,6 +37,7 @@ from .user_objects.cmds_multiuse import ( VoltageSource, Waveform, ) +from .user_objects.cmds_output import GeometryObjectsWrite, GeometryView logger = logging.getLogger(__name__) From 41e5228e11b6a0d6ecbde1c630ad6b385a61e6a6 Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:22:39 +0000 Subject: [PATCH 13/20] Fix importing cmds_geometry.cmds_geometry --- gprMax/scene.py | 8 ++++---- gprMax/subgrids/user_objects.py | 2 +- gprMax/user_objects/cmds_geometry/fractal_box.py | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/gprMax/scene.py b/gprMax/scene.py index 296fa5b6..5f592812 100644 --- a/gprMax/scene.py +++ b/gprMax/scene.py @@ -18,14 +18,14 @@ import logging from typing import List, Sequence -from gprMax.cmds_geometry.add_grass import AddGrass -from gprMax.cmds_geometry.add_surface_roughness import AddSurfaceRoughness -from gprMax.cmds_geometry.add_surface_water import AddSurfaceWater -from gprMax.cmds_geometry.fractal_box import FractalBox from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_built_in_materials from gprMax.model import Model from gprMax.subgrids.user_objects import SubGridBase as SubGridUserBase +from gprMax.user_objects.cmds_geometry.add_grass import AddGrass +from gprMax.user_objects.cmds_geometry.add_surface_roughness import AddSurfaceRoughness +from gprMax.user_objects.cmds_geometry.add_surface_water import AddSurfaceWater +from gprMax.user_objects.cmds_geometry.fractal_box import FractalBox from gprMax.user_objects.cmds_singleuse import Discretisation, Domain, TimeWindow from gprMax.user_objects.user_objects import ( GeometryUserObject, diff --git a/gprMax/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py index dc05836c..87218e45 100644 --- a/gprMax/subgrids/user_objects.py +++ b/gprMax/subgrids/user_objects.py @@ -22,12 +22,12 @@ from typing import List, Tuple, Union import numpy as np -from gprMax.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.subgrids.subgrid_hsg import SubGridHSG as SubGridHSGUser from gprMax.user_inputs import MainGridUserInput +from gprMax.user_objects.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.user_objects.user_objects import ( GeometryUserObject, GridUserObject, diff --git a/gprMax/user_objects/cmds_geometry/fractal_box.py b/gprMax/user_objects/cmds_geometry/fractal_box.py index af5d9bba..1c8f611c 100644 --- a/gprMax/user_objects/cmds_geometry/fractal_box.py +++ b/gprMax/user_objects/cmds_geometry/fractal_box.py @@ -21,11 +21,10 @@ import logging import numpy as np import gprMax.config as config -from gprMax.cmds_geometry.cmds_geometry import UserObjectGeometry, rotate_2point_object +from gprMax.cython.geometry_primitives import build_voxels_from_array, build_voxels_from_array_mask from gprMax.fractals import FractalVolume from gprMax.materials import ListMaterial - -from ..cython.geometry_primitives import build_voxels_from_array, build_voxels_from_array_mask +from gprMax.user_objects.cmds_geometry.cmds_geometry import UserObjectGeometry, rotate_2point_object logger = logging.getLogger(__name__) From 2cc52708c465f9e2a8cb0ae2f7beac9b258e9f36 Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:23:54 +0000 Subject: [PATCH 14/20] Fix importing cmds_geometry.cmds_geometry --- gprMax/user_objects/cmds_multiuse.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gprMax/user_objects/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py index fad7ded9..8f984e87 100644 --- a/gprMax/user_objects/cmds_multiuse.py +++ b/gprMax/user_objects/cmds_multiuse.py @@ -28,11 +28,6 @@ import numpy.typing as npt from scipy import interpolate import gprMax.config as config -from gprMax.cmds_geometry.cmds_geometry import ( - UserObjectGeometry, - rotate_2point_object, - rotate_polarisation, -) from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.grid.mpi_grid import MPIGrid from gprMax.materials import DispersiveMaterial as DispersiveMaterialUser @@ -51,6 +46,11 @@ from gprMax.sources import TransmissionLine as TransmissionLineUser from gprMax.sources import VoltageSource as VoltageSourceUser from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.user_inputs import MainGridUserInput +from gprMax.user_objects.cmds_geometry.cmds_geometry import ( + UserObjectGeometry, + rotate_2point_object, + rotate_polarisation, +) from gprMax.user_objects.rotatable import Rotatable from gprMax.user_objects.user_objects import GridUserObject from gprMax.utilities.utilities import round_value From 70151168fa48c937b7c4c68807c3b71af63b5111 Mon Sep 17 00:00:00 2001 From: nmannall Date: Wed, 11 Dec 2024 18:59:23 +0000 Subject: [PATCH 15/20] Refactor geometry cmds to use new user object class --- .../user_objects/cmds_geometry/add_grass.py | 26 ++++++------- .../cmds_geometry/add_surface_roughness.py | 26 ++++++------- .../cmds_geometry/add_surface_water.py | 26 ++++++------- gprMax/user_objects/cmds_geometry/box.py | 26 ++++++------- .../cmds_geometry/cmds_geometry.py | 38 ------------------- gprMax/user_objects/cmds_geometry/cone.py | 14 ++++--- gprMax/user_objects/cmds_geometry/cylinder.py | 14 ++++--- .../cmds_geometry/cylindrical_sector.py | 14 ++++--- gprMax/user_objects/cmds_geometry/edge.py | 26 ++++++------- .../user_objects/cmds_geometry/ellipsoid.py | 14 ++++--- .../user_objects/cmds_geometry/fractal_box.py | 30 +++++++-------- .../cmds_geometry/geometry_objects_read.py | 19 +++++----- gprMax/user_objects/cmds_geometry/plate.py | 26 ++++++------- gprMax/user_objects/cmds_geometry/sphere.py | 14 ++++--- gprMax/user_objects/cmds_geometry/triangle.py | 26 ++++++------- gprMax/user_objects/user_objects.py | 8 +++- 16 files changed, 168 insertions(+), 179 deletions(-) diff --git a/gprMax/user_objects/cmds_geometry/add_grass.py b/gprMax/user_objects/cmds_geometry/add_grass.py index 517b39e5..236e593d 100644 --- a/gprMax/user_objects/cmds_geometry/add_grass.py +++ b/gprMax/user_objects/cmds_geometry/add_grass.py @@ -21,15 +21,18 @@ import logging import numpy as np from gprMax.fractals import FractalSurface, Grass +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_grass +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value -from .cmds_geometry import UserObjectGeometry, rotate_2point_object +from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class AddGrass(UserObjectGeometry): +class AddGrass(GeometryUserObject, Rotatable): """Adds grass with roots to a FractalBox class in the model. Attributes: @@ -47,25 +50,21 @@ class AddGrass(UserObjectGeometry): grass should be applied to. """ + @property + def hash(self): + return "#add_grass" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#add_grass" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Perform rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): """Add Grass to fractal box.""" try: p1 = self.kwargs["p1"] @@ -89,7 +88,7 @@ class AddGrass(UserObjectGeometry): seed = None if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) # Get the correct fractal volume volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id] @@ -99,6 +98,7 @@ class AddGrass(UserObjectGeometry): logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}") raise + uip = self._create_uip(grid) p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 xf, yf, zf = p2 diff --git a/gprMax/user_objects/cmds_geometry/add_surface_roughness.py b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py index 735def56..248e2d43 100644 --- a/gprMax/user_objects/cmds_geometry/add_surface_roughness.py +++ b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py @@ -21,14 +21,17 @@ import logging import numpy as np from gprMax.fractals import FractalSurface +from gprMax.grid.fdtd_grid import FDTDGrid +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value -from .cmds_geometry import UserObjectGeometry, rotate_2point_object +from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class AddSurfaceRoughness(UserObjectGeometry): +class AddSurfaceRoughness(GeometryUserObject, Rotatable): """Adds surface roughness to a FractalBox class in the model. Attributes: @@ -48,25 +51,21 @@ class AddSurfaceRoughness(UserObjectGeometry): number generator used to create the fractals. """ + @property + def hash(self): + return "#add_surface_roughness" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#add_surface_roughness" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Perform rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -89,7 +88,7 @@ class AddSurfaceRoughness(UserObjectGeometry): seed = None if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) # Get the correct fractal volume volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id] @@ -99,6 +98,7 @@ class AddSurfaceRoughness(UserObjectGeometry): logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}") raise ValueError + uip = self._create_uip(grid) p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 xf, yf, zf = p2 diff --git a/gprMax/user_objects/cmds_geometry/add_surface_water.py b/gprMax/user_objects/cmds_geometry/add_surface_water.py index 79ec3df5..2a645da9 100644 --- a/gprMax/user_objects/cmds_geometry/add_surface_water.py +++ b/gprMax/user_objects/cmds_geometry/add_surface_water.py @@ -20,15 +20,18 @@ import logging import numpy as np +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_water +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value -from .cmds_geometry import UserObjectGeometry, rotate_2point_object +from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class AddSurfaceWater(UserObjectGeometry): +class AddSurfaceWater(GeometryUserObject, Rotatable): """Adds surface water to a FractalBox class in the model. Attributes: @@ -43,25 +46,21 @@ class AddSurfaceWater(UserObjectGeometry): surface water should be applied to. """ + @property + def hash(self): + return "#add_surface_water" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#add_surface_water" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Perform rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): """ "Create surface water on fractal box.""" try: p1 = self.kwargs["p1"] @@ -73,7 +72,7 @@ class AddSurfaceWater(UserObjectGeometry): raise if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) if volumes := [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id]: volume = volumes[0] @@ -81,6 +80,7 @@ class AddSurfaceWater(UserObjectGeometry): logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}") raise ValueError + uip = self._create_uip(grid) p1, p2 = uip.check_box_points(p1, p2, self.__str__()) xs, ys, zs = p1 xf, yf, zf = p2 diff --git a/gprMax/user_objects/cmds_geometry/box.py b/gprMax/user_objects/cmds_geometry/box.py index 0ebf05c6..718c59c6 100644 --- a/gprMax/user_objects/cmds_geometry/box.py +++ b/gprMax/user_objects/cmds_geometry/box.py @@ -22,14 +22,17 @@ import numpy as np import gprMax.config as config from gprMax.cython.geometry_primitives import build_box +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject -from .cmds_geometry import UserObjectGeometry, check_averaging, rotate_2point_object +from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class Box(UserObjectGeometry): +class Box(GeometryUserObject, Rotatable): """Introduces an orthogonal parallelepiped with specific properties into the model. @@ -42,25 +45,21 @@ class Box(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#box" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#box" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Perform rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -69,7 +68,7 @@ class Box(UserObjectGeometry): raise if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) # Check materials have been specified # Isotropic case @@ -91,6 +90,7 @@ class Box(UserObjectGeometry): # Otherwise go with the grid default averagebox = grid.averagevolumeobjects + uip = self._create_uip(grid) p3, p4 = uip.check_box_points(p1, p2, self.__str__()) # Find nearest point on grid without translation p5 = uip.round_to_grid_static_point(p1) diff --git a/gprMax/user_objects/cmds_geometry/cmds_geometry.py b/gprMax/user_objects/cmds_geometry/cmds_geometry.py index 41b6b778..13af63dd 100644 --- a/gprMax/user_objects/cmds_geometry/cmds_geometry.py +++ b/gprMax/user_objects/cmds_geometry/cmds_geometry.py @@ -26,44 +26,6 @@ import gprMax.config as config logger = logging.getLogger(__name__) -class UserObjectGeometry: - """Specific Geometry object.""" - - def __init__(self, **kwargs): - self.kwargs = kwargs - self.hash = "#example" - self.autotranslate = True - self.do_rotate = False - - def __str__(self): - """Readable string of parameters given to object.""" - s = "" - for _, v in self.kwargs.items(): - if isinstance(v, (tuple, list)): - v = " ".join([str(el) for el in v]) - s += f"{str(v)} " - - return f"{self.hash}: {s[:-1]}" - - def build(self, grid, uip): - """Creates object and adds it to the grid.""" - pass - - def rotate(self, axis, angle, origin=None): - """Rotates object - specialised for each object.""" - pass - - def grid_name(self, grid): - """Returns subgrid name for use with logging info. Returns an empty - string if the grid is the main grid. - """ - - if config.sim_config.general["subgrid"] and grid.name != "main_grid": - return f"[{grid.name}] " - else: - return "" - - def check_averaging(averaging): """Check and set material averaging value. diff --git a/gprMax/user_objects/cmds_geometry/cone.py b/gprMax/user_objects/cmds_geometry/cone.py index 11218c86..fffa1729 100644 --- a/gprMax/user_objects/cmds_geometry/cone.py +++ b/gprMax/user_objects/cmds_geometry/cone.py @@ -21,14 +21,14 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_cone +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material - -from .cmds_geometry import UserObjectGeometry, check_averaging +from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class Cone(UserObjectGeometry): +class Cone(GeometryUserObject): """Introduces a circular cone into the model. The difference with the cylinder is that the faces of the cone can have different radii and one of them can be zero. @@ -45,11 +45,14 @@ class Cone(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#cone" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#cone" - def build(self, grid, uip): + def build(self, grid: FDTDGrid) -> None: try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -79,6 +82,7 @@ class Cone(UserObjectGeometry): logger.exception(f"{self.__str__()} no materials have been specified") raise + uip = self._create_uip(grid) p3 = uip.round_to_grid_static_point(p1) p4 = uip.round_to_grid_static_point(p2) diff --git a/gprMax/user_objects/cmds_geometry/cylinder.py b/gprMax/user_objects/cmds_geometry/cylinder.py index 8a668f3b..7bcb6c15 100644 --- a/gprMax/user_objects/cmds_geometry/cylinder.py +++ b/gprMax/user_objects/cmds_geometry/cylinder.py @@ -21,14 +21,14 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_cylinder +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material - -from .cmds_geometry import UserObjectGeometry, check_averaging +from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class Cylinder(UserObjectGeometry): +class Cylinder(GeometryUserObject): """Introduces a circular cylinder into the model. Attributes: @@ -43,11 +43,14 @@ class Cylinder(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#cylinder" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#cylinder" - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -76,6 +79,7 @@ class Cylinder(UserObjectGeometry): logger.exception(f"{self.__str__()} no materials have been specified") raise + uip = self._create_uip(grid) p3 = uip.round_to_grid_static_point(p1) p4 = uip.round_to_grid_static_point(p2) diff --git a/gprMax/user_objects/cmds_geometry/cylindrical_sector.py b/gprMax/user_objects/cmds_geometry/cylindrical_sector.py index d2f20d9d..6159f8c0 100644 --- a/gprMax/user_objects/cmds_geometry/cylindrical_sector.py +++ b/gprMax/user_objects/cmds_geometry/cylindrical_sector.py @@ -21,14 +21,14 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_cylindrical_sector +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material - -from .cmds_geometry import UserObjectGeometry, check_averaging +from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class CylindricalSector(UserObjectGeometry): +class CylindricalSector(GeometryUserObject): """Introduces a cylindrical sector (shaped like a slice of pie) into the model. Attributes: @@ -52,11 +52,14 @@ class CylindricalSector(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#cylindrical_sector" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#cylindrical_sector" - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: normal = self.kwargs["normal"].lower() ctr1 = self.kwargs["ctr1"] @@ -159,6 +162,7 @@ class CylindricalSector(UserObjectGeometry): numIDy = materials[1].numID numIDz = materials[2].numID + uip = self._create_uip(grid) # yz-plane cylindrical sector if normal == "x": level, ctr1, ctr2 = uip.round_to_grid((extent1, ctr1, ctr2)) diff --git a/gprMax/user_objects/cmds_geometry/edge.py b/gprMax/user_objects/cmds_geometry/edge.py index ddbc2101..7289b242 100644 --- a/gprMax/user_objects/cmds_geometry/edge.py +++ b/gprMax/user_objects/cmds_geometry/edge.py @@ -21,13 +21,16 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_edge_x, build_edge_y, build_edge_z +from gprMax.grid.fdtd_grid import FDTDGrid +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject -from .cmds_geometry import UserObjectGeometry, rotate_2point_object +from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class Edge(UserObjectGeometry): +class Edge(GeometryUserObject, Rotatable): """Introduces a wire with specific properties into the model. Attributes: @@ -37,25 +40,21 @@ class Edge(UserObjectGeometry): to material that has already been defined. """ + @property + def hash(self): + return "#edge" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#edge" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): """Creates edge and adds it to the grid.""" try: p1 = self.kwargs["p1"] @@ -66,8 +65,9 @@ class Edge(UserObjectGeometry): raise if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) + uip = self._create_uip(grid) p3 = uip.round_to_grid_static_point(p1) p4 = uip.round_to_grid_static_point(p2) diff --git a/gprMax/user_objects/cmds_geometry/ellipsoid.py b/gprMax/user_objects/cmds_geometry/ellipsoid.py index d78963c0..464c351f 100644 --- a/gprMax/user_objects/cmds_geometry/ellipsoid.py +++ b/gprMax/user_objects/cmds_geometry/ellipsoid.py @@ -21,14 +21,14 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_ellipsoid +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material - -from .cmds_geometry import UserObjectGeometry, check_averaging +from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class Ellipsoid(UserObjectGeometry): +class Ellipsoid(GeometryUserObject): """Introduces an ellipsoidal object with specific parameters into the model. Attributes: @@ -42,11 +42,14 @@ class Ellipsoid(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#ellipsoid" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#ellipsoid" - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] xr = self.kwargs["xr"] @@ -78,6 +81,7 @@ class Ellipsoid(UserObjectGeometry): raise # Centre of ellipsoid + uip = self._create_uip(grid) p2 = uip.round_to_grid_static_point(p1) xc, yc, zc = uip.discretise_point(p1) diff --git a/gprMax/user_objects/cmds_geometry/fractal_box.py b/gprMax/user_objects/cmds_geometry/fractal_box.py index 1c8f611c..4181e227 100644 --- a/gprMax/user_objects/cmds_geometry/fractal_box.py +++ b/gprMax/user_objects/cmds_geometry/fractal_box.py @@ -23,13 +23,16 @@ import numpy as np import gprMax.config as config from gprMax.cython.geometry_primitives import build_voxels_from_array, build_voxels_from_array_mask from gprMax.fractals import FractalVolume +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import ListMaterial -from gprMax.user_objects.cmds_geometry.cmds_geometry import UserObjectGeometry, rotate_2point_object +from gprMax.user_objects.cmds_geometry.cmds_geometry import rotate_2point_object +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class FractalBox(UserObjectGeometry): +class FractalBox(GeometryUserObject, Rotatable): """Introduces an orthogonal parallelepiped with fractal distributed properties which are related to a mixing model or normal material into the model. @@ -53,26 +56,22 @@ class FractalBox(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#fractal_box" + def __init__(self, **kwargs): super().__init__(**kwargs) self.do_pre_build = True - self.hash = "#fractal_box" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def pre_build(self, grid, uip): + def pre_build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -96,7 +95,7 @@ class FractalBox(UserObjectGeometry): seed = None if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) # Check averaging try: @@ -107,6 +106,7 @@ class FractalBox(UserObjectGeometry): # a fractal box. averagefractalbox = False + uip = self._create_uip(grid) p3 = uip.round_to_grid_static_point(p1) p4 = uip.round_to_grid_static_point(p2) @@ -187,9 +187,9 @@ class FractalBox(UserObjectGeometry): ) grid.fractalvolumes.append(self.volume) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): if self.do_pre_build: - self.pre_build(grid, uip) + self.pre_build(grid) self.do_pre_build = False else: if self.volume.fractalsurfaces: diff --git a/gprMax/user_objects/cmds_geometry/geometry_objects_read.py b/gprMax/user_objects/cmds_geometry/geometry_objects_read.py index 01526962..789a569e 100644 --- a/gprMax/user_objects/cmds_geometry/geometry_objects_read.py +++ b/gprMax/user_objects/cmds_geometry/geometry_objects_read.py @@ -23,23 +23,23 @@ import h5py import gprMax.config as config from gprMax.cython.geometry_primitives import build_voxels_from_array +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.hash_cmds_file import get_user_objects +from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value -from .cmds_geometry import UserObjectGeometry - logger = logging.getLogger(__name__) -class GeometryObjectsRead(UserObjectGeometry): +class GeometryObjectsRead(GeometryUserObject): + @property + def hash(self): + return "#geometry_objects_read" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#geometry_objects_read" - def rotate(self, axis, angle, origin=None): - pass - - def build(self, grid, uip): + def build(self, grid: FDTDGrid): """Creates the object and adds it to the grid.""" try: p1 = self.kwargs["p1"] @@ -52,6 +52,7 @@ class GeometryObjectsRead(UserObjectGeometry): # Discretise the point using uip object. This has different behaviour # depending on the type of uip object. So we can use it for # the main grid or the subgrid. + uip = self._create_uip(grid) xs, ys, zs = uip.discretise_point(p1) # See if material file exists at specified path and if not try input @@ -82,7 +83,7 @@ class GeometryObjectsRead(UserObjectGeometry): scene.add(material_obj) # Creates the internal simulation objects - scene.process_cmds(material_objs, grid) + scene.build_grid_objects(material_objs, grid) # Update material type for material in grid.materials: diff --git a/gprMax/user_objects/cmds_geometry/plate.py b/gprMax/user_objects/cmds_geometry/plate.py index 0702a6f4..0b245450 100644 --- a/gprMax/user_objects/cmds_geometry/plate.py +++ b/gprMax/user_objects/cmds_geometry/plate.py @@ -21,13 +21,16 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_face_xy, build_face_xz, build_face_yz +from gprMax.grid.fdtd_grid import FDTDGrid +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject -from .cmds_geometry import UserObjectGeometry, rotate_2point_object +from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class Plate(UserObjectGeometry): +class Plate(GeometryUserObject, Rotatable): """Introduces a plate with specific properties into the model. Attributes: @@ -38,25 +41,21 @@ class Plate(UserObjectGeometry): material_ids: list of material identifiers in the x, y, z directions. """ + @property + def hash(self): + return "#plate" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#plate" - def rotate(self, axis, angle, origin=None): - """Set parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" pts = np.array([self.kwargs["p1"], self.kwargs["p2"]]) rot_pts = rotate_2point_object(pts, self.axis, self.angle, self.origin) self.kwargs["p1"] = tuple(rot_pts[0, :]) self.kwargs["p2"] = tuple(rot_pts[1, :]) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] @@ -76,8 +75,9 @@ class Plate(UserObjectGeometry): raise if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) + uip = self._create_uip(grid) p3 = uip.round_to_grid_static_point(p1) p4 = uip.round_to_grid_static_point(p2) diff --git a/gprMax/user_objects/cmds_geometry/sphere.py b/gprMax/user_objects/cmds_geometry/sphere.py index 300f1bb1..ef2db9cd 100644 --- a/gprMax/user_objects/cmds_geometry/sphere.py +++ b/gprMax/user_objects/cmds_geometry/sphere.py @@ -21,14 +21,14 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_sphere +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material - -from .cmds_geometry import UserObjectGeometry, check_averaging +from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class Sphere(UserObjectGeometry): +class Sphere(GeometryUserObject): """Introduces a spherical object with specific parameters into the model. Attributes: @@ -40,11 +40,14 @@ class Sphere(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#sphere" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#sphere" - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: p1 = self.kwargs["p1"] r = self.kwargs["r"] @@ -73,6 +76,7 @@ class Sphere(UserObjectGeometry): raise # Centre of sphere + uip = self._create_uip(grid) p2 = uip.round_to_grid_static_point(p1) xc, yc, zc = uip.discretise_point(p1) diff --git a/gprMax/user_objects/cmds_geometry/triangle.py b/gprMax/user_objects/cmds_geometry/triangle.py index 8e9a646b..42cab100 100644 --- a/gprMax/user_objects/cmds_geometry/triangle.py +++ b/gprMax/user_objects/cmds_geometry/triangle.py @@ -21,14 +21,17 @@ import logging import numpy as np from gprMax.cython.geometry_primitives import build_triangle +from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material +from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.user_objects import GeometryUserObject -from .cmds_geometry import UserObjectGeometry, check_averaging, rotate_point +from .cmds_geometry import rotate_point logger = logging.getLogger(__name__) -class Triangle(UserObjectGeometry): +class Triangle(GeometryUserObject, Rotatable): """Introduces a triangular patch or a triangular prism with specific properties into the model. @@ -44,18 +47,14 @@ class Triangle(UserObjectGeometry): averaging: string (y or n) used to switch on and off dielectric smoothing. """ + @property + def hash(self): + return "#triangle" + def __init__(self, **kwargs): super().__init__(**kwargs) - self.hash = "#triangle" - def rotate(self, axis, angle, origin=None): - """Sets parameters for rotation.""" - self.axis = axis - self.angle = angle - self.origin = origin - self.do_rotate = True - - def _do_rotate(self): + def _do_rotate(self, grid: FDTDGrid): """Performs rotation.""" p1 = rotate_point(self.kwargs["p1"], self.axis, self.angle, self.origin) p2 = rotate_point(self.kwargs["p2"], self.axis, self.angle, self.origin) @@ -64,7 +63,7 @@ class Triangle(UserObjectGeometry): self.kwargs["p2"] = tuple(p2) self.kwargs["p3"] = tuple(p3) - def build(self, grid, uip): + def build(self, grid: FDTDGrid): try: up1 = self.kwargs["p1"] up2 = self.kwargs["p2"] @@ -75,7 +74,7 @@ class Triangle(UserObjectGeometry): raise if self.do_rotate: - self._do_rotate() + self._do_rotate(grid) # Check averaging try: @@ -97,6 +96,7 @@ class Triangle(UserObjectGeometry): logger.exception(f"{self.__str__()} no materials have been specified") raise + uip = self._create_uip(grid) p4 = uip.round_to_grid_static_point(up1) p5 = uip.round_to_grid_static_point(up2) p6 = uip.round_to_grid_static_point(up3) diff --git a/gprMax/user_objects/user_objects.py b/gprMax/user_objects/user_objects.py index 50ff33c5..b4c4c905 100644 --- a/gprMax/user_objects/user_objects.py +++ b/gprMax/user_objects/user_objects.py @@ -145,4 +145,10 @@ class OutputUserObject(UserObject): class GeometryUserObject(GridUserObject): """User defined object that adds geometry to a grid.""" - pass + @property + def order(self): + """Geometry Objects do not have an ordering. + + They should be built in the order they were added to the scene. + """ + return 1 From 5b805d808ff6d92421b78fbeb4ea1cba45fb0893 Mon Sep 17 00:00:00 2001 From: nmannall Date: Thu, 12 Dec 2024 10:19:07 +0000 Subject: [PATCH 16/20] Remove old Subgrid user object and unnecessary imports --- gprMax/__init__.py | 1 - gprMax/subgrids/user_objects.py | 1 - gprMax/user_objects/cmds_multiuse.py | 6 +++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/gprMax/__init__.py b/gprMax/__init__.py index bb214c17..ac917412 100644 --- a/gprMax/__init__.py +++ b/gprMax/__init__.py @@ -42,7 +42,6 @@ from .user_objects.cmds_multiuse import ( RxArray, Snapshot, SoilPeplinski, - Subgrid, TransmissionLine, VoltageSource, Waveform, diff --git a/gprMax/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py index 87218e45..8c155f03 100644 --- a/gprMax/subgrids/user_objects.py +++ b/gprMax/subgrids/user_objects.py @@ -27,7 +27,6 @@ from gprMax.model import Model from gprMax.subgrids.grid import SubGridBaseGrid from gprMax.subgrids.subgrid_hsg import SubGridHSG as SubGridHSGUser from gprMax.user_inputs import MainGridUserInput -from gprMax.user_objects.cmds_geometry.cmds_geometry import UserObjectGeometry from gprMax.user_objects.user_objects import ( GeometryUserObject, GridUserObject, diff --git a/gprMax/user_objects/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py index 8f984e87..74d67ce7 100644 --- a/gprMax/user_objects/cmds_multiuse.py +++ b/gprMax/user_objects/cmds_multiuse.py @@ -45,9 +45,7 @@ from gprMax.sources import MagneticDipole as MagneticDipoleUser from gprMax.sources import TransmissionLine as TransmissionLineUser from gprMax.sources import VoltageSource as VoltageSourceUser from gprMax.subgrids.grid import SubGridBaseGrid -from gprMax.user_inputs import MainGridUserInput from gprMax.user_objects.cmds_geometry.cmds_geometry import ( - UserObjectGeometry, rotate_2point_object, rotate_polarisation, ) @@ -2009,8 +2007,9 @@ class PMLCFS(GridUserObject): raise ValueError +""" +TODO: Can this be removed? class Subgrid(UserObjectMulti): - """""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -2030,3 +2029,4 @@ class Subgrid(UserObjectMulti): else: logger.exception("This object is unknown to gprMax.") raise ValueError +""" From c11b064499e8214082281f79a90a86a37517407e Mon Sep 17 00:00:00 2001 From: nmannall Date: Thu, 12 Dec 2024 10:19:44 +0000 Subject: [PATCH 17/20] Fix setting model.iterations as an integer --- gprMax/user_objects/cmds_singleuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gprMax/user_objects/cmds_singleuse.py b/gprMax/user_objects/cmds_singleuse.py index 821f49c7..d6a5ec06 100644 --- a/gprMax/user_objects/cmds_singleuse.py +++ b/gprMax/user_objects/cmds_singleuse.py @@ -239,7 +239,7 @@ class TimeWindow(ModelUserObject): if self.time is not None: if self.time > 0: model.timewindow = self.time - model.iterations = np.ceil(self.time / model.dt, dtype=np.int32) + 1 + model.iterations = int(np.ceil(self.time / model.dt)) + 1 else: raise ValueError(f"{self} must have a value greater than zero") elif self.iterations is not None: From d26ff3485c2ce90d1b898942739ef0be933a5eb3 Mon Sep 17 00:00:00 2001 From: nmannall Date: Thu, 12 Dec 2024 10:20:57 +0000 Subject: [PATCH 18/20] Make Rotatable a proper Mixin class --- gprMax/user_objects/cmds_geometry/add_grass.py | 4 ++-- .../cmds_geometry/add_surface_roughness.py | 4 ++-- .../user_objects/cmds_geometry/add_surface_water.py | 4 ++-- gprMax/user_objects/cmds_geometry/box.py | 4 ++-- gprMax/user_objects/cmds_geometry/edge.py | 4 ++-- gprMax/user_objects/cmds_geometry/fractal_box.py | 4 ++-- gprMax/user_objects/cmds_geometry/plate.py | 4 ++-- gprMax/user_objects/cmds_geometry/triangle.py | 4 ++-- gprMax/user_objects/cmds_multiuse.py | 12 ++++++------ gprMax/user_objects/rotatable.py | 5 +++-- 10 files changed, 25 insertions(+), 24 deletions(-) diff --git a/gprMax/user_objects/cmds_geometry/add_grass.py b/gprMax/user_objects/cmds_geometry/add_grass.py index 236e593d..f6d9e098 100644 --- a/gprMax/user_objects/cmds_geometry/add_grass.py +++ b/gprMax/user_objects/cmds_geometry/add_grass.py @@ -23,7 +23,7 @@ import numpy as np from gprMax.fractals import FractalSurface, Grass from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_grass -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value @@ -32,7 +32,7 @@ from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class AddGrass(GeometryUserObject, Rotatable): +class AddGrass(RotatableMixin, GeometryUserObject): """Adds grass with roots to a FractalBox class in the model. Attributes: diff --git a/gprMax/user_objects/cmds_geometry/add_surface_roughness.py b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py index 248e2d43..2e49ab4f 100644 --- a/gprMax/user_objects/cmds_geometry/add_surface_roughness.py +++ b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py @@ -22,7 +22,7 @@ import numpy as np from gprMax.fractals import FractalSurface from gprMax.grid.fdtd_grid import FDTDGrid -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value @@ -31,7 +31,7 @@ from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class AddSurfaceRoughness(GeometryUserObject, Rotatable): +class AddSurfaceRoughness(RotatableMixin, GeometryUserObject): """Adds surface roughness to a FractalBox class in the model. Attributes: diff --git a/gprMax/user_objects/cmds_geometry/add_surface_water.py b/gprMax/user_objects/cmds_geometry/add_surface_water.py index 2a645da9..3f335d7d 100644 --- a/gprMax/user_objects/cmds_geometry/add_surface_water.py +++ b/gprMax/user_objects/cmds_geometry/add_surface_water.py @@ -22,7 +22,7 @@ import numpy as np from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import create_water -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from gprMax.utilities.utilities import round_value @@ -31,7 +31,7 @@ from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class AddSurfaceWater(GeometryUserObject, Rotatable): +class AddSurfaceWater(RotatableMixin, GeometryUserObject): """Adds surface water to a FractalBox class in the model. Attributes: diff --git a/gprMax/user_objects/cmds_geometry/box.py b/gprMax/user_objects/cmds_geometry/box.py index 718c59c6..6b79e759 100644 --- a/gprMax/user_objects/cmds_geometry/box.py +++ b/gprMax/user_objects/cmds_geometry/box.py @@ -24,7 +24,7 @@ import gprMax.config as config from gprMax.cython.geometry_primitives import build_box from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from .cmds_geometry import rotate_2point_object @@ -32,7 +32,7 @@ from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class Box(GeometryUserObject, Rotatable): +class Box(RotatableMixin, GeometryUserObject): """Introduces an orthogonal parallelepiped with specific properties into the model. diff --git a/gprMax/user_objects/cmds_geometry/edge.py b/gprMax/user_objects/cmds_geometry/edge.py index 7289b242..f1fb5398 100644 --- a/gprMax/user_objects/cmds_geometry/edge.py +++ b/gprMax/user_objects/cmds_geometry/edge.py @@ -22,7 +22,7 @@ import numpy as np from gprMax.cython.geometry_primitives import build_edge_x, build_edge_y, build_edge_z from gprMax.grid.fdtd_grid import FDTDGrid -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from .cmds_geometry import rotate_2point_object @@ -30,7 +30,7 @@ from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class Edge(GeometryUserObject, Rotatable): +class Edge(RotatableMixin, GeometryUserObject): """Introduces a wire with specific properties into the model. Attributes: diff --git a/gprMax/user_objects/cmds_geometry/fractal_box.py b/gprMax/user_objects/cmds_geometry/fractal_box.py index 4181e227..b6837bd1 100644 --- a/gprMax/user_objects/cmds_geometry/fractal_box.py +++ b/gprMax/user_objects/cmds_geometry/fractal_box.py @@ -26,13 +26,13 @@ from gprMax.fractals import FractalVolume from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import ListMaterial from gprMax.user_objects.cmds_geometry.cmds_geometry import rotate_2point_object -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject logger = logging.getLogger(__name__) -class FractalBox(GeometryUserObject, Rotatable): +class FractalBox(RotatableMixin, GeometryUserObject): """Introduces an orthogonal parallelepiped with fractal distributed properties which are related to a mixing model or normal material into the model. diff --git a/gprMax/user_objects/cmds_geometry/plate.py b/gprMax/user_objects/cmds_geometry/plate.py index 0b245450..5de480e9 100644 --- a/gprMax/user_objects/cmds_geometry/plate.py +++ b/gprMax/user_objects/cmds_geometry/plate.py @@ -22,7 +22,7 @@ import numpy as np from gprMax.cython.geometry_primitives import build_face_xy, build_face_xz, build_face_yz from gprMax.grid.fdtd_grid import FDTDGrid -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from .cmds_geometry import rotate_2point_object @@ -30,7 +30,7 @@ from .cmds_geometry import rotate_2point_object logger = logging.getLogger(__name__) -class Plate(GeometryUserObject, Rotatable): +class Plate(RotatableMixin, GeometryUserObject): """Introduces a plate with specific properties into the model. Attributes: diff --git a/gprMax/user_objects/cmds_geometry/triangle.py b/gprMax/user_objects/cmds_geometry/triangle.py index 42cab100..4a752d74 100644 --- a/gprMax/user_objects/cmds_geometry/triangle.py +++ b/gprMax/user_objects/cmds_geometry/triangle.py @@ -23,7 +23,7 @@ import numpy as np from gprMax.cython.geometry_primitives import build_triangle from gprMax.grid.fdtd_grid import FDTDGrid from gprMax.materials import Material -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GeometryUserObject from .cmds_geometry import rotate_point @@ -31,7 +31,7 @@ from .cmds_geometry import rotate_point logger = logging.getLogger(__name__) -class Triangle(GeometryUserObject, Rotatable): +class Triangle(RotatableMixin, GeometryUserObject): """Introduces a triangular patch or a triangular prism with specific properties into the model. diff --git a/gprMax/user_objects/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py index 74d67ce7..fc34a347 100644 --- a/gprMax/user_objects/cmds_multiuse.py +++ b/gprMax/user_objects/cmds_multiuse.py @@ -49,7 +49,7 @@ from gprMax.user_objects.cmds_geometry.cmds_geometry import ( rotate_2point_object, rotate_polarisation, ) -from gprMax.user_objects.rotatable import Rotatable +from gprMax.user_objects.rotatable import RotatableMixin from gprMax.user_objects.user_objects import GridUserObject from gprMax.utilities.utilities import round_value from gprMax.waveforms import Waveform as WaveformUser @@ -291,7 +291,7 @@ class Waveform(GridUserObject): grid.waveforms.append(w) -class VoltageSource(GridUserObject, Rotatable): +class VoltageSource(RotatableMixin, GridUserObject): """Specifies a voltage source at an electric field location. Attributes: @@ -435,7 +435,7 @@ class VoltageSource(GridUserObject, Rotatable): grid.voltagesources.append(v) -class HertzianDipole(GridUserObject, Rotatable): +class HertzianDipole(RotatableMixin, GridUserObject): """Specifies a current density term at an electric field location. The simplest excitation, often referred to as an additive or soft source. @@ -582,7 +582,7 @@ class HertzianDipole(GridUserObject, Rotatable): grid.hertziandipoles.append(h) -class MagneticDipole(GridUserObject, Rotatable): +class MagneticDipole(RotatableMixin, GridUserObject): """Simulates an infinitesimal magnetic dipole. Often referred to as an additive or soft source. @@ -720,7 +720,7 @@ class MagneticDipole(GridUserObject, Rotatable): grid.magneticdipoles.append(m) -class TransmissionLine(GridUserObject, Rotatable): +class TransmissionLine(RotatableMixin, GridUserObject): """Specifies a one-dimensional transmission line model at an electric field location. @@ -879,7 +879,7 @@ class TransmissionLine(GridUserObject, Rotatable): grid.transmissionlines.append(t) -class Rx(GridUserObject, Rotatable): +class Rx(RotatableMixin, GridUserObject): """Specifies output points in the model. These are locations where the values of the electric and magnetic field diff --git a/gprMax/user_objects/rotatable.py b/gprMax/user_objects/rotatable.py index 9f6fa2c7..19f2492d 100644 --- a/gprMax/user_objects/rotatable.py +++ b/gprMax/user_objects/rotatable.py @@ -4,7 +4,7 @@ from typing import Optional, Tuple from gprMax.grid.fdtd_grid import FDTDGrid -class Rotatable(ABC): +class RotatableMixin(ABC): """Stores parameters and defines an interface for rotatable objects. Attributes: @@ -18,7 +18,8 @@ class Rotatable(ABC): otherwise. Default False. """ - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # Forward all unused arguments self.axis = "x" self.angle = 0 self.origin = None From 8e60484b44cb1b0a679d279cd81209f5188c82ad Mon Sep 17 00:00:00 2001 From: nmannall Date: Mon, 6 Jan 2025 16:01:36 +0000 Subject: [PATCH 19/20] Make PML formulation optional in PMLProps user object --- gprMax/user_objects/cmds_singleuse.py | 29 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/gprMax/user_objects/cmds_singleuse.py b/gprMax/user_objects/cmds_singleuse.py index d6a5ec06..8ac2a6ba 100644 --- a/gprMax/user_objects/cmds_singleuse.py +++ b/gprMax/user_objects/cmds_singleuse.py @@ -391,10 +391,12 @@ class PMLThickness(ModelUserObject): ): raise ValueError(f"{self} has too many cells for the domain size") + thickness = model.G.pmls["thickness"] + logger.info( - f"PML thickness: x0={model.G.pmls['x0']}, y0={model.G.pmls['y0']}," - f" z0={model.G.pmls['z0']}, xmax={model.G.pmls['xmax']}," - f" ymax={model.G.pmls['yxmax']}, zmax={model.G.pmls['zmax']}" + f"PML thickness: x0={thickness['x0']}, y0={thickness['y0']}," + f" z0={thickness['z0']}, xmax={thickness['xmax']}," + f" ymax={thickness['ymax']}, zmax={thickness['zmax']}" ) @@ -427,7 +429,7 @@ class PMLProps(ModelUserObject): def __init__( self, - formulation: str, + formulation: Optional[str] = None, thickness: Optional[int] = None, x0: Optional[int] = None, y0: Optional[int] = None, @@ -463,7 +465,10 @@ class PMLProps(ModelUserObject): " PMLThickness user objects instead." ) - self.pml_formulation = PMLFormulation(formulation) + if formulation is not None: + self.pml_formulation = PMLFormulation(formulation) + else: + self.pml_formulation = None if thickness is not None: self.pml_thickness = PMLThickness(thickness) @@ -477,11 +482,19 @@ class PMLProps(ModelUserObject): ): self.pml_thickness = PMLThickness((x0, y0, z0, xmax, ymax, zmax)) else: - raise ValueError("Either set thickness, or all of x0, y0, z0, xmax, ymax, zmax.") + self.pml_thickness = None + + if self.pml_formulation is None and self.pml_thickness is None: + raise ValueError( + "Must set PML formulation or thickness. Thickness can be set by specifying all of x0, y0, z0, xmax, ymax, zmax." + ) def build(self, model): - self.pml_formulation.build(model) - self.pml_thickness.build(model) + if self.pml_formulation is not None: + self.pml_formulation.build(model) + + if self.pml_thickness is not None: + self.pml_thickness.build(model) class SrcSteps(ModelUserObject): From e6debbf2f4c87e6a16154d6c7aa120851190b005 Mon Sep 17 00:00:00 2001 From: nmannall Date: Mon, 6 Jan 2025 16:05:27 +0000 Subject: [PATCH 20/20] Update SubGridHSG to new UserObject approach --- gprMax/subgrids/user_objects.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/gprMax/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py index 8c155f03..992f3b7e 100644 --- a/gprMax/subgrids/user_objects.py +++ b/gprMax/subgrids/user_objects.py @@ -101,11 +101,12 @@ class SubGridBase(ModelUserObject): """Sets number of iterations that will take place in the subgrid.""" sg.iterations = model.iterations * sg.ratio - def setup(self, sg: SubGridBaseGrid, model: Model, uip: MainGridUserInput): + def setup(self, sg: SubGridBaseGrid, model: Model): """ "Common setup to both all subgrid types.""" p1 = self.kwargs["p1"] p2 = self.kwargs["p2"] + uip = self._create_uip(model.G) p1, p2 = uip.check_box_points(p1, p2, self.__str__()) self.set_discretisation(sg, model.G) @@ -179,6 +180,14 @@ class SubGridHSG(SubGridBase): stability. Defaults to True. """ + @property + def order(self): + return 18 + + @property + def hash(self): + return "#subgrid_hsg" + def __init__( self, p1=None, @@ -206,10 +215,8 @@ class SubGridHSG(SubGridBase): kwargs["filter"] = filter super().__init__(**kwargs) - self.order = 18 - self.hash = "#subgrid_hsg" - def build(self, model: Model, uip: MainGridUserInput) -> SubGridHSGUser: + def build(self, model: Model) -> SubGridHSGUser: sg = SubGridHSGUser(**self.kwargs) - self.setup(sg, model, uip) + self.setup(sg, model) return sg