diff --git a/gprMax/__init__.py b/gprMax/__init__.py
index b97fa2dc..ac917412 100644
--- a/gprMax/__init__.py
+++ b/gprMax/__init__.py
@@ -10,28 +10,29 @@ 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 .cmds_multiuse import (
+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,
AddDrudeDispersion,
AddLorentzDispersion,
ExcitationFile,
- GeometryObjectsWrite,
- GeometryView,
HertzianDipole,
MagneticDipole,
Material,
@@ -41,12 +42,12 @@ from .cmds_multiuse import (
RxArray,
Snapshot,
SoilPeplinski,
- Subgrid,
TransmissionLine,
VoltageSource,
Waveform,
)
-from .cmds_singleuse import (
+from .user_objects.cmds_output import GeometryObjectsWrite, GeometryView
+from .user_objects.cmds_singleuse import (
Discretisation,
Domain,
OMPThreads,
@@ -58,8 +59,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/cmds_singleuse.py b/gprMax/cmds_singleuse.py
deleted file mode 100644
index e39bdf29..00000000
--- a/gprMax/cmds_singleuse.py
+++ /dev/null
@@ -1,409 +0,0 @@
-# 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"])
diff --git a/gprMax/grid/fdtd_grid.py b/gprMax/grid/fdtd_grid.py
index 60004e7f..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]
@@ -362,7 +365,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 +390,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 +417,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/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/hash_cmds_multiuse.py b/gprMax/hash_cmds_multiuse.py
index c933b7c4..3bb2ecee 100644
--- a/gprMax/hash_cmds_multiuse.py
+++ b/gprMax/hash_cmds_multiuse.py
@@ -18,14 +18,12 @@
import logging
-from .cmds_multiuse import (
+from .user_objects.cmds_multiuse import (
PMLCFS,
AddDebyeDispersion,
AddDrudeDispersion,
AddLorentzDispersion,
ExcitationFile,
- GeometryObjectsWrite,
- GeometryView,
HertzianDipole,
MagneticDipole,
Material,
@@ -39,6 +37,7 @@ from .cmds_multiuse import (
VoltageSource,
Waveform,
)
+from .user_objects.cmds_output import GeometryObjectsWrite, GeometryView
logger = logging.getLogger(__name__)
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/model.py b/gprMax/model.py
index 82add10c..8d7012f5 100644
--- a/gprMax/model.py
+++ b/gprMax/model.py
@@ -56,11 +56,9 @@ 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: 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] = []
@@ -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.
diff --git a/gprMax/scene.py b/gprMax/scene.py
index 5ca94d95..5f592812 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.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_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,
+ 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/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/subgrids/user_objects.py b/gprMax/subgrids/user_objects.py
index aa7c8ee0..992f3b7e 100644
--- a/gprMax/subgrids/user_objects.py
+++ b/gprMax/subgrids/user_objects.py
@@ -25,33 +25,42 @@ import numpy as np
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.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
@@ -92,11 +101,12 @@ class SubGridBase(UserObjectMulti):
"""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)
@@ -170,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,
@@ -197,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
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 92%
rename from gprMax/cmds_geometry/add_grass.py
rename to gprMax/user_objects/cmds_geometry/add_grass.py
index fa263173..f6d9e098 100644
--- a/gprMax/cmds_geometry/add_grass.py
+++ b/gprMax/user_objects/cmds_geometry/add_grass.py
@@ -20,15 +20,19 @@ import logging
import numpy as np
-from ..fractals import FractalSurface, Grass
-from ..materials import create_grass
-from ..utilities.utilities import round_value
-from .cmds_geometry import UserObjectGeometry, rotate_2point_object
+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 RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
+from gprMax.utilities.utilities import round_value
+
+from .cmds_geometry import rotate_2point_object
logger = logging.getLogger(__name__)
-class AddGrass(UserObjectGeometry):
+class AddGrass(RotatableMixin, GeometryUserObject):
"""Adds grass with roots to a FractalBox class in the model.
Attributes:
@@ -46,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"]
@@ -88,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]
@@ -98,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/cmds_geometry/add_surface_roughness.py b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py
similarity index 92%
rename from gprMax/cmds_geometry/add_surface_roughness.py
rename to gprMax/user_objects/cmds_geometry/add_surface_roughness.py
index a127a1ae..2e49ab4f 100644
--- a/gprMax/cmds_geometry/add_surface_roughness.py
+++ b/gprMax/user_objects/cmds_geometry/add_surface_roughness.py
@@ -20,14 +20,18 @@ import logging
import numpy as np
-from ..fractals import FractalSurface
-from ..utilities.utilities import round_value
-from .cmds_geometry import UserObjectGeometry, rotate_2point_object
+from gprMax.fractals import FractalSurface
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.user_objects.rotatable import RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
+from gprMax.utilities.utilities import round_value
+
+from .cmds_geometry import rotate_2point_object
logger = logging.getLogger(__name__)
-class AddSurfaceRoughness(UserObjectGeometry):
+class AddSurfaceRoughness(RotatableMixin, GeometryUserObject):
"""Adds surface roughness to a FractalBox class in the model.
Attributes:
@@ -47,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"]
@@ -88,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]
@@ -98,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/cmds_geometry/add_surface_water.py b/gprMax/user_objects/cmds_geometry/add_surface_water.py
similarity index 89%
rename from gprMax/cmds_geometry/add_surface_water.py
rename to gprMax/user_objects/cmds_geometry/add_surface_water.py
index 79a6af72..3f335d7d 100644
--- a/gprMax/cmds_geometry/add_surface_water.py
+++ b/gprMax/user_objects/cmds_geometry/add_surface_water.py
@@ -20,14 +20,18 @@ import logging
import numpy as np
-from ..materials import create_water
-from ..utilities.utilities import round_value
-from .cmds_geometry import UserObjectGeometry, rotate_2point_object
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.materials import create_water
+from gprMax.user_objects.rotatable import RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
+from gprMax.utilities.utilities import round_value
+
+from .cmds_geometry import rotate_2point_object
logger = logging.getLogger(__name__)
-class AddSurfaceWater(UserObjectGeometry):
+class AddSurfaceWater(RotatableMixin, GeometryUserObject):
"""Adds surface water to a FractalBox class in the model.
Attributes:
@@ -42,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"]
@@ -72,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]
@@ -80,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/cmds_geometry/box.py b/gprMax/user_objects/cmds_geometry/box.py
similarity index 88%
rename from gprMax/cmds_geometry/box.py
rename to gprMax/user_objects/cmds_geometry/box.py
index 564a4132..6b79e759 100644
--- a/gprMax/cmds_geometry/box.py
+++ b/gprMax/user_objects/cmds_geometry/box.py
@@ -21,15 +21,18 @@ import logging
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 RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
-from ..cython.geometry_primitives import build_box
-from ..materials import Material
-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(RotatableMixin, GeometryUserObject):
"""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/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 84%
rename from gprMax/cmds_geometry/cmds_geometry.py
rename to gprMax/user_objects/cmds_geometry/cmds_geometry.py
index 41b6b778..13af63dd 100644
--- a/gprMax/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/cmds_geometry/cone.py b/gprMax/user_objects/cmds_geometry/cone.py
similarity index 94%
rename from gprMax/cmds_geometry/cone.py
rename to gprMax/user_objects/cmds_geometry/cone.py
index 54f0e2c3..fffa1729 100644
--- a/gprMax/cmds_geometry/cone.py
+++ b/gprMax/user_objects/cmds_geometry/cone.py
@@ -20,14 +20,15 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import build_cone
-from ..materials import Material
-from .cmds_geometry import UserObjectGeometry, check_averaging
+from gprMax.cython.geometry_primitives import build_cone
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.materials import Material
+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.
@@ -44,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"]
@@ -78,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/cmds_geometry/cylinder.py b/gprMax/user_objects/cmds_geometry/cylinder.py
similarity index 91%
rename from gprMax/cmds_geometry/cylinder.py
rename to gprMax/user_objects/cmds_geometry/cylinder.py
index 583645dd..7bcb6c15 100644
--- a/gprMax/cmds_geometry/cylinder.py
+++ b/gprMax/user_objects/cmds_geometry/cylinder.py
@@ -20,14 +20,15 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import build_cylinder
-from ..materials import Material
-from .cmds_geometry import UserObjectGeometry, check_averaging
+from gprMax.cython.geometry_primitives import build_cylinder
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.materials import Material
+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:
@@ -42,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"]
@@ -75,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/cmds_geometry/cylindrical_sector.py b/gprMax/user_objects/cmds_geometry/cylindrical_sector.py
similarity index 93%
rename from gprMax/cmds_geometry/cylindrical_sector.py
rename to gprMax/user_objects/cmds_geometry/cylindrical_sector.py
index a9d1924d..6159f8c0 100644
--- a/gprMax/cmds_geometry/cylindrical_sector.py
+++ b/gprMax/user_objects/cmds_geometry/cylindrical_sector.py
@@ -20,14 +20,15 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import build_cylindrical_sector
-from ..materials import Material
-from .cmds_geometry import UserObjectGeometry, check_averaging
+from gprMax.cython.geometry_primitives import build_cylindrical_sector
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.materials import Material
+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:
@@ -51,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"]
@@ -158,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/cmds_geometry/edge.py b/gprMax/user_objects/cmds_geometry/edge.py
similarity index 83%
rename from gprMax/cmds_geometry/edge.py
rename to gprMax/user_objects/cmds_geometry/edge.py
index 45acca04..f1fb5398 100644
--- a/gprMax/cmds_geometry/edge.py
+++ b/gprMax/user_objects/cmds_geometry/edge.py
@@ -20,14 +20,17 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import (build_edge_x, build_edge_y,
- build_edge_z)
-from .cmds_geometry import UserObjectGeometry, rotate_2point_object
+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 RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
+
+from .cmds_geometry import rotate_2point_object
logger = logging.getLogger(__name__)
-class Edge(UserObjectGeometry):
+class Edge(RotatableMixin, GeometryUserObject):
"""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/cmds_geometry/ellipsoid.py b/gprMax/user_objects/cmds_geometry/ellipsoid.py
similarity index 93%
rename from gprMax/cmds_geometry/ellipsoid.py
rename to gprMax/user_objects/cmds_geometry/ellipsoid.py
index 2919c41d..464c351f 100644
--- a/gprMax/cmds_geometry/ellipsoid.py
+++ b/gprMax/user_objects/cmds_geometry/ellipsoid.py
@@ -20,14 +20,15 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import build_ellipsoid
-from ..materials import Material
-from .cmds_geometry import UserObjectGeometry, check_averaging
+from gprMax.cython.geometry_primitives import build_ellipsoid
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.materials import Material
+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:
@@ -41,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"]
@@ -77,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/cmds_geometry/fractal_box.py b/gprMax/user_objects/cmds_geometry/fractal_box.py
similarity index 96%
rename from gprMax/cmds_geometry/fractal_box.py
rename to gprMax/user_objects/cmds_geometry/fractal_box.py
index af5d9bba..b6837bd1 100644
--- a/gprMax/cmds_geometry/fractal_box.py
+++ b/gprMax/user_objects/cmds_geometry/fractal_box.py
@@ -21,16 +21,18 @@ 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.grid.fdtd_grid import FDTDGrid
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 rotate_2point_object
+from gprMax.user_objects.rotatable import RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
logger = logging.getLogger(__name__)
-class FractalBox(UserObjectGeometry):
+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.
@@ -54,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"]
@@ -97,7 +95,7 @@ class FractalBox(UserObjectGeometry):
seed = None
if self.do_rotate:
- self._do_rotate()
+ self._do_rotate(grid)
# Check averaging
try:
@@ -108,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)
@@ -188,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/cmds_geometry/geometry_objects_read.py b/gprMax/user_objects/cmds_geometry/geometry_objects_read.py
similarity index 91%
rename from gprMax/cmds_geometry/geometry_objects_read.py
rename to gprMax/user_objects/cmds_geometry/geometry_objects_read.py
index 46b7c243..789a569e 100644
--- a/gprMax/cmds_geometry/geometry_objects_read.py
+++ b/gprMax/user_objects/cmds_geometry/geometry_objects_read.py
@@ -22,24 +22,24 @@ from pathlib import Path
import h5py
import gprMax.config as config
-
-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
+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
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/cmds_geometry/plate.py b/gprMax/user_objects/cmds_geometry/plate.py
similarity index 87%
rename from gprMax/cmds_geometry/plate.py
rename to gprMax/user_objects/cmds_geometry/plate.py
index 90557b0e..5de480e9 100644
--- a/gprMax/cmds_geometry/plate.py
+++ b/gprMax/user_objects/cmds_geometry/plate.py
@@ -20,14 +20,17 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import (build_face_xy, build_face_xz,
- build_face_yz)
-from .cmds_geometry import UserObjectGeometry, rotate_2point_object
+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 RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
+
+from .cmds_geometry import rotate_2point_object
logger = logging.getLogger(__name__)
-class Plate(UserObjectGeometry):
+class Plate(RotatableMixin, GeometryUserObject):
"""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/cmds_geometry/sphere.py b/gprMax/user_objects/cmds_geometry/sphere.py
similarity index 90%
rename from gprMax/cmds_geometry/sphere.py
rename to gprMax/user_objects/cmds_geometry/sphere.py
index d3577b34..ef2db9cd 100644
--- a/gprMax/cmds_geometry/sphere.py
+++ b/gprMax/user_objects/cmds_geometry/sphere.py
@@ -20,14 +20,15 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import build_sphere
-from ..materials import Material
-from .cmds_geometry import UserObjectGeometry, check_averaging
+from gprMax.cython.geometry_primitives import build_sphere
+from gprMax.grid.fdtd_grid import FDTDGrid
+from gprMax.materials import Material
+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:
@@ -39,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"]
@@ -72,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/cmds_geometry/triangle.py b/gprMax/user_objects/cmds_geometry/triangle.py
similarity index 90%
rename from gprMax/cmds_geometry/triangle.py
rename to gprMax/user_objects/cmds_geometry/triangle.py
index 17d4abd7..4a752d74 100644
--- a/gprMax/cmds_geometry/triangle.py
+++ b/gprMax/user_objects/cmds_geometry/triangle.py
@@ -20,14 +20,18 @@ import logging
import numpy as np
-from ..cython.geometry_primitives import build_triangle
-from ..materials import Material
-from .cmds_geometry import UserObjectGeometry, check_averaging, rotate_point
+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 RotatableMixin
+from gprMax.user_objects.user_objects import GeometryUserObject
+
+from .cmds_geometry import rotate_point
logger = logging.getLogger(__name__)
-class Triangle(UserObjectGeometry):
+class Triangle(RotatableMixin, GeometryUserObject):
"""Introduces a triangular patch or a triangular prism with specific
properties into the model.
@@ -43,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)
@@ -63,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"]
@@ -74,7 +74,7 @@ class Triangle(UserObjectGeometry):
raise
if self.do_rotate:
- self._do_rotate()
+ self._do_rotate(grid)
# Check averaging
try:
@@ -96,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/cmds_multiuse.py b/gprMax/user_objects/cmds_multiuse.py
similarity index 78%
rename from gprMax/cmds_multiuse.py
rename to gprMax/user_objects/cmds_multiuse.py
index 55cc64fb..fc34a347 100644
--- a/gprMax/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
@@ -28,123 +30,81 @@ 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.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.user_inputs import MainGridUserInput
-
-from .cmds_geometry.cmds_geometry import (
- UserObjectGeometry,
+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_objects.cmds_geometry.cmds_geometry import (
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.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
logger = logging.getLogger(__name__)
-class UserObjectMulti(ABC):
- """Object that can occur multiple times in a model."""
+class ExcitationFile(GridUserObject):
+ """Specify file containing amplitude values of custom waveforms.
- 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(UserObjectMulti):
- """An ASCII file that contains columns of amplitude values that specify
- custom waveform shapes that can be used with sources in the model.
+ 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
@@ -162,13 +122,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"
@@ -191,38 +150,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:
@@ -236,7 +218,6 @@ class Waveform(UserObjectMulti):
)
raise ValueError
- grid = uip.grid
if wavetype != "user":
try:
amp = self.kwargs["amp"]
@@ -288,7 +269,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:
@@ -310,7 +291,7 @@ class Waveform(UserObjectMulti):
grid.waveforms.append(w)
-class VoltageSource(UserObjectMulti):
+class VoltageSource(RotatableMixin, GridUserObject):
"""Specifies a voltage source at an electric field location.
Attributes:
@@ -323,19 +304,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
@@ -343,7 +323,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()
@@ -353,7 +333,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)
@@ -380,6 +359,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)
@@ -435,15 +415,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 "
@@ -456,7 +435,7 @@ class VoltageSource(UserObjectMulti):
grid.voltagesources.append(v)
-class HertzianDipole(UserObjectMulti):
+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.
@@ -469,19 +448,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
@@ -489,7 +467,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"]
@@ -498,7 +476,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)
@@ -525,6 +502,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)
@@ -575,15 +553,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(
@@ -605,7 +582,7 @@ class HertzianDipole(UserObjectMulti):
grid.hertziandipoles.append(h)
-class MagneticDipole(UserObjectMulti):
+class MagneticDipole(RotatableMixin, GridUserObject):
"""Simulates an infinitesimal magnetic dipole.
Often referred to as an additive or soft source.
@@ -618,19 +595,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
@@ -638,7 +614,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"]
@@ -647,7 +623,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)
@@ -674,6 +649,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)
@@ -725,15 +701,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 "
@@ -745,7 +720,7 @@ class MagneticDipole(UserObjectMulti):
grid.magneticdipoles.append(m)
-class TransmissionLine(UserObjectMulti):
+class TransmissionLine(RotatableMixin, GridUserObject):
"""Specifies a one-dimensional transmission line model at an electric
field location.
@@ -759,19 +734,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
@@ -779,7 +753,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"]
@@ -789,7 +763,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)
@@ -825,6 +798,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)
@@ -843,8 +817,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
@@ -885,14 +858,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(
@@ -906,7 +879,7 @@ class TransmissionLine(UserObjectMulti):
grid.transmissionlines.append(t)
-class Rx(UserObjectMulti):
+class Rx(RotatableMixin, GridUserObject):
"""Specifies output points in the model.
These are locations where the values of the electric and magnetic field
@@ -919,20 +892,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,
@@ -953,17 +926,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)
@@ -971,8 +944,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"]
@@ -981,7 +952,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()
@@ -994,7 +965,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(
@@ -1016,7 +987,7 @@ class Rx(UserObjectMulti):
return r
-class RxArray(UserObjectMulti):
+class RxArray(GridUserObject):
"""Defines multiple output points in the model.
Attributes:
@@ -1025,12 +996,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"]
@@ -1039,6 +1016,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)
@@ -1078,7 +1056,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 "
@@ -1086,8 +1063,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):
@@ -1105,7 +1080,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, "
@@ -1115,7 +1090,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.
@@ -1136,10 +1111,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
@@ -1147,9 +1129,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
@@ -1162,6 +1142,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:
@@ -1242,7 +1223,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
@@ -1322,7 +1303,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.
@@ -1334,12 +1315,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"]
@@ -1373,7 +1360,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
@@ -1391,7 +1377,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."
)
@@ -1399,7 +1385,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.
@@ -1413,12 +1399,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"]
@@ -1433,7 +1425,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):
@@ -1475,7 +1466,7 @@ class AddDebyeDispersion(UserObjectMulti):
)
-class AddLorentzDispersion(UserObjectMulti):
+class AddLorentzDispersion(GridUserObject):
"""Adds dispersive properties to already defined Material based on a
multi-pole Lorentz formulation.
@@ -1490,12 +1481,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"]
@@ -1511,7 +1508,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):
@@ -1558,7 +1554,7 @@ class AddLorentzDispersion(UserObjectMulti):
)
-class AddDrudeDispersion(UserObjectMulti):
+class AddDrudeDispersion(GridUserObject):
"""Adds dispersive properties to already defined Material based on a
multi-pole Drude formulation.
@@ -1570,12 +1566,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"]
@@ -1590,7 +1592,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):
@@ -1634,7 +1635,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)
@@ -1650,12 +1651,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"]
@@ -1698,7 +1705,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
@@ -1725,7 +1731,7 @@ class SoilPeplinski(UserObjectMulti):
grid.mixingmodels.append(s)
-class MaterialRange(UserObjectMulti):
+class MaterialRange(GridUserObject):
"""Creates varying material properties for stochastic models.
Attributes:
@@ -1740,12 +1746,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"]
@@ -1803,7 +1815,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
@@ -1826,7 +1837,7 @@ class MaterialRange(UserObjectMulti):
grid.mixingmodels.append(s)
-class MaterialList(UserObjectMulti):
+class MaterialList(GridUserObject):
"""Creates varying material properties for stochastic models.
Attributes:
@@ -1834,19 +1845,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
@@ -1860,163 +1876,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 .geometry_outputs import GeometryViewVoxels as GeometryViewUser
- else:
- from .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
@@ -2041,11 +1901,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"]
@@ -2131,7 +1998,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:
@@ -2141,11 +2007,17 @@ class PMLCFS(UserObjectMulti):
raise ValueError
+"""
+TODO: Can this be removed?
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 = []
@@ -2157,3 +2029,4 @@ class Subgrid(UserObjectMulti):
else:
logger.exception("This object is unknown to gprMax.")
raise ValueError
+"""
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
new file mode 100644
index 00000000..8ac2a6ba
--- /dev/null
+++ b/gprMax/user_objects/cmds_singleuse.py
@@ -0,0 +1,594 @@
+# 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 = 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:
+ # 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")
+
+ thickness = model.G.pmls["thickness"]
+
+ logger.info(
+ f"PML thickness: x0={thickness['x0']}, y0={thickness['y0']},"
+ f" z0={thickness['z0']}, xmax={thickness['xmax']},"
+ f" ymax={thickness['ymax']}, zmax={thickness['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: Optional[str] = None,
+ 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."
+ )
+
+ 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)
+ 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:
+ 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):
+ 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):
+ """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."
+ )
+
+
+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)
diff --git a/gprMax/user_objects/rotatable.py b/gprMax/user_objects/rotatable.py
new file mode 100644
index 00000000..19f2492d
--- /dev/null
+++ b/gprMax/user_objects/rotatable.py
@@ -0,0 +1,46 @@
+from abc import ABC, abstractmethod
+from typing import Optional, Tuple
+
+from gprMax.grid.fdtd_grid import FDTDGrid
+
+
+class RotatableMixin(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, *args, **kwargs):
+ super().__init__(*args, **kwargs) # Forward all unused arguments
+ 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, grid: FDTDGrid):
+ """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..b4c4c905
--- /dev/null
+++ b/gprMax/user_objects/user_objects.py
@@ -0,0 +1,154 @@
+from abc import ABC, abstractmethod
+from typing import List, Union
+
+from gprMax import config
+from gprMax.grid.fdtd_grid import FDTDGrid
+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
+
+ 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] = []
+ 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 ModelUserObject(UserObject):
+ """User defined object to add to the model."""
+
+ @abstractmethod
+ def build(self, model: Model):
+ """Build user object and set model properties.
+
+ Args:
+ model: Model to set the properties of.
+ """
+ pass
+
+
+class GridUserObject(UserObject):
+ """User defined object to add to a grid."""
+
+ @abstractmethod
+ 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(UserObject):
+ """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.
+
+ 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 GeometryUserObject(GridUserObject):
+ """User defined object that adds geometry to a grid."""
+
+ @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