Merge branch '38-refactor-scene-and-userobjects' into mpi

这个提交包含在:
nmannall
2025-01-06 16:43:39 +00:00
当前提交 145c463eba
共有 32 个文件被更改,包括 1711 次插入1244 次删除

查看文件

@@ -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"

查看文件

@@ -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 <http://www.gnu.org/licenses/>.
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"])

查看文件

@@ -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:

查看文件

@@ -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)

查看文件

@@ -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__)

查看文件

@@ -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)

查看文件

@@ -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.

查看文件

@@ -16,22 +16,24 @@
# You should have received a copy of the GNU General Public License
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
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)

查看文件

@@ -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__(

查看文件

@@ -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

查看文件

@@ -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

查看文件

@@ -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

查看文件

@@ -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

查看文件

@@ -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)

查看文件

@@ -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.

查看文件

@@ -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)

查看文件

@@ -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)

查看文件

@@ -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))

查看文件

@@ -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)

查看文件

@@ -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)

查看文件

@@ -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:

查看文件

@@ -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:

查看文件

@@ -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)

查看文件

@@ -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)

查看文件

@@ -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)

查看文件

@@ -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)

查看文件

@@ -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 <http://www.gnu.org/licenses/>.
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)

查看文件

@@ -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

查看文件

@@ -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