Merge pull request #481 from gprMax/mpi

这个提交包含在:
Antonis Giannopoulos
2025-03-26 18:16:03 +00:00
提交者 GitHub
当前提交 07289cb989
共有 323 个文件被更改,包括 17082 次插入8195 次删除

5
.gitattributes vendored
查看文件

@@ -1 +1,6 @@
tools/Jupyter_notebooks/* linguist-vendored
reframe_tests/regression_checks/TestGeometryView_5176823e/partial_volume.vtkhdf filter=lfs diff=lfs merge=lfs -text
reframe_tests/regression_checks/TestGeometryView_5176823e/full_volume.vtkhdf filter=lfs diff=lfs merge=lfs -text
reframe_tests/regression_checks/TestGeometryView_77980202/full_volume.vtkhdf filter=lfs diff=lfs merge=lfs -text
reframe_tests/regression_checks/TestGeometryObject_a6a096cb/full_volume.h5 filter=lfs diff=lfs merge=lfs -text
reframe_tests/regression_checks/TestGeometryObject_a6a096cb/partial_volume.h5 filter=lfs diff=lfs merge=lfs -text

9
.gitignore vendored
查看文件

@@ -1,6 +1,7 @@
# Python
*.pyc
*.pyo
.venv
# Cython
*.c
@@ -27,3 +28,11 @@ docs/.buildinfo
.vscode
testing/*.out
testing/*.png
# Output files from running the examples
examples/*.h5
examples/*.pdf
# Test coverage files
.coverage
cov.xml

查看文件

@@ -1,4 +1,5 @@
# See https://pre-commit.com for more information
exclude: '\S*.map'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
@@ -7,14 +8,15 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-toml
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.0
hooks:
- id: black
args: ["--line-length", "120"] # Adjust the max line length value as needed.
args: ["--line-length", "100"] # Adjust the max line length value as needed.
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--line-length", "120", "--profile", "black"]
args: ["--line-length", "100", "--profile", "black"]

查看文件

@@ -24,5 +24,3 @@ python:
sphinx:
configuration: docs/source/conf.py

查看文件

@@ -171,7 +171,7 @@ Argument name Type Description
====================== ========= ===========
``-n`` integer Number of required simulation runs. This option can be used to run a series of models, e.g. to create a B-scan with 60 traces: ``(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60``
``-i`` integer Model number to start/restart the simulation from. It would typically be used to restart a series of models from a specific model number, with the n argument, e.g. to restart from A-scan 45 when creating a B-scan with 60 traces.
``-mpi`` integer number of Message Passing Interface (MPI) tasks, i.e. master + workers, for MPI task farm. This option is most usefully combined with ``-n`` to allow individual models to be farmed out using an MPI task farm, e.g. to create a B-scan with 60 traces and use MPI to farm out each trace: ``(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60 -mpi 61``. For further details see the `parallel performance section of the User Guide <http://docs.gprmax.com/en/latest/openmp_mpi.html>`_
``-taskfarm`` integer number of Message Passing Interface (MPI) tasks, i.e. master + workers, for MPI task farm. This option is most usefully combined with ``-n`` to allow individual models to be farmed out using an MPI task farm, e.g. to create a B-scan with 60 traces and use MPI to farm out each trace: ``(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60 -taskfarm 61``. For further details see the `parallel performance section of the User Guide <http://docs.gprmax.com/en/latest/openmp_mpi.html>`_
``-gpu`` list/bool Flag to use NVIDIA GPU or list of NVIDIA GPU device ID(s) for specific GPU card(s), e.g. ``-gpu 0 1``
``-opencl`` list/bool Flag to use OpenCL or list of OpenCL device ID(s) for specific compute device(s).
``--geometry-only`` flag Build a model and produce any geometry views but do not run the simulation, e.g. to check the geometry of a model is correct: ``(gprMax)$ python -m gprMax examples/heterogeneous_soil.in --geometry-only``

8
docs/.gitignore vendored 普通文件
查看文件

@@ -0,0 +1,8 @@
_*/
doctrees/
dirhtml/
*.html
.buildinfo
objects.inv
searchindex.js

查看文件

@@ -29,7 +29,7 @@ By default, gprMax will try to determine and use the maximum number of OpenMP th
MPI
===
By default, the MPI task farm functionality is turned off. It can be used with the ``-mpi`` command line option, which specifies the total number of MPI tasks, i.e. master + workers, for the MPI task farm. This option is most usefully combined with ``-n`` to allow individual models to be farmed out using an MPI task farm, e.g. to create a B-scan with 60 traces and use MPI to farm out each trace: ``(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60 -mpi 61``.
By default, the MPI task farm functionality is turned off. It can be used with the ``-taskfarm`` command line option, which specifies the total number of MPI tasks, i.e. master + workers, for the MPI task farm. This option is most usefully combined with ``-n`` to allow individual models to be farmed out using an MPI task farm, e.g. to create a B-scan with 60 traces and use MPI to farm out each trace: ``(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60 -taskfarm 61``.
Software required
-----------------
@@ -117,8 +117,8 @@ For example, to run a B-scan that contains 60 A-scans (traces) on a system with
.. code-block:: none
(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60 -mpi 5 -gpu 0 1 2 3
(gprMax)$ python -m gprMax examples/cylinder_Bscan_2D.in -n 60 -taskfarm 5 -gpu 0 1 2 3
.. note::
The argument given with ``-mpi`` is the number of MPI tasks, i.e. master + workers, for the MPI task farm. So in this case, 1 master (CPU) and 4 workers (GPU cards). The integers given with the ``-gpu`` argument are the NVIDIA CUDA device IDs for the specific GPU cards to be used.
The argument given with ``-taskfarm`` is the number of MPI tasks, i.e. master + workers, for the MPI task farm. So in this case, 1 master (CPU) and 4 workers (GPU cards). The integers given with the ``-gpu`` argument are the NVIDIA CUDA device IDs for the specific GPU cards to be used.

查看文件

@@ -34,7 +34,7 @@ Here is an example of a job script for running models, e.g. A-scans to make a B-
In this example, 10 models will be distributed as independent tasks in an HPC environment using MPI.
The ``-mpi`` argument is passed to gprMax which takes the number of MPI tasks to run. This should be the number of models (worker tasks) plus one extra for the master task.
The ``-taskfarm`` argument is passed to gprMax which takes the number of MPI tasks to run. This should be the number of models (worker tasks) plus one extra for the master task.
The ``NSLOTS`` variable which is required to set the total number of slots/cores for the parallel environment ``-pe mpi`` is usually the number of MPI tasks multiplied by the number of OpenMP threads per task. In this example the number of MPI tasks is 11 and the number of OpenMP threads per task is 16, so 176 slots are required.

查看文件

@@ -622,7 +622,7 @@ For example, to create an orthogonal parallelepiped with fractal distributed pro
.. note::
* Currently (2024) we are not aware of a formulation of Perfectly Matched Layer (PML) absorbing boundary that can specifically handle distributions of material properties (such as those created by fractals) throughout the thickness of the PML, i.e. this is a required area of research. Our PML formulations can work to an extent depending on your modelling scenario and requirements. You may need to increase the thickness of the PML and/or consider tuning the parameters of the PML (:ref:`pml-tuning`) to improve performance for your specific model.
* Currently (2024) we are not aware of a formulation of Perfectly Matched Layer (PML) absorbing boundary that can specifically handle distributions of material properties (such as those created by fractals) throughout the thickness of the PML, i.e. this is a required area of research. Our PML formulations can work to an extent depending on your modelling scenario and requirements. You may need to increase the thickness of the PML and/or consider tuning the parameters of the PML (:ref:`pml-tuning`) to improve performance for your specific model.
#add_surface_roughness:
-----------------------

查看文件

@@ -10,29 +10,30 @@ 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,
DiscretePlaneWave,
ExcitationFile,
GeometryObjectsWrite,
GeometryView,
HertzianDipole,
MagneticDipole,
Material,
@@ -40,15 +41,13 @@ from .cmds_multiuse import (
MaterialRange,
Rx,
RxArray,
Snapshot,
SoilPeplinski,
Subgrid,
TransmissionLine,
VoltageSource,
Waveform,
)
from .cmds_singleuse import (
from .user_objects.cmds_output import GeometryObjectsWrite, GeometryView, Snapshot
from .user_objects.cmds_singleuse import (
Discretisation,
Domain,
OMPThreads,
@@ -60,9 +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,407 +0,0 @@
# Copyright (C) 2015-2025: 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
import numpy as np
import gprMax.config as config
from .pml import PML
from .utilities.host_info import set_omp_threads
logger = logging.getLogger(__name__)
class Properties:
pass
class UserObjectSingle:
"""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 = None
self.kwargs = kwargs
self.props = Properties()
self.autotranslate = True
for k, v in kwargs.items():
setattr(self.props, k, v)
def build(self, grid, uip):
pass
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, G, uip):
try:
title = self.kwargs["name"]
G.title = title
logger.info(f"Model title: {G.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, G, uip):
try:
G.dl = np.array(self.kwargs["p1"])
G.dx, G.dy, G.dz = self.kwargs["p1"]
except KeyError:
logger.exception(f"{self.__str__()} discretisation requires a point")
raise
if G.dl[0] <= 0:
logger.exception(
f"{self.__str__()} discretisation requires the "
f"x-direction spatial step to be greater than zero"
)
raise ValueError
if G.dl[1] <= 0:
logger.exception(
f"{self.__str__()} discretisation requires the "
f"y-direction spatial step to be greater than zero"
)
raise ValueError
if G.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: {G.dl[0]:g} x {G.dl[1]:g} x {G.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, G, uip):
try:
G.nx, G.ny, G.nz = uip.discretise_point(self.kwargs["p1"])
except KeyError:
logger.exception(f"{self.__str__()} please specify a point")
raise
if G.nx == 0 or G.ny == 0 or G.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 ({G.nx:d} x {G.ny:d} x {G.nz:d} = "
+ f"{(G.nx * G.ny * G.nz):g} cells)"
)
# Calculate time step at CFL limit; switch off appropriate PMLs for 2D
if G.nx == 1:
config.get_model_config().mode = "2D TMx"
G.pmls["thickness"]["x0"] = 0
G.pmls["thickness"]["xmax"] = 0
elif G.ny == 1:
config.get_model_config().mode = "2D TMy"
G.pmls["thickness"]["y0"] = 0
G.pmls["thickness"]["ymax"] = 0
elif G.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, G, 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
G.dt_mod = f
G.dt = G.dt * G.dt_mod
logger.info(f"Time step (modified): {G.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, G, uip):
# If number of iterations given
# The +/- 1 used in calculating the number of iterations is to account for
# the fact that the solver (iterations) loop runs from 0 to < G.iterations
try:
iterations = int(self.kwargs["iterations"])
G.timewindow = (iterations - 1) * G.dt
G.iterations = iterations
except KeyError:
pass
try:
tmp = float(self.kwargs["time"])
if tmp > 0:
G.timewindow = tmp
G.iterations = int(np.ceil(tmp / G.dt)) + 1
else:
logger.exception(
self.__str__() + " must have a value greater than zero"
)
raise ValueError
except KeyError:
pass
if not G.timewindow:
logger.exception(self.__str__() + " specify a time or number of iterations")
raise ValueError
logger.info(f"Time window: {G.timewindow:g} secs ({G.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, G, 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, G, uip):
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, G, uip):
try:
G.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 {G.srcsteps[0] * G.dx:g}m, "
f"{G.srcsteps[1] * G.dy:g}m, {G.srcsteps[2] * G.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, G, uip):
try:
G.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 {G.rxsteps[0] * G.dx:g}m, "
f"{G.rxsteps[1] * G.dy:g}m, {G.rxsteps[2] * G.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"])

查看文件

@@ -20,11 +20,14 @@ import logging
import sys
import warnings
from pathlib import Path
from typing import List, Optional, Union
import cython
import numpy as np
from colorama import Fore, Style, init
from gprMax.scene import Scene
init()
from scipy.constants import c
from scipy.constants import epsilon_0 as e0
@@ -35,34 +38,17 @@ from .utilities.utilities import get_terminal_width
logger = logging.getLogger(__name__)
# Single instance of SimConfig to hold simulation configuration parameters.
sim_config = None
# Instances of ModelConfig that hold model configuration parameters.
model_configs = []
# Each model in a simulation is given a unique number when the instance of
# ModelConfig is created
model_num = 0
def get_model_config():
"""Return ModelConfig instace for specific model."""
if sim_config.args.mpi:
return model_configs
else:
return model_configs[model_num]
class ModelConfig:
"""Configuration parameters for a model.
N.B. Multiple models can exist within a simulation
"""
def __init__(self):
def __init__(self, model_num):
self.mode = "3D"
self.grids = []
self.ompthreads = None
self.model_num = model_num
# Store information for CUDA or OpenCL solver
# dev: compute device object.
@@ -73,7 +59,7 @@ class ModelConfig:
if sim_config.general["solver"] in ["cuda", "opencl"]:
if sim_config.general["solver"] == "cuda":
devs = sim_config.args.gpu
elif sim_config.general["solver"] == "opencl":
else: # opencl
devs = sim_config.args.opencl
# If a list of lists of deviceIDs is found, flatten it
@@ -97,17 +83,13 @@ class ModelConfig:
self.mem_overhead = 65e6
self.mem_use = self.mem_overhead
self.reuse_geometry = False
# String to print at start of each model run
s = (
f"\n--- Model {model_num + 1}/{sim_config.model_end}, "
f"input file: {sim_config.input_file_path}"
)
self.inputfilestr = (
Fore.GREEN
+ f"{s} {'-' * (get_terminal_width() - 1 - len(s))}\n"
+ Style.RESET_ALL
Fore.GREEN + f"{s} {'-' * (get_terminal_width() - 1 - len(s))}\n\n" + Style.RESET_ALL
)
# Output file path and name for specific model
@@ -145,17 +127,17 @@ class ModelConfig:
"crealfunc": None,
}
def reuse_geometry(self):
return self.model_num != 0 and sim_config.args.geometry_fixed
def get_scene(self):
try:
return sim_config.scenes[model_num]
except:
return None
return sim_config.get_scene(self.model_num)
def get_usernamespace(self):
"""Namespace only used with #python blocks which are deprecated."""
tmp = {
"number_model_runs": sim_config.model_end,
"current_model_run": model_num + 1,
"current_model_run": self.model_num + 1,
"inputfile": sim_config.input_file_path.resolve(),
}
return dict(**sim_config.em_consts, **tmp)
@@ -182,17 +164,13 @@ class ModelConfig:
outputdir: string of output file directory given by input file command.
"""
if not outputdir:
try:
self.output_file_path = Path(self.args.outputfile)
except AttributeError:
self.output_file_path = sim_config.input_file_path.with_suffix("")
if outputdir is not None:
Path(outputdir).mkdir(exist_ok=True)
self.output_file_path = Path(outputdir, sim_config.input_file_path.stem)
elif sim_config.args.outputfile is not None:
self.output_file_path = Path(sim_config.args.outputfile).with_suffix("")
else:
try:
Path(outputdir).mkdir(exist_ok=True)
self.output_file_path = Path(outputdir, sim_config.input_file_path.stem)
except AttributeError:
self.output_file_path = sim_config.input_file_path.with_suffix("")
self.output_file_path = sim_config.input_file_path.with_suffix("")
parts = self.output_file_path.parts
self.output_file_path = Path(*parts[:-1], parts[-1] + self.appendmodelnumber)
@@ -215,6 +193,14 @@ class SimulationConfig:
N.B. A simulation can consist of multiple models.
"""
# TODO: Make this an enum
em_consts = {
"c": c, # Speed of light in free space (m/s)
"e0": e0, # Permittivity of free space (F/m)
"m0": m0, # Permeability of free space (H/m)
"z0": np.sqrt(m0 / e0), # Impedance of free space (Ohms)
}
def __init__(self, args):
"""
Args:
@@ -223,39 +209,67 @@ class SimulationConfig:
self.args = args
if self.args.mpi and self.args.geometry_fixed:
logger.exception("The geometry fixed option cannot be used with MPI.")
self.geometry_fixed: bool = args.geometry_fixed
self.geometry_only: bool = args.geometry_only
self.gpu: Union[List[str], bool] = args.gpu
self.mpi: List[int] = args.mpi
self.number_of_models: int = args.n
self.opencl: Union[List[str], bool] = args.opencl
self.output_file_path: str = args.outputfile
self.taskfarm: bool = args.taskfarm
self.write_processed_input_file: bool = (
args.write_processed
) # For depreciated Python blocks
if self.taskfarm and self.geometry_fixed:
logger.error("The geometry fixed option cannot be used with MPI taskfarm.")
raise ValueError
if self.args.gpu and self.args.opencl:
logger.exception("You cannot use both CUDA and OpenCl simultaneously.")
if self.gpu and self.opencl:
logger.error("You cannot use both CUDA and OpenCl simultaneously.")
raise ValueError
if self.mpi and hasattr(self.args, "subgrid") and self.args.subgrid:
logger.error("You cannot use subgrids with MPI.")
raise ValueError
# Each model in a simulation is given a unique number when the instance of ModelConfig is created
self.current_model = 0
# Instances of ModelConfig that hold model configuration parameters.
# TODO: Consider if this would be better as a dictionary.
# Or maybe a non fixed length list (i.e. append each config)
self.model_configs: List[Optional[ModelConfig]] = [None] * self.number_of_models
# General settings for the simulation
# solver: cpu, cuda, opencl.
# precision: data type for electromagnetic field output (single/double).
# progressbars: progress bars on stdoout or not - switch off
# progressbars when logging level is greater than
# info (20)
# progressbars when logging level is greater than info (20)
# or when specified by the user.
if args.show_progress_bars and args.hide_progress_bars:
logger.error("You cannot both show and hide progress bars.")
raise ValueError
self.general = {
"solver": "cpu",
"precision": "single",
"progressbars": args.log_level <= 20,
"progressbars": (
args.show_progress_bars or (args.log_level <= 20 and not args.hide_progress_bars)
),
}
self.em_consts = {
"c": c, # Speed of light in free space (m/s)
"e0": e0, # Permittivity of free space (F/m)
"m0": m0, # Permeability of free space (H/m)
"z0": np.sqrt(m0 / e0), # Impedance of free space (Ohms)
}
if self.mpi and self.general["progressbars"]:
from mpi4py import MPI
self.general["progressbars"] = MPI.COMM_WORLD.rank == 0
# Store information about host machine
self.hostinfo = get_host_info()
# CUDA
if self.args.gpu is not None:
if self.gpu is not None:
self.general["solver"] = "cuda"
# Both single and double precision are possible on GPUs, but single
# provides best performance.
@@ -272,7 +286,7 @@ class SimulationConfig:
self.devices["devs"] = detect_cuda_gpus()
# OpenCL
if self.args.opencl is not None:
if self.opencl is not None:
self.general["solver"] = "opencl"
self.general["precision"] = "single"
self.devices = {
@@ -297,19 +311,23 @@ class SimulationConfig:
if (self.general["subgrid"] and self.general["solver"] == "cuda") or (
self.general["subgrid"] and self.general["solver"] == "opencl"
):
logger.exception(
"You cannot currently use CUDA or OpenCL-based "
"solvers with models that contain sub-grids."
logger.error(
"You cannot currently use CUDA or OpenCL-based solvers with models that contain sub-grids."
)
raise ValueError
else:
self.general["subgrid"] = False
self.autotranslate_subgrid_coordinates = True
if hasattr(args, "autotranslate"):
self.autotranslate_subgrid_coordinates: bool = args.autotranslate
# Scenes parameter may not exist if user enters via CLI
try:
self.scenes: List[Optional[Scene]]
if hasattr(args, "scenes") and args.scenes is not None:
self.scenes = args.scenes
except AttributeError:
self.scenes = []
else:
self.scenes = [None] * self.number_of_models
# Set more complex parameters
self._set_precision()
@@ -333,9 +351,7 @@ class SimulationConfig:
return dev
if not found:
logger.exception(
f"Compute device with device ID {deviceID} does not exist."
)
logger.exception(f"Compute device with device ID {deviceID} does not exist.")
raise ValueError
def _set_precision(self):
@@ -376,6 +392,15 @@ class SimulationConfig:
elif self.general["solver"] == "opencl":
self.dtypes["C_complex"] = "cdouble"
def _set_input_file_path(self):
"""Sets input file path for CLI or API."""
# API
if self.args.inputfile is None:
self.input_file_path = Path(self.args.outputfile)
# API/CLI
else:
self.input_file_path = Path(self.args.inputfile)
def _set_model_start_end(self):
"""Sets range for number of models to run (internally 0 index)."""
if self.args.i:
@@ -388,11 +413,74 @@ class SimulationConfig:
self.model_start = modelstart
self.model_end = modelend
def _set_input_file_path(self):
"""Sets input file path for CLI or API."""
# API
if self.args.inputfile is None:
self.input_file_path = Path(self.args.outputfile)
# API/CLI
else:
self.input_file_path = Path(self.args.inputfile)
def get_model_config(self, model_num: Optional[int] = None) -> ModelConfig:
"""Return ModelConfig instance for specific model.
Args:
model_num: number of the model. If None, returns the config for the current model
Returns:
model_config: requested model config
"""
if model_num is None:
model_num = self.current_model
model_config = self.model_configs[model_num]
if model_config is None:
logger.error(f"Cannot get ModelConfig for model {model_num}. It has not been set.")
raise ValueError
return model_config
def set_model_config(self, model_config: ModelConfig, model_num: Optional[int] = None) -> None:
"""Set ModelConfig instace for specific model.
Args:
model_num: number of the model. If None, sets the config for the current model
"""
if model_num is None:
model_num = self.current_model
self.model_configs[model_num] = model_config
def set_current_model(self, model_num: int) -> None:
"""Set the current model by it's unique identifier
Args:
model_num: unique identifier for the current model
"""
self.current_model = model_num
def get_scene(self, model_num: Optional[int] = None) -> Optional[Scene]:
"""Return Scene instance for specific model.
Args:
model_num: number of the model. If None, returns the scene for the current model
Returns:
scene: requested scene
"""
if model_num is None:
model_num = self.current_model
return self.scenes[model_num]
def set_scene(self, scene: Scene, model_num: Optional[int] = None) -> None:
"""Set Scene instace for specific model.
Args:
model_num: number of the model. If None, sets the scene for the current model
"""
if model_num is None:
model_num = self.current_model
self.scenes[model_num] = scene
# Single instance of SimConfig to hold simulation configuration parameters.
sim_config: SimulationConfig = None
def get_model_config() -> ModelConfig:
"""Return ModelConfig instance for specific model."""
return sim_config.get_model_config()

查看文件

@@ -20,17 +20,24 @@ import datetime
import gc
import logging
import sys
from typing import Any, Dict, List, Optional
import humanize
import numpy as np
from colorama import Fore, Style, init
from gprMax.hash_cmds_file import parse_hash_commands
from gprMax.mpi_model import MPIModel
from gprMax.scene import Scene
init()
import gprMax.config as config
from gprMax.config import ModelConfig
from ._version import __version__, codename
from .model_build_run import ModelBuildRun
from .solvers import create_G, create_solver
from .model import Model
from .solvers import create_solver
from .utilities.host_info import print_cuda_info, print_host_info, print_opencl_info
from .utilities.utilities import get_terminal_width, logo, timer
@@ -38,26 +45,24 @@ logger = logging.getLogger(__name__)
class Context:
"""Standard context - models are run one after another and each model
can exploit parallelisation using either OpenMP (CPU), CUDA (GPU), or
OpenCL (CPU/GPU).
"""Standard context for building and running models.
Models are run one after another and each model can exploit
parallelisation using either OpenMP (CPU), CUDA (GPU), or OpenCL
(CPU/GPU).
"""
def __init__(self):
self.model_range = range(
config.sim_config.model_start, config.sim_config.model_end
)
self.tsimend = None
self.tsimstart = None
self.model_range = range(config.sim_config.model_start, config.sim_config.model_end)
self.sim_start_time = 0
self.sim_end_time = 0
def run(self):
"""Run the simulation in the correct context.
def _start_simulation(self) -> None:
"""Run pre-simulation steps
Returns:
results: dict that can contain useful results/data from simulation.
Start simulation timer. Output copyright notice and host info.
"""
self.tsimstart = timer()
self.sim_start_time = timer()
self.print_logo_copyright()
print_host_info(config.sim_config.hostinfo)
if config.sim_config.general["solver"] == "cuda":
@@ -65,57 +70,159 @@ class Context:
elif config.sim_config.general["solver"] == "opencl":
print_opencl_info(config.sim_config.devices["devs"])
# Clear list of model configs, which can be retained when gprMax is
# called in a loop, and want to avoid this.
config.model_configs = []
def _end_simulation(self) -> None:
"""Run post-simulation steps
Stop simulation timer. Output timing information.
"""
self.sim_end_time = timer()
self.print_sim_time_taken()
def run(self) -> Dict:
"""Run the simulation in the correct context.
Returns:
results: dict that can contain useful results/data from simulation.
"""
self._start_simulation()
for i in self.model_range:
config.model_num = i
model_config = config.ModelConfig()
config.model_configs.append(model_config)
self._run_model(i)
# Always create a grid for the first model. The next model to run
# only gets a new grid if the geometry is not re-used.
if i != 0 and config.sim_config.args.geometry_fixed:
config.get_model_config().reuse_geometry = True
else:
G = create_G()
model = ModelBuildRun(G)
model.build()
if not config.sim_config.args.geometry_only:
solver = create_solver(G)
model.solve(solver)
del solver, model
if not config.sim_config.args.geometry_fixed:
# Manual garbage collection required to stop memory leak on GPUs
# when using pycuda
del G
gc.collect()
self.tsimend = timer()
self.print_sim_time_taken()
self._end_simulation()
return {}
def print_logo_copyright(self):
def _run_model(self, model_num: int) -> None:
"""Process for running a single model.
Args:
model_num: index of model to be run
"""
config.sim_config.set_current_model(model_num)
model_config = self._create_model_config(model_num)
config.sim_config.set_model_config(model_config)
if not model_config.reuse_geometry():
scene = self._get_scene(model_num)
model = self._create_model()
scene.create_internal_objects(model)
model.build()
if not config.sim_config.geometry_only:
solver = create_solver(model)
model.solve(solver)
del solver
if not config.sim_config.geometry_fixed:
# Manual garbage collection required to stop memory leak on GPUs
# when using pycuda
del model.G, model
gc.collect()
def _create_model_config(self, model_num: int) -> ModelConfig:
"""Create model config and save to global config."""
return ModelConfig(model_num)
def _get_scene(self, model_num: int) -> Scene:
# API for multiple scenes / model runs
scene = config.sim_config.get_scene(model_num)
# If there is no scene, process the hash commands
if scene is None:
scene = Scene()
config.sim_config.set_scene(scene, model_num)
# Parse the input file into user objects and add them to the scene
scene = parse_hash_commands(scene)
return scene
def _create_model(self) -> Model:
return Model()
def print_logo_copyright(self) -> None:
"""Prints gprMax logo, version, and copyright/licencing information."""
logo_copyright = logo(f"{__version__} ({codename})")
logger.basic(logo_copyright)
def print_sim_time_taken(self):
def print_sim_time_taken(self) -> None:
"""Prints the total simulation time based on context."""
s = (
f"\n=== Simulation completed in "
f"{humanize.precisedelta(datetime.timedelta(seconds=self.tsimend - self.tsimstart), format='%0.4f')}"
"=== Simulation completed in "
f"{humanize.precisedelta(datetime.timedelta(seconds=self.sim_end_time - self.sim_start_time), format='%0.4f')}"
)
logger.basic(f"{s} {'=' * (get_terminal_width() - 1 - len(s))}\n")
class MPIContext(Context):
def __init__(self):
super().__init__()
from mpi4py import MPI
self.comm = MPI.COMM_WORLD
self.rank = self.comm.rank
requested_mpi_size = np.prod(config.sim_config.mpi)
if self.comm.size < requested_mpi_size:
raise ValueError(
f"MPI_COMM_WORLD size of {self.comm.size} is too small for requested dimensions of"
f" {config.sim_config.mpi}. {requested_mpi_size} ranks are required."
)
if self.rank >= requested_mpi_size:
logger.warning(
f"Rank {self.rank}: Only {requested_mpi_size} MPI ranks required for the"
" dimensions specified. Either increase your MPI dimension size, or request"
" fewer MPI tasks."
)
exit()
def _create_model(self) -> MPIModel:
return MPIModel()
def run(self) -> Dict:
try:
return super().run()
except:
logger.exception(f"Rank {self.rank} encountered an error. Aborting...")
self.comm.Abort()
def _run_model(self, model_num: int) -> None:
"""Process for running a single model.
Args:
model_num: index of model to be run
"""
config.sim_config.set_current_model(model_num)
model_config = self._create_model_config(model_num)
config.sim_config.set_model_config(model_config)
if not model_config.reuse_geometry():
model = self._create_model()
scene = self._get_scene(model_num)
scene.create_internal_objects(model)
model.build()
if not config.sim_config.geometry_only:
solver = create_solver(model)
model.solve(solver)
del solver
if not config.sim_config.geometry_fixed:
# Manual garbage collection required to stop memory leak on GPUs
# when using pycuda
del model.G, model
gc.collect()
class TaskfarmContext(Context):
"""Mixed mode MPI/OpenMP/CUDA context - MPI task farm is used to distribute
models, and each model parallelised using either OpenMP (CPU),
CUDA (GPU), or OpenCL (CPU/GPU).
@@ -125,23 +232,18 @@ class MPIContext(Context):
super().__init__()
from mpi4py import MPI
from gprMax.mpi import MPIExecutor
from gprMax.taskfarm import TaskfarmExecutor
self.comm = MPI.COMM_WORLD
self.rank = self.comm.rank
self.MPIExecutor = MPIExecutor
self.TaskfarmExecutor = TaskfarmExecutor
def _run_model(self, **work):
"""Process for running a single model.
def _create_model_config(self, model_num: int) -> ModelConfig:
"""Create model config and save to global config.
Args:
work: dict of any additional information that is passed to MPI
workers. By default only model number (i) is used.
Set device in model config according to MPI rank.
"""
# Create configuration for model
config.model_num = work["i"]
model_config = config.ModelConfig()
model_config = super()._create_model_config(model_num)
# Set GPU deviceID according to worker rank
if config.sim_config.general["solver"] == "cuda":
model_config.device = {
@@ -149,23 +251,18 @@ class MPIContext(Context):
"deviceID": self.rank - 1,
"snapsgpu2cpu": False,
}
config.model_configs = model_config
return model_config
G = create_G()
model = ModelBuildRun(G)
model.build()
def _run_model(self, **work) -> None:
"""Process for running a single model.
if not config.sim_config.args.geometry_only:
solver = create_solver(G)
model.solve(solver)
del solver, model
Args:
work: dict of any additional information that is passed to MPI
workers. By default only model number (i) is used.
"""
return super()._run_model(work["i"])
# Manual garbage collection required to stop memory leak on GPUs when
# using pycuda
del G
gc.collect()
def run(self):
def run(self) -> Optional[List[Optional[Dict]]]:
"""Specialise how the models are run.
Returns:
@@ -173,25 +270,17 @@ class MPIContext(Context):
"""
if self.rank == 0:
self.tsimstart = timer()
self.print_logo_copyright()
print_host_info(config.sim_config.hostinfo)
if config.sim_config.general["solver"] == "cuda":
print_cuda_info(config.sim_config.devices["devs"])
elif config.sim_config.general["solver"] == "opencl":
print_opencl_info(config.sim_config.devices["devs"])
self._start_simulation()
s = f"\n--- Input file: {config.sim_config.input_file_path}"
logger.basic(
Fore.GREEN
+ f"{s} {'-' * (get_terminal_width() - 1 - len(s))}\n"
+ Style.RESET_ALL
Fore.GREEN + f"{s} {'-' * (get_terminal_width() - 1 - len(s))}\n" + Style.RESET_ALL
)
sys.stdout.flush()
# Contruct MPIExecutor
executor = self.MPIExecutor(self._run_model, comm=self.comm)
# Contruct TaskfarmExecutor
executor = self.TaskfarmExecutor(self._run_model, comm=self.comm)
# Check GPU resources versus number of MPI tasks
if (
@@ -199,7 +288,7 @@ class MPIContext(Context):
and config.sim_config.general["solver"] == "cuda"
and executor.size - 1 > len(config.sim_config.devices["devs"])
):
logger.exception(
logger.error(
"Not enough GPU resources for number of "
"MPI tasks requested. Number of MPI tasks "
"should be equal to number of GPUs + 1."
@@ -216,6 +305,5 @@ class MPIContext(Context):
executor.join()
if executor.is_master():
self.tsimend = timer()
self.print_sim_time_taken()
self._end_simulation()
return results

查看文件

@@ -238,7 +238,7 @@ update_electric_dispersive_A = {
int x_T = (i % ($NX_T * $NY_T * $NZ_T)) / ($NY_T * $NZ_T);
int y_T = ((i % ($NX_T * $NY_T * $NZ_T)) % ($NY_T * $NZ_T)) / $NZ_T;
int z_T = ((i % ($NX_T * $NY_T * $NZ_T)) % ($NY_T * $NZ_T)) % $NZ_T;
// Ex component
if ((NY != 1 || NZ != 1) && x >= 0 && x < NX && y > 0 && y < NY && z > 0 && z < NZ) {
int materialEx = ID[IDX4D_ID(0,x_ID,y_ID,z_ID)];

查看文件

@@ -17,9 +17,81 @@
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
cimport numpy as np
cpdef get_line_properties(
int number_of_lines,
int nx,
int ny,
int nz,
np.uint32_t[:, :, :, :] ID
):
"""Generate connectivity array and get material ID for each line.
Args:
number_of_lines: Number of lines.
nx: Number of points in the x dimension.
ny: Number of points in the y dimension.
nz: Number of points in the z dimension.
ID: memoryview of sampled ID array according to geometry view
spatial discretisation.
Returns:
connectivity: NDArray of shape (2 * number_of_lines,) listing
the start and end point IDs of each line.
material_data: NDArray of shape (number_of_lines,) listing
material IDs for each line.
"""
cdef np.ndarray material_data = np.zeros(number_of_lines, dtype=np.uint32)
cdef np.ndarray connectivity = np.zeros(2 * number_of_lines, dtype=np.int32)
cdef int line_index = 0
cdef int connectivity_index = 0
cdef int point_id = 0
cdef int z_step = 1
cdef int y_step = nz + 1
cdef int x_step = y_step * (ny + 1)
cdef int i, j, k
for i in range(nx):
for j in range(ny):
for k in range(nz):
# x-direction cell edge
material_data[line_index] = ID[0, i, j, k]
connectivity[connectivity_index] = point_id
connectivity[connectivity_index + 1] = point_id + x_step
line_index += 1
connectivity_index += 2
# y-direction cell edge
material_data[line_index] = ID[1, i, j, k]
connectivity[connectivity_index] = point_id
connectivity[connectivity_index + 1] = point_id + y_step
line_index += 1
connectivity_index += 2
# z-direction cell edge
material_data[line_index] = ID[2, i, j, k]
connectivity[connectivity_index] = point_id
connectivity[connectivity_index + 1] = point_id + z_step
line_index += 1
connectivity_index += 2
# Next point
point_id += 1
# Skip point at (i, j, nz)
point_id += 1
# Skip points in line (i, ny, t) where 0 <= t <= nz
point_id += nz + 1
return connectivity, material_data
cpdef write_lines(
float xs,
float ys,

查看文件

@@ -110,9 +110,19 @@ cpdef bint is_inside_sector(
relpoint1 = px - ctrx
relpoint2 = py - ctry
return (not are_clockwise(sectorstart1, sectorstart2, relpoint1, relpoint2)
and are_clockwise(sectorend1, sectorend2, relpoint1, relpoint2)
and is_within_radius(relpoint1, relpoint2, radius))
if sectorangle <= np.pi:
return (
not are_clockwise(sectorstart1, sectorstart2, relpoint1, relpoint2)
and are_clockwise(sectorend1, sectorend2, relpoint1, relpoint2)
and is_within_radius(relpoint1, relpoint2, radius)
)
else:
return (
(
not are_clockwise(sectorstart1, sectorstart2, relpoint1, relpoint2)
or are_clockwise(sectorend1, sectorend2, relpoint1, relpoint2)
) and is_within_radius(relpoint1, relpoint2, radius)
)
cpdef bint point_in_polygon(
@@ -434,6 +444,12 @@ cpdef void build_triangle(
j2 = round_value(np.amax([z1, z2, z3]) / dz) + 1
levelcells = round_value(x1 / dx)
thicknesscells = round_value(thickness / dx)
# Bound to the size of the grid
if i2 > solid.shape[1]:
i2 = solid.shape[1]
if j2 > solid.shape[2]:
j2 = solid.shape[2]
elif normal == 'y':
area = 0.5 * (-z2 * x3 + z1 * (-x2 + x3) + x1 * (z2 - z3) + x2 * z3)
i1 = round_value(np.amin([x1, x2, x3]) / dx) - 1
@@ -442,6 +458,12 @@ cpdef void build_triangle(
j2 = round_value(np.amax([z1, z2, z3]) / dz) + 1
levelcells = round_value(y1 /dy)
thicknesscells = round_value(thickness / dy)
# Bound to the size of the grid
if i2 > solid.shape[0]:
i2 = solid.shape[0]
if j2 > solid.shape[2]:
j2 = solid.shape[2]
elif normal == 'z':
area = 0.5 * (-y2 * x3 + y1 * (-x2 + x3) + x1 * (y2 - y3) + x2 * y3)
i1 = round_value(np.amin([x1, x2, x3]) / dx) - 1
@@ -451,6 +473,18 @@ cpdef void build_triangle(
levelcells = round_value(z1 / dz)
thicknesscells = round_value(thickness / dz)
# Bound to the size of the grid
if i2 > solid.shape[0]:
i2 = solid.shape[0]
if j2 > solid.shape[1]:
j2 = solid.shape[1]
# Bound to the start of the grid
if i1 < 0:
i1 = 0
if j1 < 0:
j1 = 0
sign = np.sign(area)
for i in range(i1, i2):
@@ -960,7 +994,7 @@ cpdef void build_cone(
"""
cdef Py_ssize_t i, j, k
cdef int xs, xf, ys, yf, zs, zf, xc, yc, zc
cdef int xs, xf, ys, yf, zs, zf, xs_bound, xf_bound, ys_bound, yf_bound, zs_bound, zf_bound
cdef float f1f2mag, f2f1mag, f1ptmag, f2ptmag, dot1, dot2, factor1, factor2
cdef float theta1, theta2, distance1, distance2, R1, R2
cdef float height, distance_axis_1, distance_axis_2
@@ -1031,41 +1065,48 @@ cpdef void build_cone(
zs = round_value((z2 - Rmax) / dz) - 1
zf = round_value((z1 + Rmax) / dz) + 1
xs_bound = xs
xf_bound = xf
ys_bound = ys
yf_bound = yf
zs_bound = zs
zf_bound = zf
# Set bounds to domain if they outside
if xs < 0:
xs = 0
if xf > solid.shape[0]:
xf = solid.shape[0]
if ys < 0:
ys = 0
if yf > solid.shape[1]:
yf = solid.shape[1]
if zs < 0:
zs = 0
if zf > solid.shape[2]:
zf = solid.shape[2]
if xs_bound < 0:
xs_bound = 0
if xf_bound > solid.shape[0]:
xf_bound = solid.shape[0]
if ys_bound < 0:
ys_bound = 0
if yf_bound > solid.shape[1]:
yf_bound = solid.shape[1]
if zs_bound < 0:
zs_bound = 0
if zf_bound > solid.shape[2]:
zf_bound = solid.shape[2]
# x-aligned cone
if x_align:
for j in range(ys, yf):
for k in range(zs, zf):
for i in range(xs, xf):
if np.sqrt((j * dy + 0.5 * dy - y1)**2 + (k * dz + 0.5 * dz - z1)**2) <= ((i-xs)/(xf-xs))*(r2-r1) + r1:
for j in range(ys_bound, yf_bound):
for k in range(zs_bound, zf_bound):
for i in range(xs_bound, xf_bound):
if np.sqrt((j * dy + 0.5 * dy - y1)**2 + (k * dz + 0.5 * dz - z1)**2) <= ((i- xs)/(xf-xs))*(r2-r1) + r1:
build_voxel(i, j, k, numID, numIDx, numIDy, numIDz,
averaging, solid, rigidE, rigidH, ID)
# y-aligned cone
elif y_align:
for i in range(xs, xf):
for k in range(zs, zf):
for j in range(ys, yf):
for i in range(xs_bound, xf_bound):
for k in range(zs_bound, zf_bound):
for j in range(ys_bound, yf_bound):
if np.sqrt((i * dx + 0.5 * dx - x1)**2 + (k * dz + 0.5 * dz - z1)**2) <= ((j-ys)/(yf-ys))*(r2-r1) + r1:
build_voxel(i, j, k, numID, numIDx, numIDy, numIDz,
averaging, solid, rigidE, rigidH, ID)
# z-aligned cone
elif z_align:
for i in range(xs, xf):
for j in range(ys, yf):
for k in range(zs, zf):
for i in range(xs_bound, xf_bound):
for j in range(ys_bound, yf_bound):
for k in range(zs_bound, zf_bound):
if np.sqrt((i * dx + 0.5 * dx - x1)**2 + (j * dy + 0.5 * dy - y1)**2) <= ((k-zs)/(zf-zs))*(r2-r1) + r1:
build_voxel(i, j, k, numID, numIDx, numIDy, numIDz,
averaging, solid, rigidE, rigidH, ID)
@@ -1082,9 +1123,9 @@ cpdef void build_cone(
height = f1f2mag
for i in range(xs, xf):
for j in range(ys, yf):
for k in range(zs, zf):
for i in range(xs_bound, xf_bound):
for j in range(ys_bound, yf_bound):
for k in range(zs_bound, zf_bound):
# Build flag - default false, set to True if point is in cone
build = 0
# Vector from centre of first cone face to test point

查看文件

@@ -18,31 +18,34 @@
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
cimport cython
from libc.math cimport floor, ceil, round, sqrt, tan, cos, sin, atan2, abs, pow, exp, M_PI
from libc.stdio cimport FILE, fopen, fwrite, fclose
from libc.math cimport M_PI, abs, atan2, ceil, cos, exp, floor, pow, round, sin, sqrt, tan
from libc.stdio cimport FILE, fclose, fopen, fwrite
from libc.string cimport strcmp
from cython.parallel import prange
from gprMax.config cimport float_or_double
@cython.cdivision(True)
cpdef double[:, ::1] getProjections(
double psi,
double psi,
int[:] m
):
"""Gets the projection vectors to source magnetic fields of plane wave.
"""Gets the projection vectors to source magnetic fields of plane wave.
Args:
psi: float for angle describing polatan value of required phi angle
(which would be approximated to a rational number).
m: int array to store integer mappings, m_x, m_y, m_z which determine
the rational angles, for assignment of the correct element to 3D
FDTD grid from 1D representation, last element stores
m: int array to store integer mappings, m_x, m_y, m_z which determine
the rational angles, for assignment of the correct element to 3D
FDTD grid from 1D representation, last element stores
max(m_x, m_y, m_z).
Returns:
projections: float array to store projections for sourcing magnetic
projections: float array to store projections for sourcing magnetic
field and the sourcing vector.
"""
@@ -81,21 +84,21 @@ cpdef double[:, ::1] getProjections(
@cython.cdivision(True)
cdef int[:] getPhi(
int[:, :] integers,
double required_ratio,
int[:, :] integers,
double required_ratio,
double tolerance
):
"""Gets rational angle approximation to phi within the requested tolerance
level using Farey Fractions to determine a rational number closest to
the real number.
"""Gets rational angle approximation to phi within the requested tolerance
level using Farey Fractions to determine a rational number closest to
the real number.
Args:
integers: int array to determine the value of m_x and m_y.
required_ratio: float of tan value of the required phi angle
required_ratio: float of tan value of the required phi angle
(which would be approximated to a rational number).
tolerance: float for acceptable deviation in the tan value of the
tolerance: float for acceptable deviation in the tan value of the
rational angle from phi.
Returns:
integers: int array of sequence of the two integers [m_x, m_y].
"""
@@ -116,19 +119,19 @@ cdef int[:] getPhi(
@cython.cdivision(True)
cdef inline double getTanValue(
int[:] integers,
int[:] integers,
double[:] dr
):
"""Returns tan value of the angle approximated to theta given three integers.
Args:
integers: int array of three integers for the rational angle
integers: int array of three integers for the rational angle
approximation.
dr: double array containing the separation between grid points along
dr: double array containing the separation between grid points along
the three axes [dx, dy, dz].
Returns:
_tanValue: double of tan value of the rational angle corresponding to
_tanValue: double of tan value of the rational angle corresponding to
integers m_x, m_y, m_z.
"""
@@ -141,55 +144,55 @@ cdef inline double getTanValue(
@cython.cdivision(True)
cdef int[:, :] get_mZ(
int m_x,
int m_y,
double theta,
int m_x,
int m_y,
double theta,
double[:] Delta_r
):
"""Gets arrays to perform a binary search to determine a rational number,
m_z, closest to real number, m_z, to get desired tan Theta value.
"""Gets arrays to perform a binary search to determine a rational number,
m_z, closest to real number, m_z, to get desired tan Theta value.
Args:
m_x and m_y: ints approximating rational angle to tan value of phi.
theta: float of polar angle of incident plane wave (radians) to be
theta: float of polar angle of incident plane wave (radians) to be
approximated to a rational angle.
Delta_r: float array of projections of propagation vector along
Delta_r: float array of projections of propagation vector along
different coordinate axes.
Returns:
_integers: int array of 2D sequence of three integers [m_x, m_y, m_z]
to perform a binary search to determine value of m_z within
_integers: int array of 2D sequence of three integers [m_x, m_y, m_z]
to perform a binary search to determine value of m_z within
given limits.
"""
cdef double m_z = 0
m_z = sqrt((m_x/Delta_r[0])*(m_x/Delta_r[0]) + (m_y/Delta_r[1])*(m_y/Delta_r[1]))/(tan(theta)/Delta_r[2]) #get an estimate of the m_z value
m_z = sqrt((m_x/Delta_r[0])*(m_x/Delta_r[0]) + (m_y/Delta_r[1])*(m_y/Delta_r[1]))/(tan(theta)/Delta_r[2]) #get an estimate of the m_z value
return np.array([[m_x, m_y, floor(m_z)],
[m_x, m_y, round(m_z)],
[m_x, m_y, ceil(m_z)]], dtype=np.int32, order='C') #set up the integer array to search for an appropriate m_z
@cython.cdivision(True)
cdef int[:] getTheta(
int m_x,
int m_y,
double theta,
double Delta_theta,
int m_x,
int m_y,
double theta,
double Delta_theta,
double[:] Delta_r
):
"""Gets rational angle approximation to theta within requested tolerance
level using Binary Search to determine a rational number closest to
real number.
"""Gets rational angle approximation to theta within requested tolerance
level using Binary Search to determine a rational number closest to
real number.
Args:
m_x and m_y: ints approximating rational angle to tan value of phi.
theta: float of polar angle of incident plane wave (radians) to be
m_x and m_y: ints approximating rational angle to tan value of phi.
theta: float of polar angle of incident plane wave (radians) to be
approximated to a rational angle.
Delta_theta: float of permissible error in rational angle approximation
Delta_theta: float of permissible error in rational angle approximation
to theta (radians).
Delta_r: float array of projections of propagation vector along
Delta_r: float array of projections of propagation vector along
different coordinate axes.
Returns:
integers: int array of sequence of three integers [m_x, m_y, m_z].
"""
@@ -197,7 +200,7 @@ cdef int[:] getTheta(
cdef Py_ssize_t i, j = 0
cdef double tan_theta = 0.0
cdef int[:, :] integers = get_mZ(m_x, m_y, theta, Delta_r) #set up the integer array to search for an appropriate m_z
while True: #if tan value of m_z greater than permitted tolerance
while True: #if tan value of m_z greater than permitted tolerance
tan_theta = getTanValue(integers[1, :], Delta_r)
if(abs(tan_theta - tan(theta)) <= Delta_theta / (cos(theta) * cos(theta))):
break
@@ -211,35 +214,35 @@ cdef int[:] getTheta(
integers[2, 2] = integers[1, 2] #decrease m_z, serach in the lower half of the sample space
elif(tan_theta > tan(theta)): #if m_z results in a larger tan value, make the denominator larger
integers[0, 2] = integers[1, 2] #increase m_z, serach in the upper half of the sample space
return integers[1, :]
@cython.cdivision(True)
cpdef int[:, ::1] getIntegerForAngles(
double phi,
double Delta_phi,
double theta,
double Delta_theta,
double phi,
double Delta_phi,
double theta,
double Delta_theta,
double[:] Delta_r
):
"""Gets [m_x, m_y, m_z] to determine rational angles given phi and theta
"""Gets [m_x, m_y, m_z] to determine rational angles given phi and theta
along with the permissible tolerances.
Args:
phi: float of azimuthal angle of incident plane wave (degrees) to be
phi: float of azimuthal angle of incident plane wave (degrees) to be
approximated to a rational angle.
Delta_phi: float of permissible error in rational angle approximation
Delta_phi: float of permissible error in rational angle approximation
to phi (degrees).
theta: float of polar angle of incident plane wave (degrees) to be
theta: float of polar angle of incident plane wave (degrees) to be
approximated to a rational angle.
Delta_theta: float of permissible error in rational angle approximation
Delta_theta: float of permissible error in rational angle approximation
to theta (degrees).
Delta_r: float of projections of propagation vector along different
Delta_r: float of projections of propagation vector along different
coordinate axes.
Returns:
quadrants[0, :]: int array specifying direction of propagation of plane
quadrants[0, :]: int array specifying direction of propagation of plane
wave along the three coordinate axes.
quadrants[1, :]: int array of three integers [m_x, m_y, m_z].
"""
@@ -260,8 +263,8 @@ cpdef int[:, ::1] getIntegerForAngles(
elif(phi>=270 and phi<360):
quadrants[0, 1] = -1
phi = 360-phi
if(0 <= phi < 90): #handle the case of phi=90 degrees separately
required_ratio_phi = tan(M_PI/180*phi) * Delta_r[1] / Delta_r[0] #to avoid division by zero exception
tolerance_phi = M_PI/180*Delta_phi / (cos(M_PI/180*phi)*cos(M_PI/180*phi)) * Delta_r[1] / Delta_r[0] #get the persissible error in tan phi
@@ -270,7 +273,7 @@ cpdef int[:, ::1] getIntegerForAngles(
[ceil(required_ratio_phi), 1]], dtype=np.int32, order='C')
, required_ratio_phi, tolerance_phi) #get the integers [m_x, m_y] for rational angle approximation to phi
else:
m_x = 0
m_x = 0
m_y = 1
if(theta < 90):
m_x, m_y, m_z = getTheta(m_x, m_y, M_PI/180*theta, M_PI/180*Delta_theta, Delta_r) #get the integers [m_x, m_y, m_z] for rational angle approximation to theta
@@ -285,30 +288,30 @@ cpdef int[:, ::1] getIntegerForAngles(
@cython.wraparound(False)
@cython.boundscheck(False)
cdef void applyTFSFMagnetic(
int nthreads,
float_or_double[:, :, ::1] Hx,
int nthreads,
float_or_double[:, :, ::1] Hx,
float_or_double[:, :, ::1] Hy,
float_or_double[:, :, ::1] Hz,
float_or_double[:, ::1] E_fields,
float_or_double[:, :, ::1] Hz,
float_or_double[:, ::1] E_fields,
float_or_double[:] updatecoeffsH,
int[:] m,
int[:] m,
int[:] corners
):
"""Implements total field-scattered field formulation for magnetic field on
"""Implements total field-scattered field formulation for magnetic field on
the edge of the TF/SF region of the TFSF Box.
Args:
nthreads: int of number of threads to parallelize for loops.
Hx, Hy, Hz: double array to store magnetic fields for grid cells over
Hx, Hy, Hz: double array to store magnetic fields for grid cells over
the TFSF box at particular indices.
E_fields: double array to store electric fields of 1D representation of
E_fields: double array to store electric fields of 1D representation of
plane wave in a direction along which the wave propagates.
updatecoeffsH: float of coefficients of fields in TFSF assignment
updatecoeffsH: float of coefficients of fields in TFSF assignment
equation for the magnetic field.
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
representation, last element stores max(m_x, m_y, m_z).
corners: int array of coordinates of corners of TF/SF field boundaries.
corners: int array of coordinates of corners of TF/SF field boundaries.
"""
cdef Py_ssize_t i, j, k = 0
@@ -334,7 +337,7 @@ cdef void applyTFSFMagnetic(
cdef float_or_double coef_H_xz = updatecoeffsH[3]
cdef float_or_double coef_H_yz = updatecoeffsH[3]
cdef float_or_double coef_H_yx = updatecoeffsH[1]
cdef float_or_double coef_H_zx = updatecoeffsH[1]
cdef float_or_double coef_H_zx = updatecoeffsH[1]
cdef float_or_double coef_H_zy = updatecoeffsH[2]
#**** constant x faces -- scattered-field nodes ****
@@ -343,7 +346,7 @@ cdef void applyTFSFMagnetic(
for k in range(z_start, z_stop):
#correct Hy at firstX-1/2 by subtracting Ez_inc
index = m_x * i + m_y * j + m_z * k
Hy[i-1, j, k] -= coef_H_yx * E_z[index]
Hy[i-1, j, k] -= coef_H_yx * E_z[index]
for j in prange(y_start, y_stop, nogil=True, schedule='static', num_threads=nthreads):
for k in range(z_start, z_stop+1):
@@ -356,13 +359,13 @@ cdef void applyTFSFMagnetic(
for k in range(z_start, z_stop):
#correct Hy at lastX+1/2 by adding Ez_inc
index = m_x * i + m_y * j + m_z * k
Hy[i, j, k] += coef_H_yx * E_z[index]
Hy[i, j, k] += coef_H_yx * E_z[index]
for j in prange(y_start, y_stop, nogil=True, schedule='static', num_threads=nthreads):
for k in range(z_start, z_stop+1):
#correct Hz at lastX+1/2 by subtractinging Ey_inc
index = m_x * i + m_y * j + m_z * k
Hz[i, j, k] -= coef_H_zx * E_y[index]
Hz[i, j, k] -= coef_H_zx * E_y[index]
#**** constant y faces -- scattered-field nodes ****
j = y_start
@@ -418,33 +421,33 @@ cdef void applyTFSFMagnetic(
index = m_x * i + m_y * j + m_z * k
Hx[i, j, k] += coef_H_xz * E_y[index]
cdef void applyTFSFElectric(
int nthreads,
float_or_double[:, :, ::1] Ex,
int nthreads,
float_or_double[:, :, ::1] Ex,
float_or_double[:, :, ::1] Ey,
float_or_double[:, :, ::1] Ez,
float_or_double[:, ::1] H_fields,
float_or_double[:, :, ::1] Ez,
float_or_double[:, ::1] H_fields,
float_or_double[:] updatecoeffsE,
int[:] m,
int[:] m,
int[:] corners
):
"""Implements total field-scattered field formulation for electric field on
"""Implements total field-scattered field formulation for electric field on
edge of the TF/SF region of the TFSF Box.
Args:
nthreads: int for number of threads to parallelize the for loops.
Ex, Ey, Ez: double array for magnetic fields for grid cells over TFSF
Ex, Ey, Ez: double array for magnetic fields for grid cells over TFSF
box at particular indices.
H_fields: double array to store electric fields of 1D representation of
H_fields: double array to store electric fields of 1D representation of
plane wave in direction along which wave propagates.
updatecoeffsE: float of coefficients of fields in TFSF assignment
updatecoeffsE: float of coefficients of fields in TFSF assignment
equation for magnetic field.
m: int array of integer mappings, m_x, m_y, m_z which determine rational
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
representation, last element stores max(m_x, m_y, m_z).
corners: int array for coordinates of corners of TF/SF field boundaries.
corners: int array for coordinates of corners of TF/SF field boundaries.
"""
cdef Py_ssize_t i, j, k = 0
@@ -470,7 +473,7 @@ cdef void applyTFSFElectric(
cdef float_or_double coef_E_xz = updatecoeffsE[3]
cdef float_or_double coef_E_yz = updatecoeffsE[3]
cdef float_or_double coef_E_yx = updatecoeffsE[1]
cdef float_or_double coef_E_zx = updatecoeffsE[1]
cdef float_or_double coef_E_zx = updatecoeffsE[1]
cdef float_or_double coef_E_zy = updatecoeffsE[2]
#**** constant x faces -- total-field nodes ****/
@@ -554,36 +557,36 @@ cdef void applyTFSFElectric(
#correct Ex at lastZ face by subtracting Hy_inc
index = m_x * i + m_y * j + m_z * k
Ex[i, j, k] -= coef_E_xz * H_y[index]
cdef void initializeMagneticFields(
int[:] m,
float_or_double[:, ::1] H_fields,
double[:] projections,
float_or_double[:, ::1] H_fields,
double[:] projections,
float_or_double[:, ::1] waveformvalues_wholedt,
bint precompute,
int iteration,
double dt,
double ds,
double c,
double start,
double stop,
double start,
double stop,
double freq,
char* wavetype
):
"""Initialises first few grid points of source waveform.
Args:
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
representation, last element stores max(m_x, m_y, m_z).
H_fields: double array for electric fields of 1D representation of plane
H_fields: double array for electric fields of 1D representation of plane
wave in a direction along which the wave propagates.
projections: float array for projections of magnetic fields along
projections: float array for projections of magnetic fields along
different cartesian axes.
waveformvalues_wholedt: double array stores precomputed waveforms at
waveformvalues_wholedt: double array stores precomputed waveforms at
each timestep to initialize magnetic fields.
precompute: boolean to store whether fields values have been precomputed
precompute: boolean to store whether fields values have been precomputed
or should be computed on the fly.
iterations: int stores number of iterations in the simulation.
dt: float of timestep for the simulation.
@@ -591,7 +594,7 @@ cdef void initializeMagneticFields(
c: float of speed of light in the medium.
start: float of start time at which source is placed in the TFSF grid.
stop: float of stop time at which source is removed from TFSF grid.
freq: float of frequency of introduced wave which determines grid points
freq: float of frequency of introduced wave which determines grid points
per wavelength for wave source.
wavetype: string of type of waveform whose magnitude should be returned.
"""
@@ -613,33 +616,33 @@ cdef void initializeMagneticFields(
H_fields[0, r] = projections[0] * getSource(time_x-start, freq, wavetype, dt)
H_fields[1, r] = projections[1] * getSource(time_y-start, freq, wavetype, dt)
H_fields[2, r] = projections[2] * getSource(time_z-start, freq, wavetype, dt)
@cython.cdivision(True)
cdef void updateMagneticFields(
int n,
float_or_double[:, ::1] H_fields,
float_or_double[:, ::1] E_fields,
float_or_double[:] updatecoeffsH,
int n,
float_or_double[:, ::1] H_fields,
float_or_double[:, ::1] E_fields,
float_or_double[:] updatecoeffsH,
int[:] m
):
"""Updates magnetic fields for next time step using Equation 8 of
"""Updates magnetic fields for next time step using Equation 8 of
DOI: 10.1109/LAWP.2009.2016851.
Args:
n: int for spatial length of DPW array to update each length grid
n: int for spatial length of DPW array to update each length grid
cell.
H_fields: double array of magnetic fields of DPW until temporal
H_fields: double array of magnetic fields of DPW until temporal
index time.
E_fields: double array of electric fields of DPW until temporal
E_fields: double array of electric fields of DPW until temporal
index time.
updatecoeffsH: double array of coefficients of fields in update
updatecoeffsH: double array of coefficients of fields in update
equation for magnetic field.
m: int array of integer mappings, m_x, m_y, m_z which determine
rational angles for assignment of correct element to 3D FDTD
grid from 1D representation, last element stores
m: int array of integer mappings, m_x, m_y, m_z which determine
rational angles for assignment of correct element to 3D FDTD
grid from 1D representation, last element stores
max(m_x, m_y, m_z).
"""
"""
cdef Py_ssize_t j = 0
@@ -659,7 +662,7 @@ cdef void updateMagneticFields(
cdef float_or_double coef_H_yx = updatecoeffsH[1]
cdef float_or_double coef_H_zt = updatecoeffsH[0]
cdef float_or_double coef_H_zx = updatecoeffsH[1]
cdef float_or_double coef_H_zx = updatecoeffsH[1]
cdef float_or_double coef_H_zy = updatecoeffsH[2]
cdef int m_x = m[0]
@@ -670,31 +673,31 @@ cdef void updateMagneticFields(
H_x[j] = coef_H_xt * H_x[j] + coef_H_xz * ( E_y[j+m_z] - E_y[j] ) - coef_H_xy * ( E_z[j+m_y] - E_z[j] ) #equation 8 of Tan, Potter paper
H_y[j] = coef_H_yt * H_y[j] + coef_H_yx * ( E_z[j+m_x] - E_z[j] ) - coef_H_yz * ( E_x[j+m_z] - E_x[j] ) #equation 8 of Tan, Potter paper
H_z[j] = coef_H_zt * H_z[j] + coef_H_zy * ( E_x[j+m_y] - E_x[j] ) - coef_H_zx * ( E_y[j+m_x] - E_y[j] ) #equation 8 of Tan, Potter paper
@cython.cdivision(True)
cdef void updateElectricFields(
int n,
float_or_double[:, ::1] H_fields,
float_or_double[:, ::1] E_fields,
float_or_double[:] updatecoeffsE,
int n,
float_or_double[:, ::1] H_fields,
float_or_double[:, ::1] E_fields,
float_or_double[:] updatecoeffsE,
int[:] m
):
"""Updates electric fields for next time step using Equation 9 of
"""Updates electric fields for next time step using Equation 9 of
DOI: 10.1109/LAWP.2009.2016851.
Args:
n: int for spatial length of DPW array to update each length grid
n: int for spatial length of DPW array to update each length grid
cell.
H_fields: double array of magnetic fields of DPW until temporal
H_fields: double array of magnetic fields of DPW until temporal
index time.
E_fields: double array of electric fields of DPW until temporal
E_fields: double array of electric fields of DPW until temporal
index time.
updatecoeffsE: double array of coefficients of fields in update
updatecoeffsE: double array of coefficients of fields in update
equation for electric field.
m: int array of integer mappings, m_x, m_y, m_z which determine
rational angles for assignment of correct element to 3D FDTD
grid from 1D representation, last element stores
m: int array of integer mappings, m_x, m_y, m_z which determine
rational angles for assignment of correct element to 3D FDTD
grid from 1D representation, last element stores
max(m_x, m_y, m_z).
"""
@@ -716,38 +719,38 @@ cdef void updateElectricFields(
cdef float_or_double coef_E_yx = updatecoeffsE[1]
cdef float_or_double coef_E_zt = updatecoeffsE[0]
cdef float_or_double coef_E_zx = updatecoeffsE[1]
cdef float_or_double coef_E_zx = updatecoeffsE[1]
cdef float_or_double coef_E_zy = updatecoeffsE[2]
cdef int m_x = m[0]
cdef int m_y = m[1]
cdef int m_z = m[2]
for j in range(m[3], n-m[3]): #loop to update the electric field at each spatial index
for j in range(m[3], n-m[3]): #loop to update the electric field at each spatial index
E_x[j] = coef_E_xt * E_x[j] + coef_E_xz * ( H_z[j] - H_z[j-m_y] ) - coef_E_xy * ( H_y[j] - H_y[j-m_z] ) #equation 9 of Tan, Potter paper
E_y[j] = coef_E_yt * E_y[j] + coef_E_yx * ( H_x[j] - H_x[j-m_z] ) - coef_E_yz * ( H_z[j] - H_z[j-m_x] ) #equation 9 of Tan, Potter paper
E_z[j] = coef_E_zt * E_z[j] + coef_E_zy * ( H_y[j] - H_y[j-m_x] ) - coef_E_zx * ( H_x[j] - H_x[j-m_y] ) #equation 9 of Tan, Potter paper
@cython.cdivision(True)
cpdef double getSource(
double time,
double freq,
char* wavetype,
double time,
double freq,
char* wavetype,
double dt
):
"""Gets magnitude of source field in direction perpendicular to propagation
"""Gets magnitude of source field in direction perpendicular to propagation
of plane wave.
Args:
time: float of time at which magnitude of source is calculated.
freq: float of frequency of introduced wave which determines grid points
freq: float of frequency of introduced wave which determines grid points
per wavelength for wave source.
wavetype: string of type of waveform whose magnitude should be returned.
dt: double of time upto which wave should exist in a impulse delta pulse.
Returns:
sourceMagnitude: double of magnitude of source for requested indices at
sourceMagnitude: double of magnitude of source for requested indices at
current time.
"""
@@ -758,7 +761,7 @@ cpdef double getSource(
elif (strcmp(wavetype, "gaussiandot") == 0 or strcmp(wavetype, "gaussianprime") == 0):
return -4.0 * M_PI * M_PI * freq * (time * freq - 1.0
) * exp(-2.0 * (M_PI * (time * freq - 1.0)) * (M_PI * (time * freq - 1.0)))
elif (strcmp(wavetype, "gaussiandotnorm") == 0):
return -2.0 * M_PI * (time * freq - 1.0
) * exp(-2.0 * (M_PI * (time * freq - 1.0)) * (M_PI * (time * freq - 1.0))) * exp(0.5)
@@ -766,15 +769,15 @@ cpdef double getSource(
elif (strcmp(wavetype, "gaussiandotdot") == 0 or strcmp(wavetype, "gaussiandoubleprime") == 0):
return (2.0 * M_PI * freq) * (2.0 * M_PI * freq) * (2.0 * (M_PI * (time * freq - 1.0)) * (M_PI * (time * freq - 1.0)) - 1.0
) * exp(-2.0 * (M_PI * (time * freq - 1.0)) * (M_PI * (time * freq - 1.0)))
elif (strcmp(wavetype, "gaussiandotdotnorm") == 0):
return (2.0 * (M_PI *(time * freq - 1.0)) * (M_PI * (time * freq - 1.0)) - 1.0
) * exp(-2.0 * (M_PI * (time * freq - 1.0)) * (M_PI * (time * freq - 1.0)))
elif (strcmp(wavetype, "ricker") == 0):
return (1.0 - 2.0 * (M_PI *(time * freq - 1.0)) * (M_PI * (time * freq - 1.0))
) * exp(-2.0 * (M_PI * (time * freq - 1.0)) * (M_PI * (time * freq - 1.0))) # define a Ricker wave source
elif (strcmp(wavetype, "sine") == 0):
if (time * freq <= 1):
return sin(2.0 * M_PI * freq * time)
@@ -793,41 +796,41 @@ cpdef double getSource(
@cython.cdivision(True)
cpdef void calculate1DWaveformValues(
float_or_double[:, :, ::1] waveformvalues_wholedt,
int iterations,
int[:] m,
double dt,
double ds,
float_or_double[:, :, ::1] waveformvalues_wholedt,
int iterations,
int[:] m,
double dt,
double ds,
double c,
double start,
double stop,
double freq,
double start,
double stop,
double freq,
char* wavetype
):
"""Precomputes source waveform values so that initialization is faster,
"""Precomputes source waveform values so that initialization is faster,
if requested.
Args:
waveformvalues_wholedt: double array of precomputed waveforms at each
waveformvalues_wholedt: double array of precomputed waveforms at each
timestep to initialize magnetic fields.
iterations: int of number of iterations in simulation.
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
m: int array of integer mappings, m_x, m_y, m_z which determine rational
angles for assignment of correct element to 3D FDTD grid from 1D
representation, last element stores max(m_x, m_y, m_z).
dt: float of timestep for the simulation.
ds: float of projection vector for sourcing the plane wave.
c: float of speed of light in the medium.
start: float of start time at which source is placed in the TFSF grid.
stop: float of stop time at which source is removed from TFSF grid.
freq: float of frequency of introduced wave which determines grid points
freq: float of frequency of introduced wave which determines grid points
per wavelength for wave source.
wavetype: string of type of waveform whose magnitude should be returned.
"""
cdef double time_x, time_y, time_z = 0.0
cdef Py_ssize_t iteration, r = 0
for iteration in range(iterations):
for iteration in range(iterations):
for r in range(m[3]):
time_x = dt * iteration - (r+ (m[1]+m[2])*0.5) * ds/c
time_y = dt * iteration - (r+ (m[2]+m[0])*0.5) * ds/c
@@ -839,11 +842,11 @@ cpdef void calculate1DWaveformValues(
waveformvalues_wholedt[iteration, 2, r] = getSource(time_z-start, freq, wavetype, dt)
cpdef void updatePlaneWave(
int n,
cpdef void updatePlaneWave(
int n,
int nthreads,
float_or_double[:, ::1] H_fields,
float_or_double[:, ::1] E_fields,
float_or_double[:, ::1] H_fields,
float_or_double[:, ::1] E_fields,
float_or_double[:] updatecoeffsE,
float_or_double[:] updatecoeffsH,
float_or_double[:, :, ::1] Ex,
@@ -851,7 +854,7 @@ cpdef void updatePlaneWave(
float_or_double[:, :, ::1] Ez,
float_or_double[:, :, ::1] Hx,
float_or_double[:, :, ::1] Hy,
float_or_double[:, :, ::1] Hz,
float_or_double[:, :, ::1] Hz,
double[:] projections,
float_or_double[:, ::1] waveformvalues_wholedt,
int[:] m,
@@ -861,8 +864,8 @@ cpdef void updatePlaneWave(
double dt,
double ds,
double c,
double start,
double stop,
double start,
double stop,
double freq,
char* wavetype
):
@@ -876,15 +879,14 @@ cpdef void updatePlaneWave(
@cython.cdivision(True)
cdef void takeSnapshot3D(double[:, :, ::1] field, char* filename):
"""Writes fields of plane wave simulation at a particular time step.
Args:
fields: double array of fields for grid cells over TFSF box at
fields: double array of fields for grid cells over TFSF box at
particular indices of TFSF box at particular time step.
filename: string of file location where fields are to be written.
"""
cdef FILE *fptr = fopen(filename, "wb")
fwrite(&field[0, 0, 0], sizeof(double), field.size, fptr)
fclose(fptr)

查看文件

@@ -49,7 +49,7 @@ cpdef pml_average_er_mr(
cdef Py_ssize_t m, n
cdef int numID
# Sum and average of relative permittivities and permeabilities in PML slab
cdef float sumer, summr, averageer, averagemr
cdef double sumer, summr, averageer, averagemr
for m in prange(n1, nogil=True, schedule='static', num_threads=nthreads):
for n in range(n2):
@@ -60,3 +60,39 @@ cpdef pml_average_er_mr(
averagemr = summr / (n1 * n2)
return averageer, averagemr
cpdef pml_sum_er_mr(
int n1,
int n2,
int nthreads,
np.uint32_t[:, :] solid,
float_or_double[::1] ers,
float_or_double[::1] mrs
):
"""Calculates average permittivity and permeability in PML slab (based on
underlying material er and mr from solid array). Used to build PML.
Args:
n1, n2: ints for PML size in cells perpendicular to thickness direction.
nthreads: int for number of threads to use.
solid: memoryviews to access solid array.
ers, mrs: memoryviews to access arrays containing permittivity and
permeability.
Returns:
averageer, averagemr: floats for average permittivity and permeability
in PML slab.
"""
cdef Py_ssize_t m, n
cdef int numID
# Sum and average of relative permittivities and permeabilities in PML slab
cdef double sumer, summr
for m in prange(n1, nogil=True, schedule='static', num_threads=nthreads):
for n in range(n2):
numID = solid[m ,n]
sumer += ers[numID]
summr += mrs[numID]
return sumer, summr

查看文件

@@ -53,17 +53,13 @@ cpdef void create_electric_average(
G: FDTDGrid class describing a grid in a model.
"""
# Make an ID composed of the names of the four materials that will be averaged
requiredID = (G.materials[numID1].ID + '+' + G.materials[numID2].ID + '+' +
G.materials[numID3].ID + '+' + G.materials[numID4].ID)
# Make an ID composed of the names of the four materials that will
# be averaged. Sort the names to ensure the same four component
# materials always form the same ID.
requiredID = Material.create_compound_id(G.materials[numID1], G.materials[numID2], G.materials[numID3], G.materials[numID4])
# Check if this material already exists
tmp = requiredID.split('+')
material = [x for x in G.materials if
x.ID.count(tmp[0]) == requiredID.count(tmp[0]) and
x.ID.count(tmp[1]) == requiredID.count(tmp[1]) and
x.ID.count(tmp[2]) == requiredID.count(tmp[2]) and
x.ID.count(tmp[3]) == requiredID.count(tmp[3])]
material = [x for x in G.materials if x.ID == requiredID]
if material:
G.ID[componentID, i, j, k] = material[0].numID
@@ -108,14 +104,10 @@ cpdef void create_magnetic_average(
"""
# Make an ID composed of the names of the two materials that will be averaged
requiredID = G.materials[numID1].ID + '+' + G.materials[numID2].ID
requiredID = Material.create_compound_id(G.materials[numID1], G.materials[numID2])
# Check if this material already exists
tmp = requiredID.split('+')
material = [x for x in G.materials if
(x.ID.count(tmp[0]) == requiredID.count(tmp[0]) and
x.ID.count(tmp[1]) == requiredID.count(tmp[1])) or
(x.ID.count(tmp[0]) % 2 == 0 and x.ID.count(tmp[1]) % 2 == 0)]
material = [x for x in G.materials if x.ID == requiredID]
if material:
G.ID[componentID, i, j, k] = material[0].numID

查看文件

@@ -17,15 +17,18 @@
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
import logging
from pathlib import Path
import h5py
from gprMax.grid.fdtd_grid import FDTDGrid
from ._version import __version__
logger = logging.getLogger(__name__)
def store_outputs(G):
def store_outputs(G: FDTDGrid, iteration: int):
"""Stores field component values for every receiver and transmission line.
Args:
@@ -33,7 +36,6 @@ def store_outputs(G):
"""
# Assign iteration and fields to local variables
iteration = G.iteration
Ex, Ey, Ez, Hx, Hy, Hz = G.Ex, G.Ey, G.Ez, G.Hx, G.Hy, G.Hz
for rx in G.rxs:
@@ -45,39 +47,41 @@ def store_outputs(G):
# Store current component
else:
func = globals()[output]
rx.outputs[output][iteration] = func(
rx.xcoord, rx.ycoord, rx.zcoord, Hx, Hy, Hz, G
)
rx.outputs[output][iteration] = func(rx.xcoord, rx.ycoord, rx.zcoord, Hx, Hy, Hz, G)
for tl in G.transmissionlines:
tl.Vtotal[iteration] = tl.voltage[tl.antpos]
tl.Itotal[iteration] = tl.current[tl.antpos]
def write_hdf5_outputfile(outputfile, G):
# TODO: Add type information for grid (without a circular dependency)
def write_hdf5_outputfile(outputfile: Path, title: str, model):
"""Writes an output file in HDF5 (.h5) format.
Args:
outputfile: string of the name of the output file.
G: FDTDGrid class describing a grid in a model.
"""
# Create output file and write top-level meta data, meta data for main grid,
# and any outputs in the main grid
f = h5py.File(outputfile, "w")
f.attrs["gprMax"] = __version__
f.attrs["Title"] = G.title
write_hd5_data(f, G)
f.attrs["Title"] = title
f.attrs["Iterations"] = model.iterations
f.attrs["srcsteps"] = model.srcsteps
f.attrs["rxsteps"] = model.rxsteps
write_hd5_data(f, model.G)
# Write meta data and data for any subgrids
sg_rxs = [True for sg in G.subgrids if sg.rxs]
sg_tls = [True for sg in G.subgrids if sg.transmissionlines]
sg_rxs = [True for sg in model.subgrids if sg.rxs]
sg_tls = [True for sg in model.subgrids if sg.transmissionlines]
if sg_rxs or sg_tls:
for sg in G.subgrids:
for sg in model.subgrids:
grp = f.create_group(f"/subgrids/{sg.name}")
write_hd5_data(grp, sg, is_subgrid=True)
logger.basic(f"Written output file: {outputfile.name}")
logger.basic("")
logger.basic(f"Written output file: {outputfile.name}\n")
def write_hd5_data(basegrp, grid, is_subgrid=False):
@@ -90,23 +94,20 @@ def write_hd5_data(basegrp, grid, is_subgrid=False):
"""
# Write meta data for grid
basegrp.attrs["Iterations"] = grid.iterations
basegrp.attrs["nx_ny_nz"] = (grid.nx, grid.ny, grid.nz)
basegrp.attrs["dx_dy_dz"] = (grid.dx, grid.dy, grid.dz)
basegrp.attrs["dt"] = grid.dt
nsrc = len(
grid.voltagesources
+ grid.hertziandipoles
+ grid.magneticdipoles
+ grid.transmissionlines
grid.voltagesources + grid.hertziandipoles + grid.magneticdipoles + grid.transmissionlines
)
basegrp.attrs["nsrc"] = nsrc
basegrp.attrs["nrx"] = len(grid.rxs)
basegrp.attrs["srcsteps"] = grid.srcsteps
basegrp.attrs["rxsteps"] = grid.rxsteps
if is_subgrid:
# Write additional meta data about subgrid
basegrp.attrs["Iterations"] = grid.iterations
basegrp.attrs["srcsteps"] = grid.srcsteps
basegrp.attrs["rxsteps"] = grid.rxsteps
basegrp.attrs["is_os_sep"] = grid.is_os_sep
basegrp.attrs["pml_separation"] = grid.pml_separation
basegrp.attrs["subgrid_pml_thickness"] = grid.pmls["thickness"]["x0"]
@@ -143,6 +144,10 @@ def write_hd5_data(basegrp, grid, is_subgrid=False):
basegrp["tls/tl" + str(tlindex + 1) + "/Vtotal"] = tl.Vtotal
basegrp["tls/tl" + str(tlindex + 1) + "/Itotal"] = tl.Itotal
# Ensure the order of receivers is always consistent (Needed for
# consistancy when using MPI with multiple receivers)
grid.rxs.sort(key=lambda rx: rx.ID)
# Create group, add positional data and write field component arrays for receivers
for rxindex, rx in enumerate(grid.rxs):
grp = basegrp.create_group("rxs/rx" + str(rxindex + 1))
@@ -170,9 +175,7 @@ def Ix(x, y, z, Hx, Hy, Hz, G):
if y == 0 or z == 0:
Ix = 0
else:
Ix = G.dy * (Hy[x, y, z - 1] - Hy[x, y, z]) + G.dz * (
Hz[x, y, z] - Hz[x, y - 1, z]
)
Ix = G.dy * (Hy[x, y, z - 1] - Hy[x, y, z]) + G.dz * (Hz[x, y, z] - Hz[x, y - 1, z])
return Ix
@@ -189,9 +192,7 @@ def Iy(x, y, z, Hx, Hy, Hz, G):
if x == 0 or z == 0:
Iy = 0
else:
Iy = G.dx * (Hx[x, y, z] - Hx[x, y, z - 1]) + G.dz * (
Hz[x - 1, y, z] - Hz[x, y, z]
)
Iy = G.dx * (Hx[x, y, z] - Hx[x, y, z - 1]) + G.dz * (Hz[x - 1, y, z] - Hz[x, y, z])
return Iy
@@ -208,8 +209,6 @@ def Iz(x, y, z, Hx, Hy, Hz, G):
if x == 0 or y == 0:
Iz = 0
else:
Iz = G.dx * (Hx[x, y - 1, z] - Hx[x, y, z]) + G.dy * (
Hy[x, y, z] - Hy[x - 1, y, z]
)
Iz = G.dx * (Hx[x, y - 1, z] - Hx[x, y, z]) + G.dy * (Hy[x, y, z] - Hy[x - 1, y, z])
return Iz

查看文件

@@ -56,7 +56,9 @@ class FractalSurface:
self.nz = zf - zs
self.dtype = np.dtype(np.complex128)
self.seed = seed
self.dimension = dimension # Fractal dimension from: http://dx.doi.org/10.1017/CBO9781139174695
self.dimension = (
dimension # Fractal dimension from: http://dx.doi.org/10.1017/CBO9781139174695
)
self.weighting = np.array([1, 1], dtype=np.float64)
self.fractalrange = (0, 0)
self.filldepth = 0
@@ -124,11 +126,9 @@ class FractalSurface:
fractalmax = np.amax(self.fractalsurface)
fractalrange = fractalmax - fractalmin
self.fractalsurface = (
self.fractalsurface
* ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange)
self.fractalsurface * ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange)
+ self.fractalrange[0]
- ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange)
* fractalmin
- ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange) * fractalmin
)
@@ -164,7 +164,9 @@ class FractalVolume:
self.averaging = False
self.dtype = np.dtype(np.complex128)
self.seed = seed
self.dimension = dimension # Fractal dimension from: http://dx.doi.org/10.1017/CBO9781139174695
self.dimension = (
dimension # Fractal dimension from: http://dx.doi.org/10.1017/CBO9781139174695
)
self.weighting = np.array([1, 1, 1], dtype=np.float64)
self.nbins = 0
self.fractalsurfaces = []
@@ -174,19 +176,13 @@ class FractalVolume:
# Scale filter according to size of fractal volume
if self.nx == 1:
filterscaling = np.amin(np.array([self.ny, self.nz])) / np.array(
[self.ny, self.nz]
)
filterscaling = np.amin(np.array([self.ny, self.nz])) / np.array([self.ny, self.nz])
filterscaling = np.insert(filterscaling, 0, 1)
elif self.ny == 1:
filterscaling = np.amin(np.array([self.nx, self.nz])) / np.array(
[self.nx, self.nz]
)
filterscaling = np.amin(np.array([self.nx, self.nz])) / np.array([self.nx, self.nz])
filterscaling = np.insert(filterscaling, 1, 1)
elif self.nz == 1:
filterscaling = np.amin(np.array([self.nx, self.ny])) / np.array(
[self.nx, self.ny]
)
filterscaling = np.amin(np.array([self.nx, self.ny])) / np.array([self.nx, self.ny])
filterscaling = np.insert(filterscaling, 2, 1)
else:
filterscaling = np.amin(np.array([self.nx, self.ny, self.nz])) / np.array(
@@ -241,9 +237,7 @@ class FractalVolume:
)
# Bin fractal values
bins = np.linspace(
np.amin(self.fractalvolume), np.amax(self.fractalvolume), self.nbins
)
bins = np.linspace(np.amin(self.fractalvolume), np.amax(self.fractalvolume), self.nbins)
for j in range(self.ny):
for k in range(self.nz):
self.fractalvolume[:, j, k] = np.digitize(

查看文件

@@ -16,28 +16,38 @@
# You should have received a copy of the GNU General Public License
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
import json
import logging
import sys
from abc import ABC, abstractmethod
from io import TextIOWrapper
from itertools import chain
from pathlib import Path
from typing import Dict, List, Sequence, Tuple, Union
import h5py
import numpy as np
from evtk.hl import imageToVTK, linesToVTK
from evtk.vtk import VtkGroup, VtkImageData, VtkUnstructuredGrid
import numpy.typing as npt
from tqdm import tqdm
import gprMax.config as config
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.materials import Material
from gprMax.receivers import Rx
from gprMax.sources import Source
from gprMax.vtkhdf_filehandlers.vtk_image_data import VtkImageData
from gprMax.vtkhdf_filehandlers.vtk_unstructured_grid import VtkUnstructuredGrid
from gprMax.vtkhdf_filehandlers.vtkhdf import VtkCellType, VtkHdfFile
from ._version import __version__
from .cython.geometry_outputs import write_lines
from .cython.geometry_outputs import get_line_properties
from .subgrids.grid import SubGridBaseGrid
from .utilities.utilities import get_terminal_width
logger = logging.getLogger(__name__)
def save_geometry_views(gvs):
def save_geometry_views(gvs: "List[GeometryView]"):
"""Creates and saves geometryviews.
Args:
@@ -47,28 +57,42 @@ def save_geometry_views(gvs):
logger.info("")
for i, gv in enumerate(gvs):
gv.set_filename()
vtk_data = gv.prep_vtk()
gv.prep_vtk()
pbar = tqdm(
total=gv.nbytes,
unit="byte",
unit_scale=True,
desc=f"Writing geometry view file {i + 1}/{len(gvs)}, "
f"{gv.filename.name}{gv.vtkfiletype.ext}",
desc=f"Writing geometry view file {i + 1}/{len(gvs)}, {gv.filename.name}",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
gv.write_vtk(vtk_data)
gv.write_vtk()
pbar.update(gv.nbytes)
pbar.close()
logger.info("")
class GeometryView:
class GeometryView(ABC):
"""Base class for Geometry Views."""
def __init__(self, xs, ys, zs, xf, yf, zf, dx, dy, dz, filename, grid):
FILE_EXTENSION = ".vtkhdf"
def __init__(
self,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
dx: int,
dy: int,
dz: int,
filename: str,
grid: FDTDGrid,
):
"""
Args:
xs, xf, ys, yf, zs, zf: ints for extent of geometry view in cells.
@@ -77,44 +101,89 @@ class GeometryView:
grid: FDTDGrid class describing a grid in a model.
"""
self.xs = xs
self.ys = ys
self.zs = zs
self.xf = xf
self.yf = yf
self.zf = zf
self.nx = self.xf - self.xs
self.ny = self.yf - self.ys
self.nz = self.zf - self.zs
self.dx = dx
self.dy = dy
self.dz = dz
self.filename = filename
self.start = np.array([xs, ys, zs], dtype=np.int32)
self.stop = np.array([xf, yf, zf], dtype=np.int32)
self.step = np.array([dx, dy, dz], dtype=np.int32)
self.size = (self.stop - self.start) // self.step
self.filename = Path(filename)
self.filenamebase = filename
self.grid = grid
self.nbytes = None
self.material_data = None
self.materials = None
# Properties for backwards compatibility
@property
def xs(self) -> int:
return self.start[0]
@property
def ys(self) -> int:
return self.start[1]
@property
def zs(self) -> int:
return self.start[2]
@property
def xf(self) -> int:
return self.stop[0]
@property
def yf(self) -> int:
return self.stop[1]
@property
def zf(self) -> int:
return self.stop[2]
@property
def dx(self) -> int:
return self.step[0]
@property
def dy(self) -> int:
return self.step[1]
@property
def dz(self) -> int:
return self.step[2]
@property
def nx(self) -> int:
return self.size[0]
@property
def ny(self) -> int:
return self.size[1]
@property
def nz(self) -> int:
return self.size[2]
def set_filename(self):
"""Constructs filename from user-supplied name and model run number."""
"""Construct filename from user-supplied name and model number."""
parts = config.get_model_config().output_file_path.parts
self.filename = Path(
*parts[:-1], self.filenamebase + config.get_model_config().appendmodelnumber
)
).with_suffix(self.FILE_EXTENSION)
@abstractmethod
def prep_vtk(self):
pass
@abstractmethod
def write_vtk(self):
pass
class GeometryViewLines(GeometryView):
"""Unstructured grid (.vtu) for a per-cell-edge geometry view."""
def __init__(self, *args):
super().__init__(*args)
self.vtkfiletype = VtkUnstructuredGrid
"""Unstructured grid for a per-cell-edge geometry view."""
def prep_vtk(self):
"""Prepares data for writing to VTK file.
Returns:
vtk_data: dict of coordinates, data, and comments for VTK file.
"""
"""Prepares data for writing to VTKHDF file."""
# Sample ID array according to geometry view spatial discretisation
# Only create a new array if subsampling is required
@@ -123,7 +192,7 @@ class GeometryViewLines(GeometryView):
or (self.dx, self.dy, self.dz) != (1, 1, 1)
or (self.xs, self.ys, self.zs) != (0, 0, 0)
):
# Require contiguous for evtk library
# Require contiguous array
ID = np.ascontiguousarray(
self.grid.ID[
:,
@@ -136,81 +205,66 @@ class GeometryViewLines(GeometryView):
# This array is contiguous by design
ID = self.grid.ID
x, y, z, lines = write_lines(
(self.xs * self.grid.dx),
(self.ys * self.grid.dy),
(self.zs * self.grid.dz),
self.nx,
self.ny,
self.nz,
(self.dx * self.grid.dx),
(self.dy * self.grid.dy),
(self.dz * self.grid.dz),
ID,
)
x = np.arange(self.nx + 1, dtype=np.float64)
y = np.arange(self.ny + 1, dtype=np.float64)
z = np.arange(self.nz + 1, dtype=np.float64)
coords = np.meshgrid(x, y, z, indexing="ij")
self.points = np.vstack(list(map(np.ravel, coords))).T
self.points += self.start
self.points *= self.step * self.grid.dl
# Add offset to subgrid geometry to correctly locate within main grid
if isinstance(self.grid, SubGridBaseGrid):
x += self.grid.i0 * self.grid.dx * self.grid.ratio
y += self.grid.j0 * self.grid.dy * self.grid.ratio
z += self.grid.k0 * self.grid.dz * self.grid.ratio
offset = [self.grid.i0, self.grid.j0, self.grid.k0]
self.points += offset * self.grid.dl * self.grid.ratio
# Each point is the 'source' for 3 lines.
# NB: Excluding points at the far edge of the geometry as those
# are the 'source' for no lines
n_lines = 3 * self.nx * self.ny * self.nz
self.cell_types = np.full(n_lines, VtkCellType.LINE)
self.cell_offsets = np.arange(0, 2 * n_lines + 2, 2, dtype=np.int32)
self.connectivity, self.material_data = get_line_properties(
n_lines, self.nx, self.ny, self.nz, ID
)
assert isinstance(self.connectivity, np.ndarray)
assert isinstance(self.material_data, np.ndarray)
# Write information about any PMLs, sources, receivers
comments = Comments(self.grid, self)
comments.averaged_materials = True
comments.materials_only = True
info = comments.get_gprmax_info()
comments = json.dumps(info)
self.metadata = Metadata(self.grid, self, averaged_materials=True, materials_only=True)
# Number of bytes of data to be written to file
offsets_size = np.arange(start=2, step=2, stop=len(x) + 1, dtype="int32").nbytes
connect_size = len(x) * np.dtype("int32").itemsize
cell_type_size = len(x) * np.dtype("uint8").itemsize
self.nbytes = (
x.nbytes
+ y.nbytes
+ z.nbytes
+ lines.nbytes
+ offsets_size
+ connect_size
+ cell_type_size
self.points.nbytes
+ self.cell_types.nbytes
+ self.connectivity.nbytes
+ self.cell_offsets.nbytes
+ self.material_data.nbytes
)
vtk_data = {"x": x, "y": y, "z": z, "data": lines, "comments": comments}
def write_vtk(self):
"""Writes geometry information to a VTKHDF file."""
return vtk_data
def write_vtk(self, vtk_data):
"""Writes geometry information to a VTK file.
Args:
vtk_data: dict of coordinates, data, and comments for VTK file.
"""
# Write the VTK file .vtu
linesToVTK(
str(self.filename),
vtk_data["x"],
vtk_data["y"],
vtk_data["z"],
cellData={"Material": vtk_data["data"]},
comments=[vtk_data["comments"]],
)
# Write the VTK file
with VtkUnstructuredGrid(
self.filename,
self.points,
self.cell_types,
self.connectivity,
self.cell_offsets,
) as f:
f.add_cell_data("Material", self.material_data)
self.metadata.write_to_vtkhdf(f)
class GeometryViewVoxels(GeometryView):
"""Imagedata (.vti) for a per-cell geometry view."""
def __init__(self, *args):
super().__init__(*args)
self.vtkfiletype = VtkImageData
"""Image data for a per-cell geometry view."""
def prep_vtk(self):
"""Prepares data for writing to VTK file.
Returns:
vtk_data: dict of data and comments for VTK file.
"""
"""Prepares data for writing to VTKHDF file."""
# Sample solid array according to geometry view spatial discretisation
# Only create a new array if subsampling is required
@@ -219,8 +273,8 @@ class GeometryViewVoxels(GeometryView):
or (self.dx, self.dy, self.dz) != (1, 1, 1)
or (self.xs, self.ys, self.zs) != (0, 0, 0)
):
# Require contiguous for evtk library
solid = np.ascontiguousarray(
# Require contiguous array
self.material_data = np.ascontiguousarray(
self.grid.solid[
self.xs : self.xf : self.dx,
self.ys : self.yf : self.dy,
@@ -229,82 +283,64 @@ class GeometryViewVoxels(GeometryView):
)
else:
# This array is contiguous by design
solid = self.grid.solid
# Write information about any PMLs, sources, receivers
comments = Comments(self.grid, self)
info = comments.get_gprmax_info()
comments = json.dumps(info)
self.nbytes = solid.nbytes
vtk_data = {"data": solid, "comments": comments}
return vtk_data
def write_vtk(self, vtk_data):
"""Writes geometry information to a VTK file.
Args:
vtk_data: dict of data and comments for VTK file.
"""
self.material_data = self.grid.solid
if isinstance(self.grid, SubGridBaseGrid):
origin = (
(self.grid.i0 * self.grid.dx * self.grid.ratio),
(self.grid.j0 * self.grid.dy * self.grid.ratio),
(self.grid.k0 * self.grid.dz * self.grid.ratio),
self.origin = np.array(
[
(self.grid.i0 * self.grid.dx * self.grid.ratio),
(self.grid.j0 * self.grid.dy * self.grid.ratio),
(self.grid.k0 * self.grid.dz * self.grid.ratio),
]
)
else:
origin = (
(self.xs * self.grid.dx),
(self.ys * self.grid.dy),
(self.zs * self.grid.dz),
)
self.origin = self.start * self.grid.dl
# Write the VTK file .vti
imageToVTK(
str(self.filename),
origin=origin,
spacing=(
(self.dx * self.grid.dx),
(self.dy * self.grid.dy),
(self.dz * self.grid.dz),
),
cellData={"Material": vtk_data["data"]},
comments=[vtk_data["comments"]],
)
self.spacing = self.step * self.grid.dl
# Write information about any PMLs, sources, receivers
self.metadata = Metadata(self.grid, self)
self.nbytes = self.material_data.nbytes
def write_vtk(self):
"""Writes geometry information to a VTKHDF file."""
with VtkImageData(self.filename, self.size, self.origin, self.spacing) as f:
f.add_cell_data("Material", self.material_data)
self.metadata.write_to_vtkhdf(f)
class Comments:
class Metadata:
"""Comments can be strings included in the header of XML VTK file, and are
used to hold extra (gprMax) information about the VTK data.
"""
def __init__(self, grid, gv):
def __init__(
self,
grid: FDTDGrid,
gv: GeometryView,
averaged_materials: bool = False,
materials_only: bool = False,
):
self.grid = grid
self.gv = gv
self.averaged_materials = False
self.materials_only = False
self.averaged_materials = averaged_materials
self.materials_only = materials_only
def get_gprmax_info(self):
"""Returns gprMax specific information relating material, source,
and receiver names to numeric identifiers.
"""
self.gprmax_version = __version__
self.dx_dy_dz = self.grid.dl
self.nx_ny_nz = np.array([self.grid.nx, self.grid.ny, self.grid.nz], dtype=np.int32)
# Comments for Paraview macro
comments = {
"gprMax_version": __version__,
"dx_dy_dz": self.dx_dy_dz_comment(),
"nx_ny_nz": self.nx_ny_nz_comment(),
"Materials": self.materials_comment(),
} # Write the name and numeric ID for each material
self.materials = self.materials_comment()
# Write information on PMLs, sources, and receivers
if not self.materials_only:
# Information on PML thickness
if self.grid.pmls["slabs"]:
comments["PMLthickness"] = self.pml_gv_comment()
self.pml_thickness = self.pml_gv_comment()
else:
self.pml_thickness = None
srcs = (
self.grid.hertziandipoles
+ self.grid.magneticdipoles
@@ -312,67 +348,91 @@ class Comments:
+ self.grid.transmissionlines
)
if srcs:
comments["Sources"] = self.srcs_rx_gv_comment(srcs)
self.source_ids, self.source_positions = self.srcs_rx_gv_comment(srcs)
else:
self.source_ids = None
self.source_positions = None
if self.grid.rxs:
comments["Receivers"] = self.srcs_rx_gv_comment(self.grid.rxs)
self.receiver_ids, self.receiver_positions = self.srcs_rx_gv_comment(self.grid.rxs)
else:
self.receiver_ids = None
self.receiver_positions = None
return comments
def write_to_vtkhdf(self, file_handler: VtkHdfFile):
file_handler.add_field_data(
"gprMax_version",
self.gprmax_version,
dtype=h5py.string_dtype(),
)
file_handler.add_field_data("dx_dy_dz", self.dx_dy_dz)
file_handler.add_field_data("nx_ny_nz", self.nx_ny_nz)
def pml_gv_comment(self):
file_handler.add_field_data(
"material_ids",
self.materials,
dtype=h5py.string_dtype(),
)
if not self.materials_only:
if self.pml_thickness is not None:
file_handler.add_field_data("pml_thickness", self.pml_thickness)
if self.source_ids is not None and self.source_positions is not None:
file_handler.add_field_data(
"source_ids", self.source_ids, dtype=h5py.string_dtype()
)
file_handler.add_field_data("sources", self.source_positions)
if self.receiver_ids is not None and self.receiver_positions is not None:
file_handler.add_field_data(
"receiver_ids", self.receiver_ids, dtype=h5py.string_dtype()
)
file_handler.add_field_data("receivers", self.receiver_positions)
def pml_gv_comment(self) -> List[int]:
grid = self.grid
# Only render PMLs if they are in the geometry view
pmlstorender = dict.fromkeys(grid.pmls["thickness"], 0)
thickness: Dict[str, int] = grid.pmls["thickness"]
gv_pml_depth = dict.fromkeys(thickness, 0)
# Casting to int required as json does not handle numpy types
if grid.pmls["thickness"]["x0"] - self.gv.xs > 0:
pmlstorender["x0"] = int(grid.pmls["thickness"]["x0"] - self.gv.xs)
if grid.pmls["thickness"]["y0"] - self.gv.ys > 0:
pmlstorender["y0"] = int(grid.pmls["thickness"]["y0"] - self.gv.ys)
if grid.pmls["thickness"]["z0"] - self.gv.zs > 0:
pmlstorender["z0"] = int(grid.pmls["thickness"]["z0"] - self.gv.zs)
if self.gv.xf > grid.nx - grid.pmls["thickness"]["xmax"]:
pmlstorender["xmax"] = int(
self.gv.xf - (grid.nx - grid.pmls["thickness"]["xmax"])
)
if self.gv.yf > grid.ny - grid.pmls["thickness"]["ymax"]:
pmlstorender["ymax"] = int(
self.gv.yf - (grid.ny - grid.pmls["thickness"]["ymax"])
)
if self.gv.zf > grid.nz - grid.pmls["thickness"]["zmax"]:
pmlstorender["zmax"] = int(
self.gv.zf - (grid.nz - grid.pmls["thickness"]["zmax"])
)
if self.gv.xs < thickness["x0"]:
gv_pml_depth["x0"] = thickness["x0"] - self.gv.xs
if self.gv.ys < thickness["y0"]:
gv_pml_depth["y0"] = thickness["y0"] - self.gv.ys
if thickness["z0"] - self.gv.zs > 0:
gv_pml_depth["z0"] = thickness["z0"] - self.gv.zs
if self.gv.xf > grid.nx - thickness["xmax"]:
gv_pml_depth["xmax"] = self.gv.xf - (grid.nx - thickness["xmax"])
if self.gv.yf > grid.ny - thickness["ymax"]:
gv_pml_depth["ymax"] = self.gv.yf - (grid.ny - thickness["ymax"])
if self.gv.zf > grid.nz - thickness["zmax"]:
gv_pml_depth["zmax"] = self.gv.zf - (grid.nz - thickness["zmax"])
return list(pmlstorender.values())
return list(gv_pml_depth.values())
def srcs_rx_gv_comment(self, srcs):
def srcs_rx_gv_comment(
self, srcs: Union[Sequence[Source], List[Rx]]
) -> Tuple[List[str], npt.NDArray[np.float32]]:
"""Used to name sources and/or receivers."""
sc = []
for src in srcs:
p = (
src.xcoord * self.grid.dx,
src.ycoord * self.grid.dy,
src.zcoord * self.grid.dz,
)
p = list(map(float, p))
names: List[str] = []
positions: npt.NDArray[np.float32] = np.empty((len(srcs), 3))
for index, src in enumerate(srcs):
position = src.coord * self.grid.dl
names.append(src.ID)
positions[index] = position
s = {"name": src.ID, "position": p}
sc.append(s)
return names, positions
return sc
def dx_dy_dz_comment(self) -> npt.NDArray[np.float64]:
return self.grid.dl
def dx_dy_dz_comment(self):
return list(map(float, [self.grid.dx, self.grid.dy, self.grid.dz]))
def nx_ny_nz_comment(self) -> npt.NDArray[np.int32]:
return np.array([self.grid.nx, self.grid.ny, self.grid.nz], dtype=np.int32)
def nx_ny_nz_comment(self):
return list(map(int, [self.grid.nx, self.grid.ny, self.grid.nz]))
def materials_comment(self):
def materials_comment(self) -> List[str]:
if not self.averaged_materials:
return [
m.ID for m in self.grid.materials if m.type != "dielectric-smoothed"
]
return [m.ID for m in self.grid.materials if m.type != "dielectric-smoothed"]
else:
return [m.ID for m in self.grid.materials]
@@ -380,9 +440,7 @@ class Comments:
class GeometryObjects:
"""Geometry objects to be written to file."""
def __init__(
self, xs=None, ys=None, zs=None, xf=None, yf=None, zf=None, basefilename=None
):
def __init__(self, xs=None, ys=None, zs=None, xf=None, yf=None, zf=None, basefilename=None):
"""
Args:
xs, xf, ys, yf, zs, zf: ints for extent of the volume in cells.
@@ -412,22 +470,14 @@ class GeometryObjects:
(self.nx + 1) * (self.ny + 1) * (self.nz + 1) * np.dtype(np.uint32).itemsize
)
self.rigidsize = (
18
* (self.nx + 1)
* (self.ny + 1)
* (self.nz + 1)
* np.dtype(np.int8).itemsize
18 * (self.nx + 1) * (self.ny + 1) * (self.nz + 1) * np.dtype(np.int8).itemsize
)
self.IDsize = (
6
* (self.nx + 1)
* (self.ny + 1)
* (self.nz + 1)
* np.dtype(np.uint32).itemsize
6 * (self.nx + 1) * (self.ny + 1) * (self.nz + 1) * np.dtype(np.uint32).itemsize
)
self.datawritesize = self.solidsize + self.rigidsize + self.IDsize
def write_hdf5(self, G, pbar):
def write_hdf5(self, title: str, G: FDTDGrid, pbar: tqdm):
"""Writes a geometry objects file in HDF5 format.
Args:
@@ -435,91 +485,173 @@ class GeometryObjects:
pbar: Progress bar class instance.
"""
ID = G.ID[:, self.xs : self.xf + 1, self.ys : self.yf + 1, self.zs : self.zf + 1]
# Get materials present in subset of ID array and sort by material ID
material_ids, inverse_map = np.unique(ID, return_inverse=True)
get_material = np.vectorize(lambda id: G.materials[id])
materials = sorted(get_material(material_ids))
# Create map from material ID to 0 - number of materials
materials_map = {material.numID: index for index, material in enumerate(materials)}
map_materials = np.vectorize(lambda id: materials_map[id])
# Remap ID array to the reduced list of materials
ID = np.array(map_materials(material_ids))[inverse_map].reshape(ID.shape)
data = G.solid[self.xs : self.xf, self.ys : self.yf, self.zs : self.zf].astype("int16")
data = map_materials(data)
rigidE = G.rigidE[:, self.xs : self.xf, self.ys : self.yf, self.zs : self.zf]
rigidH = G.rigidH[:, self.xs : self.xf, self.ys : self.yf, self.zs : self.zf]
with h5py.File(self.filename_hdf5, "w") as fdata:
fdata.attrs["gprMax"] = __version__
fdata.attrs["Title"] = G.title
fdata.attrs["Title"] = title
fdata.attrs["dx_dy_dz"] = (G.dx, G.dy, G.dz)
# Get minimum and maximum integers of materials in geometry objects volume
minmat = np.amin(
G.ID[
:,
self.xs : self.xf + 1,
self.ys : self.yf + 1,
self.zs : self.zf + 1,
]
)
maxmat = np.amax(
G.ID[
:,
self.xs : self.xf + 1,
self.ys : self.yf + 1,
self.zs : self.zf + 1,
]
)
fdata["/data"] = (
G.solid[
self.xs : self.xf + 1, self.ys : self.yf + 1, self.zs : self.zf + 1
].astype("int16")
- minmat
)
fdata["/data"] = data
pbar.update(self.solidsize)
fdata["/rigidE"] = G.rigidE[
:, self.xs : self.xf + 1, self.ys : self.yf + 1, self.zs : self.zf + 1
]
fdata["/rigidH"] = G.rigidH[
:, self.xs : self.xf + 1, self.ys : self.yf + 1, self.zs : self.zf + 1
]
fdata["/rigidE"] = rigidE
fdata["/rigidH"] = rigidH
pbar.update(self.rigidsize)
fdata["/ID"] = (
G.ID[
:,
self.xs : self.xf + 1,
self.ys : self.yf + 1,
self.zs : self.zf + 1,
]
- minmat
)
fdata["/ID"] = ID
pbar.update(self.IDsize)
# Write materials list to a text file
# This includes all materials in range whether used in volume or not
with open(self.filename_materials, "w") as fmaterials:
for numID in range(minmat, maxmat + 1):
for material in G.materials:
if material.numID == numID:
fmaterials.write(
f"#material: {material.er:g} {material.se:g} "
f"{material.mr:g} {material.sm:g} {material.ID}\n"
)
if hasattr(material, "poles"):
if "debye" in material.type:
dispersionstr = (
f"#add_dispersion_debye: {material.poles:g} "
)
for pole in range(material.poles):
dispersionstr += (
f"{material.deltaer[pole]:g} "
f"{material.tau[pole]:g} "
)
elif "lorenz" in material.type:
dispersionstr = (
f"#add_dispersion_lorenz: {material.poles:g} "
)
for pole in range(material.poles):
dispersionstr += (
f"{material.deltaer[pole]:g} "
f"{material.tau[pole]:g} "
f"{material.alpha[pole]:g} "
)
elif "drude" in material.type:
dispersionstr = (
f"#add_dispersion_drude: {material.poles:g} "
)
for pole in range(material.poles):
dispersionstr += (
f"{material.tau[pole]:g} "
f"{material.alpha[pole]:g} "
)
dispersionstr += material.ID
fmaterials.write(dispersionstr + "\n")
for material in materials:
self.output_material(material, fmaterials)
def output_material(self, material: Material, file: TextIOWrapper):
file.write(
f"#material: {material.er:g} {material.se:g} "
f"{material.mr:g} {material.sm:g} {material.ID}\n"
)
if hasattr(material, "poles"):
if "debye" in material.type:
dispersionstr = "#add_dispersion_debye: " f"{material.poles:g} "
for pole in range(material.poles):
dispersionstr += f"{material.deltaer[pole]:g} " f"{material.tau[pole]:g} "
elif "lorenz" in material.type:
dispersionstr = f"#add_dispersion_lorenz: " f"{material.poles:g} "
for pole in range(material.poles):
dispersionstr += (
f"{material.deltaer[pole]:g} "
f"{material.tau[pole]:g} "
f"{material.alpha[pole]:g} "
)
elif "drude" in material.type:
dispersionstr = f"#add_dispersion_drude: " f"{material.poles:g} "
for pole in range(material.poles):
dispersionstr += f"{material.tau[pole]:g} " f"{material.alpha[pole]:g} "
dispersionstr += material.ID
file.write(dispersionstr + "\n")
class MPIGeometryObjects(GeometryObjects):
# def __init__(self, start, stop, global_size, comm):
# super().__init__(...)
def write_hdf5(self, title: str, G: MPIGrid, pbar: tqdm):
"""Writes a geometry objects file in HDF5 format.
Args:
G: FDTDGrid class describing a grid in a model.
pbar: Progress bar class instance.
"""
# Get neighbours
self.neighbours = np.full((3, 2), -1, dtype=int)
self.neighbours[0] = self.comm.Shift(direction=0, disp=1)
self.neighbours[1] = self.comm.Shift(direction=1, disp=1)
self.neighbours[2] = self.comm.Shift(direction=2, disp=1)
# Make subsection of array one larger if no positive neighbour
slice_stop = np.where(
self.neighbours[:, 1] == -1,
self.stop + 1,
self.stop,
)
array_slice = list(map(slice, self.start, slice_stop))
ID = G.ID[:, array_slice[0], array_slice[1], array_slice[2]]
# Get materials present in subset of ID
local_material_num_ids, inverse_map = np.unique(ID, return_inverse=True)
get_material = np.vectorize(lambda id: G.materials[id])
local_materials = get_material(local_material_num_ids)
# Send all materials to the coordinating rank
materials = self.comm.gather(local_materials, root=0)
if self.comm.rank == 0:
# Filter out duplicate materials and sort by material ID
materials = np.fromiter(chain.from_iterable(materials), dtype=Material)
global_materials = np.unique(materials)
global_material_ids = [m.ID for m in global_materials]
else:
global_material_ids = None
global_material_ids = self.comm.bcast(global_material_ids, root=0)
# Create map from material ID (str) to global material numID
materials_map = {
material_id: index for index, material_id in enumerate(global_material_ids)
}
# Remap ID array to the global material numID
ID = np.array([materials_map[m.ID] for m in local_materials])[inverse_map].reshape(ID.shape)
# Other geometry arrays do not have halos, so adjustment to
# 'stop' is not necessary
array_slice = list(map(slice, self.start, self.stop))
data = G.solid[array_slice[0], array_slice[1], array_slice[2]]
map_local_materials = np.vectorize(lambda id: materials_map[G.materials[id].ID])
data = map_local_materials(data)
rigidE = G.rigidE[:, array_slice[0], array_slice[1], array_slice[2]]
rigidH = G.rigidH[:, array_slice[0], array_slice[1], array_slice[2]]
start = self.offset
stop = start + self.size
with h5py.File(self.filename_hdf5, "w", driver="mpio", comm=self.comm) as fdata:
fdata.attrs["gprMax"] = __version__
fdata.attrs["Title"] = title
fdata.attrs["dx_dy_dz"] = (G.dx, G.dy, G.dz)
dset = fdata.create_dataset("/data", self.global_size, dtype=data.dtype)
dset[start[0] : stop[0], start[1] : stop[1], start[2] : stop[2]] = data
pbar.update(self.solidsize)
rigid_E_dataset = fdata.create_dataset(
"/rigidE", (12, *self.global_size), dtype=rigidE.dtype
)
rigid_E_dataset[:, start[0] : stop[0], start[1] : stop[1], start[2] : stop[2]] = rigidE
rigid_H_dataset = fdata.create_dataset(
"/rigidH", (6, *self.global_size), dtype=rigidH.dtype
)
rigid_H_dataset[:, start[0] : stop[0], start[1] : stop[1], start[2] : stop[2]] = rigidH
pbar.update(self.rigidsize)
stop = np.where(
self.neighbours[:, 1] == -1,
stop + 1,
stop,
)
dset = fdata.create_dataset("/ID", (6, *(self.global_size + 1)), dtype=ID.dtype)
dset[:, start[0] : stop[0], start[1] : stop[1], start[2] : stop[2]] = ID
pbar.update(self.IDsize)
# Write materials list to a text file
if self.comm.rank == 0:
with open(self.filename_materials, "w") as materials_file:
for material in global_materials:
self.output_material(material, materials_file)

查看文件

@@ -15,22 +15,22 @@
#
# You should have received a copy of the GNU General Public License
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
import argparse
import gprMax.config as config
from .contexts import Context, MPIContext
from .contexts import Context, MPIContext, TaskfarmContext
from .utilities.logging import logging_config
# Arguments (used for API) and their default values (used for API and CLI)
args_defaults = {
"scenes": [],
"scenes": None,
"inputfile": None,
"outputfile": None,
"n": 1,
"i": None,
"mpi": False,
"taskfarm": False,
"mpi": None,
"gpu": None,
"opencl": None,
"subgrid": False,
@@ -38,47 +38,77 @@ args_defaults = {
"geometry_only": False,
"geometry_fixed": False,
"write_processed": False,
"show_progress_bars": False,
"hide_progress_bars": False,
"log_level": 20, # Level DEBUG = 10; INFO = 20; BASIC = 25
"log_file": False,
"log_all_ranks": False,
}
# Argument help messages (used for CLI argparse)
help_msg = {
"scenes": "(list, req): Scenes to run the model. Multiple scene objects "
"can given in order to run multiple simulation runs. Each scene "
"must contain the essential simulation objects",
"inputfile": "(str, opt): Input file path. Can also run simulation by "
"providing an input file.",
"scenes": (
"(list, req): Scenes to run the model. Multiple scene objects can given in order to run"
" multiple simulation runs. Each scene must contain the essential simulation objects"
),
"inputfile": "(str, opt): Input file path. Can also run simulation by providing an input file.",
"outputfile": "(str, req): File path to the output data file.",
"n": "(int, req): Number of required simulation runs.",
"i": "(int, opt): Model number to start/restart simulation from. It would "
"typically be used to restart a series of models from a specific "
"model number, with the n argument, e.g. to restart from A-scan 45 "
"when creating a B-scan with 60 traces.",
"mpi": "(bool, opt): Flag to use Message Passing Interface (MPI) task farm. "
"This option is most usefully combined with n to allow individual "
"models to be farmed out using a MPI task farm, e.g. to create a "
"B-scan with 60 traces and use MPI to farm out each trace. For "
"further details see the performance section of the User Guide.",
"gpu": "(list/bool, opt): Flag to use NVIDIA GPU or list of NVIDIA GPU "
"device ID(s) for specific GPU card(s).",
"opencl": "(list/bool, opt): Flag to use OpenCL or list of OpenCL device "
"ID(s) for specific compute device(s).",
"i": (
"(int, opt): Model number to start/restart simulation from. It would typically be used to"
" restart a series of models from a specific model number, with the n argument, e.g. to"
" restart from A-scan 45 when creating a B-scan with 60 traces."
),
"taskfarm": (
"(bool, opt): Flag to use Message Passing Interface (MPI) task farm. This option is most"
" usefully combined with n to allow individual models to be farmed out using a MPI task"
" farm, e.g. to create a B-scan with 60 traces and use MPI to farm out each trace. For"
" further details see the performance section of the User Guide."
),
"mpi": (
"(list, opt): Flag to use Message Passing Interface (MPI) to divide the model between MPI"
" ranks. Three integers should be provided to define the number of MPI processes (min 1) in"
" the x, y, and z dimensions."
),
"gpu": (
"(list/bool, opt): Flag to use NVIDIA GPU or list of NVIDIA GPU device ID(s) for specific"
" GPU card(s)."
),
"opencl": (
"(list/bool, opt): Flag to use OpenCL or list of OpenCL device ID(s) for specific compute"
" device(s)."
),
"subgrid": "(bool, opt): Flag to use sub-gridding.",
"autotranslate": "(bool, opt): For sub-gridding - auto translate objects "
"with main grid coordinates to their equivalent local "
"grid coordinate within the subgrid. If this option is "
"off users must specify sub-grid object point within the "
"global subgrid space.",
"geometry_only": "(bool, opt): Build a model and produce any geometry "
"views but do not run the simulation.",
"geometry_fixed": "(bool, opt): Run a series of models where the geometry "
"does not change between models.",
"write_processed": "(bool, opt): Writes another input file after any "
"Python code (#python blocks) and in the original input "
"file has been processed.",
"autotranslate": (
"(bool, opt): For sub-gridding - auto translate objects with main grid coordinates to their"
" equivalent local grid coordinate within the subgrid. If this option is off users must"
" specify sub-grid object point within the global subgrid space."
),
"geometry_only": (
"(bool, opt): Build a model and produce any geometry views but do not run the simulation."
),
"geometry_fixed": (
"(bool, opt): Run a series of models where the geometry does not change between models."
),
"write_processed": (
"(bool, opt): Writes another input file after any Python code (#python blocks) and in the"
" original input file has been processed."
),
"show_progress_bars": (
"(bool, opt): Forces progress bars to be displayed - by default, progress bars are"
" displayed when the log level is info (20) or less."
),
"hide_progress_bars": (
"(bool, opt): Forces progress bars to be hidden - by default, progress bars are hidden when"
" the log level is greater than info (20)."
),
"log_level": "(int, opt): Level of logging to use.",
"log_file": "(bool, opt): Write logging information to file.",
"log_all_ranks": (
"(bool, opt): Write logging information from all MPI ranks. Default behaviour only provides"
" log output from rank 0. When used with --log-file, each rank will write to an individual"
" file."
),
}
@@ -88,6 +118,7 @@ def run(
outputfile=args_defaults["outputfile"],
n=args_defaults["n"],
i=args_defaults["i"],
taskfarm=args_defaults["taskfarm"],
mpi=args_defaults["mpi"],
gpu=args_defaults["gpu"],
opencl=args_defaults["opencl"],
@@ -96,49 +127,69 @@ def run(
geometry_only=args_defaults["geometry_only"],
geometry_fixed=args_defaults["geometry_fixed"],
write_processed=args_defaults["write_processed"],
show_progress_bars=args_defaults["show_progress_bars"],
hide_progress_bars=args_defaults["hide_progress_bars"],
log_level=args_defaults["log_level"],
log_file=args_defaults["log_file"],
log_all_ranks=args_defaults["log_all_ranks"],
):
"""Entry point for application programming interface (API). Runs the
simulation for the given list of scenes.
"""Entry point for application programming interface (API).
Runs the simulation for the given list of scenes.
Args:
scenes: list of the scenes to run the model. Multiple scene objects can
be given in order to run multiple simulation runs. Each scene
must contain the essential simulation objects.
inputfile: optional string for input file path. Can also run simulation
by providing an input file.
scenes: list of the scenes to run the model. Multiple scene
objects can be given in order to run multiple simulation
runs. Each scene must contain the essential simulation
objects.
inputfile: optional string for input file path. Can also run
simulation by providing an input file.
outputfile: string for file path to the output data file
n: optional int for number of required simulation runs.
i: optional int for model number to start/restart simulation from.
It would typically be used to restart a series of models from a
specific model number, with the n argument, e.g. to restart from
A-scan 45 when creating a B-scan with 60 traces.
mpi: optional boolean flag to use Message Passing Interface (MPI) task
farm. This option is most usefully combined with n to allow
individual models to be farmed out using a MPI task farm,
e.g. to create a B-scan with 60 traces and use MPI to farm out
each trace. For further details see the performance section of
the User Guide
gpu: optional list/boolean to use NVIDIA GPU or list of NVIDIA GPU device
ID(s) for specific GPU card(s).
opencl: optional list/boolean to use OpenCL or list of OpenCL device ID(s)
for specific compute device(s).
i: optional int for model number to start/restart simulation
from. It would typically be used to restart a series of
models from a specific model number, with the n argument,
e.g. to restart from A-scan 45 when creating a B-scan with
60 traces.
taskfarm: optional boolean flag to use Message Passing Interface
(MPI) task farm. This option is most usefully combined with
n to allow individual models to be farmed out using a MPI
task farm, e.g. to create a B-scan with 60 traces and use
MPI to farm out each trace. For further details see the
performance section of the User Guide.
mpi: optional flag to use Message Passing Interface (MPI) to
divide the model between MPI ranks. Three integers should be
provided to define the number of MPI processes (min 1) in
the x, y, and z dimensions.
gpu: optional list/boolean to use NVIDIA GPU or list of NVIDIA
GPU device ID(s) for specific GPU card(s).
opencl: optional list/boolean to use OpenCL or list of OpenCL
device ID(s) for specific compute device(s).
subgrid: optional boolean to use sub-gridding.
autotranslate: optional boolean for sub-gridding to auto translate
objects with main grid coordinates to their equivalent
local grid coordinate within the subgrid. If this option
is off users must specify sub-grid object point within
the global subgrid space.
autotranslate: optional boolean for sub-gridding to auto
translate objects with main grid coordinates to their
equivalent local grid coordinate within the subgrid. If this
option is off users must specify sub-grid object point
within the global subgrid space.
geometry_only: optional boolean to build a model and produce any
geometry views but do not run the simulation.
geometry_fixed: optional boolean to run a series of models where the
geometry does not change between models.
write_processed: optional boolean to write another input file after any
#python blocks (which are deprecated) in the
original input file has been processed.
geometry views but do not run the simulation.
geometry_fixed: optional boolean to run a series of models where
the geometry does not change between models.
write_processed: optional boolean to write another input file
after any #python blocks (which are deprecated) in the
original input file has been processed.
show_progress_bars: optional boolean to force progress bars to
be displayed - by default, progress bars are displayed when
the log level is info (20) or less.
hide_progress_bars: optional boolean to force progress bars to
be hidden - by default, progress bars are hidden when the
log level is greater than info (20).
log_level: optional int for level of logging to use.
log_file: optional boolean to write logging information to file.
log_all_ranks: optional boolean to write logging information
from all MPI ranks. Default behaviour only provides log
output from rank 0. When used with --log-file, each ran
will write to an individual file.
"""
args = argparse.Namespace(
@@ -148,6 +199,7 @@ def run(
"outputfile": outputfile,
"n": n,
"i": i,
"taskfarm": taskfarm,
"mpi": mpi,
"gpu": gpu,
"opencl": opencl,
@@ -156,8 +208,11 @@ def run(
"geometry_only": geometry_only,
"geometry_fixed": geometry_fixed,
"write_processed": write_processed,
"show_progress_bars": show_progress_bars,
"hide_progress_bars": hide_progress_bars,
"log_level": log_level,
"log_file": log_file,
"log_all_ranks": log_all_ranks,
}
)
@@ -172,17 +227,26 @@ def cli():
prog="gprMax", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("inputfile", help=help_msg["inputfile"])
parser.add_argument("-outputfile", "-o", help=help_msg["outputfile"])
parser.add_argument("-n", default=args_defaults["n"], type=int, help=help_msg["n"])
parser.add_argument("-i", type=int, help=help_msg["i"])
parser.add_argument(
"-mpi", action="store_true", default=args_defaults["mpi"], help=help_msg["mpi"]
"--taskfarm",
"-t",
action="store_true",
default=args_defaults["taskfarm"],
help=help_msg["taskfarm"],
)
parser.add_argument(
"-gpu", type=int, action="append", nargs="*", help=help_msg["gpu"]
)
parser.add_argument(
"-opencl", type=int, action="append", nargs="*", help=help_msg["opencl"]
"--mpi",
type=int,
action="store",
nargs=3,
default=args_defaults["mpi"],
help=help_msg["mpi"],
)
parser.add_argument("-gpu", type=int, action="append", nargs="*", help=help_msg["gpu"])
parser.add_argument("-opencl", type=int, action="append", nargs="*", help=help_msg["opencl"])
parser.add_argument(
"--geometry-only",
action="store_true",
@@ -202,10 +266,19 @@ def cli():
help=help_msg["write_processed"],
)
parser.add_argument(
"--log-level",
type=int,
default=args_defaults["log_level"],
help=help_msg["log_level"],
"--show-progress-bars",
action="store_true",
default=args_defaults["show_progress_bars"],
help=help_msg["show_progress_bars"],
)
parser.add_argument(
"--hide-progress-bars",
action="store_true",
default=args_defaults["hide_progress_bars"],
help=help_msg["hide_progress_bars"],
)
parser.add_argument(
"--log-level", type=int, default=args_defaults["log_level"], help=help_msg["log_level"]
)
parser.add_argument(
"--log-file",
@@ -213,6 +286,12 @@ def cli():
default=args_defaults["log_file"],
help=help_msg["log_file"],
)
parser.add_argument(
"--log-all-ranks",
action="store_true",
default=args_defaults["log_all_ranks"],
help=help_msg["log_all_ranks"],
)
args = parser.parse_args()
results = run_main(args)
@@ -227,16 +306,25 @@ def run_main(args):
args: namespace with arguments from either API or CLI.
Returns:
results: dict that can contain useful results/data from simulation.
Enables these to be propagated to calling script.
results: dict that can contain useful results/data from
simulation. Enables these to be propagated to calling
script.
"""
results = {}
logging_config(level=args.log_level, log_file=args.log_file)
logging_config(
level=args.log_level,
log_file=args.log_file,
mpi_logger=args.mpi is not None,
log_all_ranks=args.log_all_ranks,
)
config.sim_config = config.SimulationConfig(args)
# MPI running with (OpenMP/CUDA/OpenCL)
if config.sim_config.args.mpi:
# MPI taskfarm running with (OpenMP/CUDA/OpenCL)
if config.sim_config.args.taskfarm:
context = TaskfarmContext()
# MPI running to divide model between ranks
elif config.sim_config.args.mpi is not None:
context = MPIContext()
# Standard running (OpenMP/CUDA/OpenCL)
else:

查看文件

@@ -1,593 +0,0 @@
# Copyright (C) 2015-2025: 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 decimal as d
from collections import OrderedDict
from importlib import import_module
import numpy as np
import gprMax.config as config
from .pml import PML
from .utilities.utilities import fft_power, round_value
np.seterr(invalid="raise")
class FDTDGrid:
"""Holds attributes associated with entire grid. A convenient way for
accessing regularly used parameters.
"""
def __init__(self):
self.title = ""
self.name = "main_grid"
self.mem_use = 0
self.nx = 0
self.ny = 0
self.nz = 0
self.dx = 0
self.dy = 0
self.dz = 0
self.dt = 0
self.dt_mod = None # Time step stability factor
self.iteration = 0 # Current iteration number
self.iterations = 0 # Total number of iterations
self.timewindow = 0
# PML parameters - set some defaults to use if not user provided
self.pmls = {}
self.pmls["formulation"] = "HORIPML"
self.pmls["cfs"] = []
self.pmls["slabs"] = []
# Ordered dictionary required so *updating* the PMLs always follows the
# same order (the order for *building* PMLs does not matter). The order
# itself does not matter, however, if must be the same from model to
# model otherwise the numerical precision from adding the PML
# corrections will be different.
self.pmls["thickness"] = OrderedDict((key, 10) for key in PML.boundaryIDs)
self.materials = []
self.mixingmodels = []
self.averagevolumeobjects = True
self.fractalvolumes = []
self.geometryviews = []
self.geometryobjectswrite = []
self.waveforms = []
self.voltagesources = []
self.hertziandipoles = []
self.magneticdipoles = []
self.transmissionlines = []
self.discreteplanewaves = []
self.rxs = []
self.srcsteps = [0, 0, 0]
self.rxsteps = [0, 0, 0]
self.snapshots = []
self.subgrids = []
def within_bounds(self, p):
if p[0] < 0 or p[0] > self.nx:
raise ValueError("x")
if p[1] < 0 or p[1] > self.ny:
raise ValueError("y")
if p[2] < 0 or p[2] > self.nz:
raise ValueError("z")
def discretise_point(self, p):
x = round_value(float(p[0]) / self.dx)
y = round_value(float(p[1]) / self.dy)
z = round_value(float(p[2]) / self.dz)
return (x, y, z)
def round_to_grid(self, p):
p = self.discretise_point(p)
p_r = (p[0] * self.dx, p[1] * self.dy, p[2] * self.dz)
return p_r
def within_pml(self, p):
if (
p[0] < self.pmls["thickness"]["x0"]
or p[0] > self.nx - self.pmls["thickness"]["xmax"]
or p[1] < self.pmls["thickness"]["y0"]
or p[1] > self.ny - self.pmls["thickness"]["ymax"]
or p[2] < self.pmls["thickness"]["z0"]
or p[2] > self.nz - self.pmls["thickness"]["zmax"]
):
return True
else:
return False
def initialise_geometry_arrays(self):
"""Initialise an array for volumetric material IDs (solid);
boolean arrays for specifying whether materials can have dielectric
smoothing (rigid); and an array for cell edge IDs (ID).
Solid and ID arrays are initialised to free_space (one);
rigid arrays to allow dielectric smoothing (zero).
"""
self.solid = np.ones((self.nx, self.ny, self.nz), dtype=np.uint32)
self.rigidE = np.zeros((12, self.nx, self.ny, self.nz), dtype=np.int8)
self.rigidH = np.zeros((6, self.nx, self.ny, self.nz), dtype=np.int8)
self.ID = np.ones((6, self.nx + 1, self.ny + 1, self.nz + 1), dtype=np.uint32)
self.IDlookup = {"Ex": 0, "Ey": 1, "Ez": 2, "Hx": 3, "Hy": 4, "Hz": 5}
def initialise_field_arrays(self):
"""Initialise arrays for the electric and magnetic field components."""
self.Ex = np.zeros(
(self.nx + 1, self.ny + 1, self.nz + 1),
dtype=config.sim_config.dtypes["float_or_double"],
)
self.Ey = np.zeros(
(self.nx + 1, self.ny + 1, self.nz + 1),
dtype=config.sim_config.dtypes["float_or_double"],
)
self.Ez = np.zeros(
(self.nx + 1, self.ny + 1, self.nz + 1),
dtype=config.sim_config.dtypes["float_or_double"],
)
self.Hx = np.zeros(
(self.nx + 1, self.ny + 1, self.nz + 1),
dtype=config.sim_config.dtypes["float_or_double"],
)
self.Hy = np.zeros(
(self.nx + 1, self.ny + 1, self.nz + 1),
dtype=config.sim_config.dtypes["float_or_double"],
)
self.Hz = np.zeros(
(self.nx + 1, self.ny + 1, self.nz + 1),
dtype=config.sim_config.dtypes["float_or_double"],
)
def initialise_std_update_coeff_arrays(self):
"""Initialise arrays for storing update coefficients."""
self.updatecoeffsE = np.zeros(
(len(self.materials), 5), dtype=config.sim_config.dtypes["float_or_double"]
)
self.updatecoeffsH = np.zeros(
(len(self.materials), 5), dtype=config.sim_config.dtypes["float_or_double"]
)
def initialise_dispersive_arrays(self):
"""Initialise field arrays when there are dispersive materials present."""
self.Tx = np.zeros(
(
config.get_model_config().materials["maxpoles"],
self.nx + 1,
self.ny + 1,
self.nz + 1,
),
dtype=config.get_model_config().materials["dispersivedtype"],
)
self.Ty = np.zeros(
(
config.get_model_config().materials["maxpoles"],
self.nx + 1,
self.ny + 1,
self.nz + 1,
),
dtype=config.get_model_config().materials["dispersivedtype"],
)
self.Tz = np.zeros(
(
config.get_model_config().materials["maxpoles"],
self.nx + 1,
self.ny + 1,
self.nz + 1,
),
dtype=config.get_model_config().materials["dispersivedtype"],
)
def initialise_dispersive_update_coeff_array(self):
"""Initialise array for storing update coefficients when there are dispersive
materials present.
"""
self.updatecoeffsdispersive = np.zeros(
(len(self.materials), 3 * config.get_model_config().materials["maxpoles"]),
dtype=config.get_model_config().materials["dispersivedtype"],
)
def reset_fields(self):
"""Clear arrays for field components and PMLs."""
# Clear arrays for field components
self.initialise_field_arrays()
if config.get_model_config().materials["maxpoles"] > 0:
self.initialise_dispersive_arrays()
# Clear arrays for fields in PML
for pml in self.pmls["slabs"]:
pml.initialise_field_arrays()
def mem_est_basic(self):
"""Estimates the amount of memory (RAM) required for grid arrays.
Returns:
mem_use: int of memory (bytes).
"""
solidarray = self.nx * self.ny * self.nz * np.dtype(np.uint32).itemsize
# 12 x rigidE array components + 6 x rigidH array components
rigidarrays = (
(12 + 6) * self.nx * self.ny * self.nz * np.dtype(np.int8).itemsize
)
# 6 x field arrays + 6 x ID arrays
fieldarrays = (
(6 + 6)
* (self.nx + 1)
* (self.ny + 1)
* (self.nz + 1)
* np.dtype(config.sim_config.dtypes["float_or_double"]).itemsize
)
# PML arrays
pmlarrays = 0
for k, v in self.pmls["thickness"].items():
if v > 0:
if "x" in k:
pmlarrays += (v + 1) * self.ny * (self.nz + 1)
pmlarrays += (v + 1) * (self.ny + 1) * self.nz
pmlarrays += v * self.ny * (self.nz + 1)
pmlarrays += v * (self.ny + 1) * self.nz
elif "y" in k:
pmlarrays += self.nx * (v + 1) * (self.nz + 1)
pmlarrays += (self.nx + 1) * (v + 1) * self.nz
pmlarrays += (self.nx + 1) * v * self.nz
pmlarrays += self.nx * v * (self.nz + 1)
elif "z" in k:
pmlarrays += self.nx * (self.ny + 1) * (v + 1)
pmlarrays += (self.nx + 1) * self.ny * (v + 1)
pmlarrays += (self.nx + 1) * self.ny * v
pmlarrays += self.nx * (self.ny + 1) * v
mem_use = int(fieldarrays + solidarray + rigidarrays + pmlarrays)
return mem_use
def mem_est_dispersive(self):
"""Estimates the amount of memory (RAM) required for dispersive grid arrays.
Returns:
mem_use: int of memory (bytes).
"""
mem_use = int(
3
* config.get_model_config().materials["maxpoles"]
* (self.nx + 1)
* (self.ny + 1)
* (self.nz + 1)
* np.dtype(config.get_model_config().materials["dispersivedtype"]).itemsize
)
return mem_use
def mem_est_fractals(self):
"""Estimates the amount of memory (RAM) required to build any objects
which use the FractalVolume/FractalSurface classes.
Returns:
mem_use: int of memory (bytes).
"""
mem_use = 0
for vol in self.fractalvolumes:
mem_use += vol.nx * vol.ny * vol.nz * vol.dtype.itemsize
for surface in vol.fractalsurfaces:
surfacedims = surface.get_surface_dims()
mem_use += surfacedims[0] * surfacedims[1] * surface.dtype.itemsize
return mem_use
def tmx(self):
"""Add PEC boundaries to invariant direction in 2D TMx mode.
N.B. 2D modes are a single cell slice of 3D grid.
"""
# Ey & Ez components
self.ID[1, 0, :, :] = 0
self.ID[1, 1, :, :] = 0
self.ID[2, 0, :, :] = 0
self.ID[2, 1, :, :] = 0
def tmy(self):
"""Add PEC boundaries to invariant direction in 2D TMy mode.
N.B. 2D modes are a single cell slice of 3D grid.
"""
# Ex & Ez components
self.ID[0, :, 0, :] = 0
self.ID[0, :, 1, :] = 0
self.ID[2, :, 0, :] = 0
self.ID[2, :, 1, :] = 0
def tmz(self):
"""Add PEC boundaries to invariant direction in 2D TMz mode.
N.B. 2D modes are a single cell slice of 3D grid.
"""
# Ex & Ey components
self.ID[0, :, :, 0] = 0
self.ID[0, :, :, 1] = 0
self.ID[1, :, :, 0] = 0
self.ID[1, :, :, 1] = 0
def calculate_dt(self):
"""Calculate time step at the CFL limit."""
if config.get_model_config().mode == "2D TMx":
self.dt = 1 / (
config.sim_config.em_consts["c"]
* np.sqrt((1 / self.dy**2) + (1 / self.dz**2))
)
elif config.get_model_config().mode == "2D TMy":
self.dt = 1 / (
config.sim_config.em_consts["c"]
* np.sqrt((1 / self.dx**2) + (1 / self.dz**2))
)
elif config.get_model_config().mode == "2D TMz":
self.dt = 1 / (
config.sim_config.em_consts["c"]
* np.sqrt((1 / self.dx**2) + (1 / self.dy**2))
)
else:
self.dt = 1 / (
config.sim_config.em_consts["c"]
* np.sqrt((1 / self.dx**2) + (1 / self.dy**2) + (1 / self.dz**2))
)
# Round down time step to nearest float with precision one less than
# hardware maximum. Avoids inadvertently exceeding the CFL due to
# binary representation of floating point number.
self.dt = round_value(self.dt, decimalplaces=d.getcontext().prec - 1)
class CUDAGrid(FDTDGrid):
"""Additional grid methods for solving on GPU using CUDA."""
def __init__(self):
super().__init__()
self.gpuarray = import_module("pycuda.gpuarray")
# Threads per block - used for main electric/magnetic field updates
self.tpb = (128, 1, 1)
# Blocks per grid - used for main electric/magnetic field updates
self.bpg = None
def set_blocks_per_grid(self):
"""Set the blocks per grid size used for updating the electric and
magnetic field arrays on a GPU.
"""
self.bpg = (
int(np.ceil(((self.nx + 1) * (self.ny + 1) * (self.nz + 1)) / self.tpb[0])),
1,
1,
)
def htod_geometry_arrays(self):
"""Initialise an array for cell edge IDs (ID) on compute device."""
self.ID_dev = self.gpuarray.to_gpu(self.ID)
def htod_field_arrays(self):
"""Initialise field arrays on compute device."""
self.Ex_dev = self.gpuarray.to_gpu(self.Ex)
self.Ey_dev = self.gpuarray.to_gpu(self.Ey)
self.Ez_dev = self.gpuarray.to_gpu(self.Ez)
self.Hx_dev = self.gpuarray.to_gpu(self.Hx)
self.Hy_dev = self.gpuarray.to_gpu(self.Hy)
self.Hz_dev = self.gpuarray.to_gpu(self.Hz)
def htod_dispersive_arrays(self):
"""Initialise dispersive material coefficient arrays on compute device."""
self.updatecoeffsdispersive_dev = self.gpuarray.to_gpu(
self.updatecoeffsdispersive
)
self.Tx_dev = self.gpuarray.to_gpu(self.Tx)
self.Ty_dev = self.gpuarray.to_gpu(self.Ty)
self.Tz_dev = self.gpuarray.to_gpu(self.Tz)
class OpenCLGrid(FDTDGrid):
"""Additional grid methods for solving on compute device using OpenCL."""
def __init__(self):
super().__init__()
self.clarray = import_module("pyopencl.array")
def htod_geometry_arrays(self, queue):
"""Initialise an array for cell edge IDs (ID) on compute device.
Args:
queue: pyopencl queue.
"""
self.ID_dev = self.clarray.to_device(queue, self.ID)
def htod_field_arrays(self, queue):
"""Initialise field arrays on compute device.
Args:
queue: pyopencl queue.
"""
self.Ex_dev = self.clarray.to_device(queue, self.Ex)
self.Ey_dev = self.clarray.to_device(queue, self.Ey)
self.Ez_dev = self.clarray.to_device(queue, self.Ez)
self.Hx_dev = self.clarray.to_device(queue, self.Hx)
self.Hy_dev = self.clarray.to_device(queue, self.Hy)
self.Hz_dev = self.clarray.to_device(queue, self.Hz)
def htod_dispersive_arrays(self, queue):
"""Initialise dispersive material coefficient arrays on compute device.
Args:
queue: pyopencl queue.
"""
self.updatecoeffsdispersive_dev = self.clarray.to_device(
queue, self.updatecoeffsdispersive
)
# self.updatecoeffsdispersive_dev = self.clarray.to_device(queue, np.ones((95,95,95), dtype=np.float32))
self.Tx_dev = self.clarray.to_device(queue, self.Tx)
self.Ty_dev = self.clarray.to_device(queue, self.Ty)
self.Tz_dev = self.clarray.to_device(queue, self.Tz)
def dispersion_analysis(G):
"""Analysis of numerical dispersion (Taflove et al, 2005, p112) -
worse case of maximum frequency and minimum wavelength
Args:
G: FDTDGrid class describing a grid in a model.
Returns:
results: dict of results from dispersion analysis.
"""
# deltavp: physical phase velocity error (percentage)
# N: grid sampling density
# material: material with maximum permittivity
# maxfreq: maximum significant frequency
# error: error message
results = {"deltavp": None, "N": None, "material": None, "maxfreq": [], "error": ""}
# Find maximum significant frequency
if G.waveforms:
for waveform in G.waveforms:
if waveform.type in ["sine", "contsine"]:
results["maxfreq"].append(4 * waveform.freq)
elif waveform.type == "impulse":
results["error"] = "impulse waveform used."
elif waveform.type == "user":
results["error"] = "user waveform detected."
else:
# Time to analyse waveform - 4*pulse_width as using entire
# time window can result in demanding FFT
waveform.calculate_coefficients()
iterations = round_value(4 * waveform.chi / G.dt)
iterations = min(iterations, G.iterations)
waveformvalues = np.zeros(G.iterations)
for iteration in range(G.iterations):
waveformvalues[iteration] = waveform.calculate_value(
iteration * G.dt, G.dt
)
# Ensure source waveform is not being overly truncated before attempting any FFT
if np.abs(waveformvalues[-1]) < np.abs(np.amax(waveformvalues)) / 100:
# FFT
freqs, power = fft_power(waveformvalues, G.dt)
# Get frequency for max power
freqmaxpower = np.where(np.isclose(power, 0))[0][0]
# Set maximum frequency to a threshold drop from maximum power, ignoring DC value
try:
freqthres = (
np.where(
power[freqmaxpower:]
< -config.get_model_config().numdispersion[
"highestfreqthres"
]
)[0][0]
+ freqmaxpower
)
results["maxfreq"].append(freqs[freqthres])
except ValueError:
results["error"] = (
"unable to calculate maximum power "
+ "from waveform, most likely due to "
+ "undersampling."
)
# Ignore case where someone is using a waveform with zero amplitude, i.e. on a receiver
elif waveform.amp == 0:
pass
# If waveform is truncated don't do any further analysis
else:
results["error"] = (
"waveform does not fit within specified "
+ "time window and is therefore being truncated."
)
else:
results["error"] = "no waveform detected."
if results["maxfreq"]:
results["maxfreq"] = max(results["maxfreq"])
# Find minimum wavelength (material with maximum permittivity)
maxer = 0
matmaxer = ""
for x in G.materials:
if x.se != float("inf"):
er = x.er
# If there are dispersive materials calculate the complex
# relative permittivity at maximum frequency and take the real part
if x.__class__.__name__ == "DispersiveMaterial":
er = x.calculate_er(results["maxfreq"])
er = er.real
if er > maxer:
maxer = er
matmaxer = x.ID
results["material"] = next(x for x in G.materials if x.ID == matmaxer)
# Minimum velocity
minvelocity = config.c / np.sqrt(maxer)
# Minimum wavelength
minwavelength = minvelocity / results["maxfreq"]
# Maximum spatial step
if "3D" in config.get_model_config().mode:
delta = max(G.dx, G.dy, G.dz)
elif "2D" in config.get_model_config().mode:
if G.nx == 1:
delta = max(G.dy, G.dz)
elif G.ny == 1:
delta = max(G.dx, G.dz)
elif G.nz == 1:
delta = max(G.dx, G.dy)
# Courant stability factor
S = (config.c * G.dt) / delta
# Grid sampling density
results["N"] = minwavelength / delta
# Check grid sampling will result in physical wave propagation
if (
int(np.floor(results["N"]))
>= config.get_model_config().numdispersion["mingridsampling"]
):
# Numerical phase velocity
vp = np.pi / (
results["N"] * np.arcsin((1 / S) * np.sin((np.pi * S) / results["N"]))
)
# Physical phase velocity error (percentage)
results["deltavp"] = (((vp * config.c) - config.c) / config.c) * 100
# Store rounded down value of grid sampling density
results["N"] = int(np.floor(results["N"]))
return results

75
gprMax/grid/cuda_grid.py 普通文件
查看文件

@@ -0,0 +1,75 @@
# 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/>.
from importlib import import_module
import numpy as np
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.pml import CUDAPML
class CUDAGrid(FDTDGrid):
"""Additional grid methods for solving on GPU using CUDA."""
def __init__(self):
super().__init__()
self.gpuarray = import_module("pycuda.gpuarray")
# Threads per block - used for main electric/magnetic field updates
self.tpb = (128, 1, 1)
# Blocks per grid - used for main electric/magnetic field updates
self.bpg = None
def _construct_pml(self, pml_ID: str, thickness: int) -> CUDAPML:
return super()._construct_pml(pml_ID, thickness, CUDAPML)
def set_blocks_per_grid(self):
"""Set the blocks per grid size used for updating the electric and
magnetic field arrays on a GPU.
"""
self.bpg = (
int(np.ceil(((self.nx + 1) * (self.ny + 1) * (self.nz + 1)) / self.tpb[0])),
1,
1,
)
def htod_geometry_arrays(self):
"""Initialise an array for cell edge IDs (ID) on compute device."""
self.ID_dev = self.gpuarray.to_gpu(self.ID)
def htod_field_arrays(self):
"""Initialise field arrays on compute device."""
self.Ex_dev = self.gpuarray.to_gpu(self.Ex)
self.Ey_dev = self.gpuarray.to_gpu(self.Ey)
self.Ez_dev = self.gpuarray.to_gpu(self.Ez)
self.Hx_dev = self.gpuarray.to_gpu(self.Hx)
self.Hy_dev = self.gpuarray.to_gpu(self.Hy)
self.Hz_dev = self.gpuarray.to_gpu(self.Hz)
def htod_dispersive_arrays(self):
"""Initialise dispersive material coefficient arrays on compute device."""
self.updatecoeffsdispersive_dev = self.gpuarray.to_gpu(self.updatecoeffsdispersive)
self.Tx_dev = self.gpuarray.to_gpu(self.Tx)
self.Ty_dev = self.gpuarray.to_gpu(self.Ty)
self.Tz_dev = self.gpuarray.to_gpu(self.Tz)

1051
gprMax/grid/fdtd_grid.py 普通文件

文件差异内容过多而无法显示 加载差异

697
gprMax/grid/mpi_grid.py 普通文件
查看文件

@@ -0,0 +1,697 @@
# 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 itertools
import logging
from enum import IntEnum, unique
from typing import List, Optional, Tuple, TypeVar, Union
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from numpy import ndarray
from gprMax import config
from gprMax.cython.pml_build import pml_sum_er_mr
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.pml import MPIPML, PML
from gprMax.receivers import Rx
from gprMax.sources import Source
logger = logging.getLogger(__name__)
CoordType = TypeVar("CoordType", bound=Union[Rx, Source])
@unique
class Dim(IntEnum):
X = 0
Y = 1
Z = 2
@unique
class Dir(IntEnum):
NEG = 0
POS = 1
class MPIGrid(FDTDGrid):
HALO_SIZE = 1
COORDINATOR_RANK = 0
def __init__(self, comm: MPI.Cartcomm):
self.comm = comm
self.x_comm = comm.Sub([False, True, True])
self.y_comm = comm.Sub([True, False, True])
self.z_comm = comm.Sub([True, True, False])
self.pml_comm = MPI.COMM_NULL
self.mpi_tasks = np.array(self.comm.dims, dtype=np.int32)
self.lower_extent = np.zeros(3, dtype=np.int32)
self.upper_extent = np.zeros(3, dtype=np.int32)
self.negative_halo_offset = np.zeros(3, dtype=np.bool_)
self.global_size = np.zeros(3, dtype=np.int32)
self.neighbours = np.full((3, 2), -1, dtype=int)
self.neighbours[Dim.X] = self.comm.Shift(direction=Dim.X, disp=1)
self.neighbours[Dim.Y] = self.comm.Shift(direction=Dim.Y, disp=1)
self.neighbours[Dim.Z] = self.comm.Shift(direction=Dim.Z, disp=1)
self.send_halo_map = np.empty((3, 2), dtype=MPI.Datatype)
self.recv_halo_map = np.empty((3, 2), dtype=MPI.Datatype)
super().__init__()
@property
def rank(self) -> int:
return self.comm.Get_rank()
@property
def coords(self) -> List[int]:
return self.comm.coords
@property
def gx(self) -> int:
return self.global_size[Dim.X]
@gx.setter
def gx(self, value: int):
self.global_size[Dim.X] = value
@property
def gy(self) -> int:
return self.global_size[Dim.Y]
@gy.setter
def gy(self, value: int):
self.global_size[Dim.Y] = value
@property
def gz(self) -> int:
return self.global_size[Dim.Z]
@gz.setter
def gz(self, value: int):
self.global_size[Dim.Z] = value
def set_pml_thickness(self, thickness: Union[int, Tuple[int, int, int, int, int, int]]):
super().set_pml_thickness(thickness)
# Set PML thickness to zero if not at the edge of the domain
if self.has_neighbour(Dim.X, Dir.NEG):
self.pmls["thickness"]["x0"] = 0
if self.has_neighbour(Dim.X, Dir.POS):
self.pmls["thickness"]["xmax"] = 0
if self.has_neighbour(Dim.Y, Dir.NEG):
self.pmls["thickness"]["y0"] = 0
if self.has_neighbour(Dim.Y, Dir.POS):
self.pmls["thickness"]["ymax"] = 0
if self.has_neighbour(Dim.Z, Dir.NEG):
self.pmls["thickness"]["z0"] = 0
if self.has_neighbour(Dim.Z, Dir.POS):
self.pmls["thickness"]["zmax"] = 0
def is_coordinator(self) -> bool:
"""Test if the current rank is the coordinator.
Returns:
is_coordinator: True if `self.rank` equals
`self.COORDINATOR_RANK`.
"""
return self.rank == self.COORDINATOR_RANK
def create_sub_communicator(
self, local_start: npt.NDArray[np.int32], local_stop: npt.NDArray[np.int32]
) -> Optional[MPI.Cartcomm]:
if self.local_bounds_overlap_grid(local_start, local_stop):
comm = self.comm.Split()
assert isinstance(comm, MPI.Intracomm)
start_grid_coord = self.get_grid_coord_from_local_coordinate(local_start)
# Subtract 1 from local_stop as the upper extent is
# exclusive meaning the last coordinate included in the sub
# communicator is actually (local_stop - 1).
stop_grid_coord = self.get_grid_coord_from_local_coordinate(local_stop - 1) + 1
comm = comm.Create_cart((stop_grid_coord - start_grid_coord).tolist())
return comm
else:
self.comm.Split(MPI.UNDEFINED)
return None
def get_grid_coord_from_local_coordinate(
self, local_coord: npt.NDArray[np.int32]
) -> npt.NDArray[np.int32]:
"""Get the MPI grid coordinate for a local grid coordinate.
Args:
local_coord: Local grid coordinate.
Returns:
grid_coord: Coordinate of the MPI rank containing the local
grid coordinate.
"""
coord = self.local_to_global_coordinate(local_coord)
return self.get_grid_coord_from_coordinate(coord)
def get_grid_coord_from_coordinate(self, coord: npt.NDArray[np.int32]) -> npt.NDArray[np.int32]:
"""Get the MPI grid coordinate for a global grid coordinate.
Args:
coord: Global grid coordinate.
Returns:
grid_coord: Coordinate of the MPI rank containing the global
grid coordinate.
"""
step_size = self.global_size // self.mpi_tasks
overflow = self.global_size % self.mpi_tasks
# The first n MPI ranks where n is the overflow, will have size
# step_size + 1. Additionally, step_size may be zero in some
# dimensions (e.g. in the 2D case) so we need to avoid division
# by zero.
return np.where(
(step_size + 1) * overflow >= coord,
coord // (step_size + 1),
np.minimum((coord - overflow) // np.maximum(step_size, 1), self.mpi_tasks - 1),
)
def get_rank_from_coordinate(self, coord: npt.NDArray) -> int:
"""Get the MPI rank for a global grid coordinate.
A coordinate only exists on a single rank (halos are ignored).
Args:
coord: Global grid coordinate.
Returns:
rank: MPI rank containing the global grid coordinate.
"""
grid_coord = self.get_grid_coord_from_coordinate(coord)
return self.comm.Get_cart_rank(grid_coord.tolist())
def get_ranks_between_coordinates(
self, start_coord: npt.NDArray, stop_coord: npt.NDArray
) -> List[int]:
"""Get the MPI ranks for between two global grid coordinates.
`stop_coord` must not be less than `start_coord` in any
dimension, however it can be equal. The returned ranks will
contain coordinates inclusive of both `start_coord` and
`stop_coord`.
Args:
start_coord: Starting global grid coordinate.
stop_coord: End global grid coordinate.
Returns:
ranks: List of MPI ranks
"""
start = self.get_grid_coord_from_coordinate(start_coord)
stop = self.get_grid_coord_from_coordinate(stop_coord) + 1
coord_to_rank = lambda c: self.comm.Get_cart_rank((start + c).tolist())
return [coord_to_rank(coord) for coord in np.ndindex(*(stop - start))]
def global_to_local_coordinate(
self, global_coord: npt.NDArray[np.int32]
) -> npt.NDArray[np.int32]:
"""Convert a global grid coordinate to a local grid coordinate.
The returned coordinate will be relative to the current MPI
rank's local grid. It may be negative, or greater than the size
of the local grid if the point lies outside the local grid.
Args:
global_coord: Global grid coordinate.
Returns:
local_coord: Local grid coordinate.
"""
return global_coord - self.lower_extent
def local_to_global_coordinate(
self, local_coord: npt.NDArray[np.int32]
) -> npt.NDArray[np.int32]:
"""Convert a local grid coordinate to a global grid coordinate.
Args:
local_coord: Local grid coordinate.
Returns:
global_coord: Global grid coordinate.
"""
return local_coord + self.lower_extent
def global_coord_inside_grid(
self, global_coord: npt.NDArray[np.int32], allow_inside_halo: bool = False
) -> bool:
"""Check if a global coordinate falls with in the local grid.
Args:
global_coord: Global grid coordinate.
allow_inside_halo: If True, the function returns True when
the coordinate is inside the grid halo. Otherwise, it
will return False when the coordinate is inside the grid
halo. (defaults to False)
Returns:
is_inside_grid: True if the global coordinate falls inside
the local grid bounds.
"""
if allow_inside_halo:
lower_bound = self.lower_extent
upper_bound = self.upper_extent + 1
else:
lower_bound = self.lower_extent + self.negative_halo_offset
upper_bound = self.upper_extent
return all(global_coord >= lower_bound) and all(global_coord <= upper_bound)
def local_bounds_overlap_grid(
self, local_start: npt.NDArray[np.int32], local_stop: npt.NDArray[np.int32]
) -> bool:
"""Check if local bounds overlap with the grid.
The bounds overlap if any of the 3D box as defined by the lower
and upper bounds overlaps with the local grid (excluding the
halo).
Args:
local_start: Lower bound in the local grid coordinate space.
local_stop: Upper bound in the local grid coordinate space.
Returns:
overlaps_grid: True if the box generated by the lower and
upper bound overlaps with the local grid.
"""
return all(local_start < self.size) and all(local_stop > self.negative_halo_offset)
def gather_coord_objects(self, objects: List[CoordType]) -> List[CoordType]:
"""Gather coord objects on the coordinator MPI rank.
The sending MPI rank converts the object locations to the global
grid. The coord objects (sources and receivers) are all sent to
the coordinatoor rank.
Args:
objects: Coord objects to be gathered.
Returns:
gathered_objects: List of gathered coord objects if the
current rank is the coordinator. Otherwise, the original
list of objects is returned.
"""
for o in objects:
o.coord = self.local_to_global_coordinate(o.coord)
gathered_objects: Optional[List[List[CoordType]]] = self.comm.gather(
objects, root=self.COORDINATOR_RANK
)
if gathered_objects is not None:
return list(itertools.chain(*gathered_objects))
else:
return objects
def gather_grid_objects(self):
"""Gather sources and receivers."""
self.rxs = self.gather_coord_objects(self.rxs)
self.voltagesources = self.gather_coord_objects(self.voltagesources)
self.magneticdipoles = self.gather_coord_objects(self.magneticdipoles)
self.hertziandipoles = self.gather_coord_objects(self.hertziandipoles)
self.transmissionlines = self.gather_coord_objects(self.transmissionlines)
def _halo_swap(self, array: ndarray, dim: Dim, dir: Dir):
"""Perform a halo swap in the specifed dimension and direction.
If no neighbour exists for the current rank in the specifed
dimension and direction, the halo swap is skipped.
Args:
array: Array to perform the halo swap with.
dim: Dimension of halo to swap.
dir: Direction of halo to swap.
"""
neighbour = self.neighbours[dim][dir]
if neighbour != -1:
self.comm.Sendrecv(
[array, self.send_halo_map[dim][dir]],
neighbour,
0,
[array, self.recv_halo_map[dim][dir]],
neighbour,
0,
None,
)
def _halo_swap_by_dimension(self, array: ndarray, dim: Dim):
"""Perform halo swaps in the specifed dimension.
Perform a halo swaps in the positive and negative direction for
the specified dimension. The order of the swaps is determined by
the current rank's MPI grid coordinate to prevent deadlock.
Args:
array: Array to perform the halo swaps with.
dim: Dimension of halos to swap.
"""
if self.coords[dim] % 2 == 0:
self._halo_swap(array, dim, Dir.NEG)
self._halo_swap(array, dim, Dir.POS)
else:
self._halo_swap(array, dim, Dir.POS)
self._halo_swap(array, dim, Dir.NEG)
def _halo_swap_array(self, array: ndarray):
"""Perform halo swaps for the specified array.
Args:
array: Array to perform the halo swaps with.
"""
self._halo_swap_by_dimension(array, Dim.X)
self._halo_swap_by_dimension(array, Dim.Y)
self._halo_swap_by_dimension(array, Dim.Z)
def halo_swap_electric(self):
"""Perform halo swaps for electric field arrays."""
self._halo_swap_array(self.Ex)
self._halo_swap_array(self.Ey)
self._halo_swap_array(self.Ez)
def halo_swap_magnetic(self):
"""Perform halo swaps for magnetic field arrays."""
self._halo_swap_array(self.Hx)
self._halo_swap_array(self.Hy)
self._halo_swap_array(self.Hz)
def _construct_pml(self, pml_ID: str, thickness: int) -> MPIPML:
"""Build instance of MPIPML and set the MPI communicator.
Args:
pml_ID: Identifier of PML slab.
thickness: Thickness of PML slab in cells.
"""
pml = super()._construct_pml(pml_ID, thickness, MPIPML)
if pml.ID[0] == "x":
pml.comm = self.x_comm
elif pml.ID[0] == "y":
pml.comm = self.y_comm
elif pml.ID[0] == "z":
pml.comm = self.z_comm
pml.global_comm = self.pml_comm
return pml
def _calculate_average_pml_material_properties(self, pml: MPIPML) -> Tuple[float, float]:
"""Calculate average material properties for the provided PML.
Args:
pml: PML to calculate the properties of.
Returns:
averageer, averagemr: Average permittivity and permeability
in the PML slab.
"""
# Arrays to hold values of permittivity and permeability (avoids
# accessing Material class in Cython.)
ers = np.zeros(len(self.materials))
mrs = np.zeros(len(self.materials))
for i, m in enumerate(self.materials):
ers[i] = m.er
mrs[i] = m.mr
# Need to account for the negative halo (remove it) to avoid
# double counting. The solid array does not have a positive halo
# so we don't need to consider that.
if pml.ID[0] == "x":
o1 = self.negative_halo_offset[1]
o2 = self.negative_halo_offset[2]
n1 = self.ny - o1
n2 = self.nz - o2
solid = self.solid[pml.xs, o1:, o2:]
elif pml.ID[0] == "y":
o1 = self.negative_halo_offset[0]
o2 = self.negative_halo_offset[2]
n1 = self.nx - o1
n2 = self.nz - o2
solid = self.solid[o1:, pml.ys, o2:]
elif pml.ID[0] == "z":
o1 = self.negative_halo_offset[0]
o2 = self.negative_halo_offset[1]
n1 = self.nx - o1
n2 = self.ny - o2
solid = self.solid[o1:, o2:, pml.zs]
else:
raise ValueError(f"Unknown PML ID '{pml.ID}'")
sumer, summr = pml_sum_er_mr(n1, n2, config.get_model_config().ompthreads, solid, ers, mrs)
n = pml.comm.allreduce(n1 * n2, MPI.SUM)
sumer = pml.comm.allreduce(sumer, MPI.SUM)
summr = pml.comm.allreduce(summr, MPI.SUM)
averageer = sumer / n
averagemr = summr / n
return averageer, averagemr
def build(self):
"""Set local properties and objects, then build the grid."""
if any(self.global_size + 1 < self.mpi_tasks):
logger.error(
f"Too many MPI tasks requested ({self.mpi_tasks}) for grid of size"
f" {self.global_size + 1}. Make sure the number of MPI tasks in each dimension is"
" less than the size of the grid."
)
raise ValueError
self.set_halo_map()
# TODO: Check PML is not thicker than the grid size
# Get PMLs present in this grid
pmls = [
PML.boundaryIDs.index(key) for key, value in self.pmls["thickness"].items() if value > 0
]
if len(pmls) > 0:
# Use PML ID as the key to ensure rank 0 is always the same
# PML. This is needed to ensure the CFS sigma.max parameter
# is calculated using the first PML present.
self.pml_comm = self.comm.Split(0, pmls[0])
else:
self.pml_comm = self.comm.Split(MPI.UNDEFINED)
super().build()
def update_sources_and_recievers(self):
"""Update position of sources and receivers.
If any sources or receivers have stepped outside of the local
grid, they will be moved to the correct MPI rank.
"""
super().update_sources_and_recievers()
# Check it is possible for sources and receivers to have moved
model_num = config.sim_config.current_model
if (all(self.srcsteps == 0) and all(self.rxsteps == 0)) or model_num == 0:
return
# Get items that are outside the local bounds of the grid
items_to_send = list(
itertools.filterfalse(
lambda x: self.within_bounds(x.coord),
itertools.chain(
self.voltagesources,
self.hertziandipoles,
self.magneticdipoles,
self.transmissionlines,
self.discreteplanewaves,
self.rxs,
),
)
)
# Map items being sent to the global coordinate space
for item in items_to_send:
item.coord = self.local_to_global_coordinate(item.coord)
send_count_by_rank = np.zeros(self.comm.size, dtype=np.int32)
# Send items to correct rank
for rank, items in itertools.groupby(
items_to_send, lambda x: self.get_rank_from_coordinate(x.coord)
):
self.comm.isend(list(items), rank)
send_count_by_rank[rank] += 1
# Communicate the number of messages sent to each rank
if self.is_coordinator():
self.comm.Reduce(MPI.IN_PLACE, [send_count_by_rank, MPI.INT32_T], op=MPI.SUM)
else:
self.comm.Reduce([send_count_by_rank, MPI.INT32_T], None, op=MPI.SUM)
# Get number of messages this rank will receive
messages_to_receive = np.zeros(1, dtype=np.int32)
if self.is_coordinator():
self.comm.Scatter([send_count_by_rank, MPI.INT32_T], [messages_to_receive, MPI.INT32_T])
else:
self.comm.Scatter(None, [messages_to_receive, MPI.INT32_T])
# Receive new items for this rank
for _ in range(messages_to_receive[0]):
new_items = self.comm.recv(None, MPI.ANY_SOURCE)
for item in new_items:
item.coord = self.global_to_local_coordinate(item.coord)
if isinstance(item, Rx):
self.add_receiver(item)
else:
self.add_source(item)
# If this rank sent any items, remove them from our source and
# receiver lists
if len(items_to_send) > 0:
# Map items sent back to the local coordinate space
for item in items_to_send:
item.coord = self.global_to_local_coordinate(item.coord)
filter_items = lambda items: list(
filter(lambda item: self.within_bounds(item.coord), items)
)
self.voltagesources = filter_items(self.voltagesources)
self.hertziandipoles = filter_items(self.hertziandipoles)
self.magneticdipoles = filter_items(self.magneticdipoles)
self.transmissionlines = filter_items(self.transmissionlines)
self.discreteplanewaves = filter_items(self.discreteplanewaves)
self.rxs = filter_items(self.rxs)
def has_neighbour(self, dim: Dim, dir: Dir) -> bool:
"""Test if the current rank has a specified neighbour.
Args:
dim: Dimension of neighbour.
dir: Direction of neighbour.
Returns:
has_neighbour: True if the current rank has a neighbour in
the specified dimension and direction.
"""
return self.neighbours[dim][dir] != -1
def set_halo_map(self):
"""Create MPI DataTypes for field array halo exchanges."""
size = (self.size + 1).tolist()
for dim in Dim:
halo_size = (self.size + 1 - np.sum(self.neighbours >= 0, axis=1)).tolist()
halo_size[dim] = 1
start = [1 if self.has_neighbour(dim, Dir.NEG) else 0 for dim in Dim]
if self.has_neighbour(dim, Dir.NEG):
start[dim] = 1
self.send_halo_map[dim][Dir.NEG] = MPI.FLOAT.Create_subarray(size, halo_size, start)
start[dim] = 0
self.recv_halo_map[dim][Dir.NEG] = MPI.FLOAT.Create_subarray(size, halo_size, start)
self.send_halo_map[dim][Dir.NEG].Commit()
self.recv_halo_map[dim][Dir.NEG].Commit()
if self.has_neighbour(dim, Dir.POS):
start[dim] = size[dim] - 2
self.send_halo_map[dim][Dir.POS] = MPI.FLOAT.Create_subarray(size, halo_size, start)
start[dim] = size[dim] - 1
self.recv_halo_map[dim][Dir.POS] = MPI.FLOAT.Create_subarray(size, halo_size, start)
self.send_halo_map[dim][Dir.POS].Commit()
self.recv_halo_map[dim][Dir.POS].Commit()
def calculate_local_extents(self):
"""Calculate size and extents of the local grid"""
self.size = self.global_size // self.mpi_tasks
overflow = self.global_size % self.mpi_tasks
# Ranks with coordinates less than the overflow have their size
# increased by one. Account for this by adding the overflow or
# this rank's coordinates, whichever is smaller.
self.lower_extent = self.size * self.coords + np.minimum(self.coords, overflow)
# For each coordinate, if it is less than the overflow, add 1
self.size += self.coords < overflow
# Account for a negative halo
# Field arrays are created with dimensions size + 1 so space for
# a positive halo will always exist. Grids not needing a
# positive halo, still need the extra size as that makes the
# global grid on the whole one larger than the user dimensions.
self.negative_halo_offset = self.neighbours[:, 0] >= 0
self.size += self.negative_halo_offset
self.lower_extent -= self.negative_halo_offset
self.upper_extent = self.lower_extent + self.size
logger.debug(
f"Global grid size: {self.global_size}, Local grid size: {self.size}, Lower extent:"
f" {self.lower_extent}, Upper extent: {self.upper_extent}"
)
def within_bounds(self, local_point: npt.NDArray[np.int32]) -> bool:
"""Check a local point is within the grid.
Args:
local_point: Point to check.
Returns:
within_bounds: True if the point is within the local grid
(i.e. this rank's grid). False otherwise.
Raises:
ValueError: Raised if the point is outside the global grid.
"""
gx, gy, gz = self.local_to_global_coordinate(local_point)
if gx < 0 or gx > self.gx:
raise ValueError("x")
if gy < 0 or gy > self.gy:
raise ValueError("y")
if gz < 0 or gz > self.gz:
raise ValueError("z")
return all(local_point >= self.negative_halo_offset) and all(local_point < self.size)
def within_pml(self, local_point: npt.NDArray[np.int32]) -> bool:
"""Check if the provided point is within a PML.
Args:
local_point: Point to check. This must use this grid's
coordinate system.
Returns:
within_pml: True if the point is within a PML.
"""
# within_pml check will only be valid if the point is also
# within the local grid
return (
super().within_pml(local_point)
and all(local_point >= self.negative_halo_offset)
and all(local_point <= self.size)
)

70
gprMax/grid/opencl_grid.py 普通文件
查看文件

@@ -0,0 +1,70 @@
# 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/>.
from importlib import import_module
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.pml import PML, OpenCLPML
class OpenCLGrid(FDTDGrid):
"""Additional grid methods for solving on compute device using OpenCL."""
def __init__(self):
super().__init__()
self.clarray = import_module("pyopencl.array")
def _construct_pml(self, pml_ID: str, thickness: int) -> OpenCLPML:
return super()._construct_pml(pml_ID, thickness, OpenCLPML)
def htod_geometry_arrays(self, queue):
"""Initialise an array for cell edge IDs (ID) on compute device.
Args:
queue: pyopencl queue.
"""
self.ID_dev = self.clarray.to_device(queue, self.ID)
def htod_field_arrays(self, queue):
"""Initialise field arrays on compute device.
Args:
queue: pyopencl queue.
"""
self.Ex_dev = self.clarray.to_device(queue, self.Ex)
self.Ey_dev = self.clarray.to_device(queue, self.Ey)
self.Ez_dev = self.clarray.to_device(queue, self.Ez)
self.Hx_dev = self.clarray.to_device(queue, self.Hx)
self.Hy_dev = self.clarray.to_device(queue, self.Hy)
self.Hz_dev = self.clarray.to_device(queue, self.Hz)
def htod_dispersive_arrays(self, queue):
"""Initialise dispersive material coefficient arrays on compute device.
Args:
queue: pyopencl queue.
"""
self.updatecoeffsdispersive_dev = self.clarray.to_device(queue, self.updatecoeffsdispersive)
# self.updatecoeffsdispersive_dev = self.clarray.to_device(queue, np.ones((95,95,95), dtype=np.float32))
self.Tx_dev = self.clarray.to_device(queue, self.Tx)
self.Ty_dev = self.clarray.to_device(queue, self.Ty)
self.Tz_dev = self.clarray.to_device(queue, self.Tz)

查看文件

@@ -49,9 +49,7 @@ def process_python_include_code(inputfile, usernamespace):
# Strip out any newline characters and comments that must begin with double hashes
inputlines = [
line.rstrip()
for line in inputfile
if (not line.startswith("##") and line.rstrip("\n"))
line.rstrip() for line in inputfile if (not line.startswith("##") and line.rstrip("\n"))
]
# Rewind input file in preparation for any subsequent reading function
@@ -152,9 +150,7 @@ def process_include_files(hashcmds):
# See if file exists at specified path and if not try input file directory
includefile = Path(includefile)
if not includefile.exists():
includefile = Path(
config.sim_config.input_file_path.parent, includefile
)
includefile = Path(config.sim_config.input_file_path.parent, includefile)
with open(includefile, "r") as f:
# Strip out any newline characters and comments that must begin with double hashes
@@ -289,6 +285,15 @@ def check_cmd_names(processedlines, checkessential=True):
lindex = 0
while lindex < len(processedlines):
cmd = processedlines[lindex].split(":")
# Check the command name and parameters were both found
if len(cmd) < 2:
logger.error(
f"Unable to identify command and parameters in '{processedlines[lindex].strip()}'."
" There must be a colon ':' between the command name and parameters."
)
exit(1)
cmdname = cmd[0]
cmdparams = cmd[1]
@@ -323,9 +328,7 @@ def check_cmd_names(processedlines, checkessential=True):
singlecmds[cmdname] = cmd[1].strip(" \t\n")
else:
logger.exception(
"You can only have a single instance of "
+ cmdname
+ " in your model"
"You can only have a single instance of " + cmdname + " in your model"
)
raise SyntaxError
@@ -399,8 +402,7 @@ def parse_hash_commands(scene):
if key != "__builtins__":
uservars += f"{key}: {value}, "
logger.info(
f"Constants/variables used/available for Python scripting: "
+ f"{{{uservars[:-2]}}}\n"
f"Constants/variables used/available for Python scripting: " + f"{{{uservars[:-2]}}}\n"
)
# Write a file containing the input commands after Python or include

查看文件

@@ -20,19 +20,19 @@ 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.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.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__)
@@ -56,12 +56,10 @@ 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"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires exactly five parameters")
raise ValueError
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
@@ -71,9 +69,7 @@ def process_geometrycmds(geometry):
elif tmp[0] == "#edge:":
if len(tmp) != 8:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires exactly seven parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires exactly seven parameters")
raise ValueError
edge = Edge(
@@ -86,9 +82,7 @@ def process_geometrycmds(geometry):
elif tmp[0] == "#plate:":
if len(tmp) < 8:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least seven parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least seven parameters")
raise ValueError
# Isotropic case
@@ -108,18 +102,14 @@ def process_geometrycmds(geometry):
)
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(plate)
elif tmp[0] == "#triangle:":
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]))
@@ -129,9 +119,7 @@ def process_geometrycmds(geometry):
# Isotropic case with no user specified averaging
if len(tmp) == 12:
triangle = Triangle(
p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11]
)
triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_id=tmp[11])
# Isotropic case with user specified averaging
elif len(tmp) == 13:
@@ -141,28 +129,22 @@ def process_geometrycmds(geometry):
p3=p3,
thickness=thickness,
material_id=tmp[11],
averaging=tmp[12].lower(),
averaging=tmp[12],
)
# Uniaxial anisotropic case
elif len(tmp) == 14:
triangle = Triangle(
p1=p1, p2=p2, p3=p3, thickness=thickness, material_ids=tmp[11:]
)
triangle = Triangle(p1=p1, p2=p2, p3=p3, thickness=thickness, material_ids=tmp[11:])
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(triangle)
elif tmp[0] == "#box:":
if len(tmp) < 8:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least seven parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least seven parameters")
raise ValueError
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
@@ -174,25 +156,21 @@ def process_geometrycmds(geometry):
# Isotropic case with user specified averaging
elif len(tmp) == 9:
box = Box(p1=p1, p2=p2, material_id=tmp[7], averaging=tmp[8].lower())
box = Box(p1=p1, p2=p2, material_id=tmp[7], averaging=tmp[8])
# Uniaxial anisotropic case
elif len(tmp) == 10:
box = Box(p1=p1, p2=p2, material_ids=tmp[7:])
else:
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(box)
elif tmp[0] == "#cylinder:":
if len(tmp) < 9:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least eight parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least eight parameters")
raise ValueError
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
@@ -205,27 +183,21 @@ def process_geometrycmds(geometry):
# Isotropic case with user specified averaging
elif len(tmp) == 10:
cylinder = Cylinder(
p1=p1, p2=p2, r=r, material_id=tmp[8], averaging=tmp[9].lower()
)
cylinder = Cylinder(p1=p1, p2=p2, r=r, material_id=tmp[8], averaging=tmp[9])
# Uniaxial anisotropic case
elif len(tmp) == 11:
cylinder = Cylinder(p1=p1, p2=p2, r=r, material_ids=tmp[8:])
else:
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(cylinder)
elif tmp[0] == "#cone:":
if len(tmp) < 10:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least nine parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least nine parameters")
raise ValueError
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
@@ -245,7 +217,7 @@ def process_geometrycmds(geometry):
r1=r1,
r2=r2,
material_id=tmp[9],
averaging=tmp[10].lower(),
averaging=tmp[10],
)
# Uniaxial anisotropic case
@@ -253,18 +225,14 @@ def process_geometrycmds(geometry):
cone = Cone(p1=p1, p2=p2, r1=r1, r2=r2, material_ids=tmp[9:])
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(cone)
elif tmp[0] == "#cylindrical_sector:":
if len(tmp) < 10:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least nine parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least nine parameters")
raise ValueError
normal = tmp[1].lower()
@@ -301,7 +269,7 @@ def process_geometrycmds(geometry):
r=r,
start=start,
end=end,
averaging=tmp[10].lower(),
averaging=tmp[10],
material_id=tmp[9],
)
@@ -320,18 +288,14 @@ def process_geometrycmds(geometry):
)
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(cylindrical_sector)
elif tmp[0] == "#sphere:":
if len(tmp) < 6:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least five parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least five parameters")
raise ValueError
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
@@ -343,27 +307,21 @@ def process_geometrycmds(geometry):
# Isotropic case with user specified averaging
elif len(tmp) == 7:
sphere = Sphere(
p1=p1, r=r, material_id=tmp[5], averaging=tmp[6].lower()
)
sphere = Sphere(p1=p1, r=r, material_id=tmp[5], averaging=tmp[6])
# Uniaxial anisotropic case
elif len(tmp) == 8:
sphere = Sphere(p1=p1, r=r, material_id=tmp[5:])
else:
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(sphere)
elif tmp[0] == "#ellipsoid:":
if len(tmp) < 8:
logger.exception(
"'" + " ".join(tmp) + "'" + " requires at least seven parameters"
)
logger.exception("'" + " ".join(tmp) + "'" + " requires at least seven parameters")
raise ValueError
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
@@ -383,7 +341,7 @@ def process_geometrycmds(geometry):
yr=yr,
zr=zr,
material_id=tmp[7],
averaging=tmp[8].lower(),
averaging=tmp[8],
)
# Uniaxial anisotropic case
@@ -391,9 +349,7 @@ def process_geometrycmds(geometry):
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr, material_id=tmp[7:])
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(ellipsoid)
@@ -446,12 +402,10 @@ def process_geometrycmds(geometry):
mixing_model_id=mixing_model_id,
id=ID,
seed=tmp[14],
averaging=tmp[15].lower(),
averaging=tmp[15],
)
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(fb)
@@ -463,10 +417,7 @@ def process_geometrycmds(geometry):
if tmp[0] == "#add_surface_roughness:":
if len(tmp) < 13:
logger.exception(
"'"
+ " ".join(tmp)
+ "'"
+ " requires at least twelve parameters"
"'" + " ".join(tmp) + "'" + " requires at least twelve parameters"
)
raise ValueError
@@ -498,10 +449,7 @@ def process_geometrycmds(geometry):
)
else:
logger.exception(
"'"
+ " ".join(tmp)
+ "'"
+ " too many parameters have been given"
"'" + " ".join(tmp) + "'" + " too many parameters have been given"
)
raise ValueError
@@ -510,10 +458,7 @@ def process_geometrycmds(geometry):
if tmp[0] == "#add_surface_water:":
if len(tmp) != 9:
logger.exception(
"'"
+ " ".join(tmp)
+ "'"
+ " requires exactly eight parameters"
"'" + " ".join(tmp) + "'" + " requires exactly eight parameters"
)
raise ValueError
@@ -522,18 +467,13 @@ def process_geometrycmds(geometry):
depth = float(tmp[7])
fractal_box_id = tmp[8]
asf = AddSurfaceWater(
p1=p1, p2=p2, depth=depth, fractal_box_id=fractal_box_id
)
asf = AddSurfaceWater(p1=p1, p2=p2, depth=depth, fractal_box_id=fractal_box_id)
scene_objects.append(asf)
if tmp[0] == "#add_grass:":
if len(tmp) < 12:
logger.exception(
"'"
+ " ".join(tmp)
+ "'"
+ " requires at least eleven parameters"
"'" + " ".join(tmp) + "'" + " requires at least eleven parameters"
)
raise ValueError
@@ -565,10 +505,7 @@ def process_geometrycmds(geometry):
)
else:
logger.exception(
"'"
+ " ".join(tmp)
+ "'"
+ " too many parameters have been given"
"'" + " ".join(tmp) + "'" + " too many parameters have been given"
)
raise ValueError

查看文件

@@ -18,14 +18,13 @@
import logging
from .cmds_multiuse import (
from .user_objects.cmds_multiuse import (
PMLCFS,
AddDebyeDispersion,
AddDrudeDispersion,
AddLorentzDispersion,
DiscretePlaneWave,
ExcitationFile,
GeometryObjectsWrite,
GeometryView,
HertzianDipole,
MagneticDipole,
Material,
@@ -33,13 +32,12 @@ from .cmds_multiuse import (
MaterialRange,
Rx,
RxArray,
Snapshot,
SoilPeplinski,
TransmissionLine,
DiscretePlaneWave,
VoltageSource,
Waveform,
)
from .user_objects.cmds_output import GeometryObjectsWrite, GeometryView, Snapshot
logger = logging.getLogger(__name__)
@@ -63,18 +61,11 @@ def process_multicmds(multicmds):
tmp = cmdinstance.split()
if len(tmp) != 4:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires exactly four parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires exactly four parameters"
)
raise ValueError
waveform = Waveform(
wave_type=tmp[0], amp=float(tmp[1]), freq=float(tmp[2]), id=tmp[3]
)
waveform = Waveform(wave_type=tmp[0], amp=float(tmp[1]), freq=float(tmp[2]), id=tmp[3])
scene_objects.append(waveform)
cmdname = "#voltage_source"
@@ -95,16 +86,11 @@ def process_multicmds(multicmds):
resistance=float(tmp[4]),
waveform_id=tmp[5],
start=float(tmp[6]),
end=float(tmp[7]),
stop=float(tmp[7]),
)
else:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires at least six parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires at least six parameters"
)
raise ValueError
@@ -136,7 +122,7 @@ def process_multicmds(multicmds):
p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])),
waveform_id=tmp[4],
start=float(tmp[5]),
end=float(tmp[6]),
stop=float(tmp[6]),
)
else:
logger.exception(
@@ -172,7 +158,7 @@ def process_multicmds(multicmds):
p1=(float(tmp[1]), float(tmp[2]), float(tmp[3])),
waveform_id=tmp[4],
start=float(tmp[5]),
end=float(tmp[6]),
stop=float(tmp[6]),
)
else:
logger.exception(
@@ -188,12 +174,7 @@ def process_multicmds(multicmds):
tmp = cmdinstance.split()
if len(tmp) < 6:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires at least six parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires at least six parameters"
)
raise ValueError
@@ -211,7 +192,7 @@ def process_multicmds(multicmds):
resistance=float(tmp[4]),
waveform_id=tmp[5],
start=tmp[6],
end=tmp[7],
stop=tmp[7],
)
else:
logger.exception(
@@ -227,12 +208,7 @@ def process_multicmds(multicmds):
tmp = cmdinstance.split()
if len(tmp) < 10:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires at least ten parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires at least ten parameters"
)
raise ValueError
@@ -284,9 +260,7 @@ def process_multicmds(multicmds):
raise ValueError
if len(tmp) > 1:
ex_file = ExcitationFile(
filepath=tmp[0], kind=tmp[1], fill_value=tmp[2]
)
ex_file = ExcitationFile(filepath=tmp[0], kind=tmp[1], fill_value=tmp[2])
else:
ex_file = ExcitationFile(filepath=tmp[0])
@@ -312,7 +286,7 @@ def process_multicmds(multicmds):
rx = Rx(
p1=(float(tmp[0]), float(tmp[1]), float(tmp[2])),
id=tmp[3],
outputs=[" ".join(tmp[4:])],
outputs=tmp[4:],
)
scene_objects.append(rx)
@@ -323,12 +297,7 @@ def process_multicmds(multicmds):
tmp = cmdinstance.split()
if len(tmp) != 9:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires exactly nine parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires exactly nine parameters"
)
raise ValueError
@@ -358,16 +327,19 @@ def process_multicmds(multicmds):
p2 = (float(tmp[3]), float(tmp[4]), float(tmp[5]))
dl = (float(tmp[6]), float(tmp[7]), float(tmp[8]))
filename = tmp[10]
fileext = "." + filename.split(".")[-1]
try:
iterations = int(tmp[9])
snapshot = Snapshot(
p1=p1, p2=p2, dl=dl, iterations=iterations, filename=filename
p1=p1, p2=p2, dl=dl, iterations=iterations, filename=filename, fileext=fileext
)
except ValueError:
time = float(tmp[9])
snapshot = Snapshot(p1=p1, p2=p2, dl=dl, time=time, filename=filename)
snapshot = Snapshot(
p1=p1, p2=p2, dl=dl, time=time, filename=filename, fileext=fileext
)
scene_objects.append(snapshot)
@@ -377,21 +349,12 @@ def process_multicmds(multicmds):
tmp = cmdinstance.split()
if len(tmp) != 5:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires exactly five parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires exactly five parameters"
)
raise ValueError
material = Material(
er=float(tmp[0]),
se=float(tmp[1]),
mr=float(tmp[2]),
sm=float(tmp[3]),
id=tmp[4],
er=float(tmp[0]), se=float(tmp[1]), mr=float(tmp[2]), sm=float(tmp[3]), id=tmp[4]
)
scene_objects.append(material)
@@ -536,9 +499,7 @@ def process_multicmds(multicmds):
p2 = float(tmp[3]), float(tmp[4]), float(tmp[5])
dl = float(tmp[6]), float(tmp[7]), float(tmp[8])
geometry_view = GeometryView(
p1=p1, p2=p2, dl=dl, filename=tmp[9], output_type=tmp[10]
)
geometry_view = GeometryView(p1=p1, p2=p2, dl=dl, filename=tmp[9], output_type=tmp[10])
scene_objects.append(geometry_view)
cmdname = "#geometry_objects_write"
@@ -596,12 +557,7 @@ def process_multicmds(multicmds):
if len(tmp) < 2:
logger.exception(
"'"
+ cmdname
+ ": "
+ " ".join(tmp)
+ "'"
+ " requires at least two parameters"
"'" + cmdname + ": " + " ".join(tmp) + "'" + " requires at least two parameters"
)
raise ValueError

查看文件

@@ -18,7 +18,7 @@
import logging
from .cmds_singleuse import (
from .user_objects.cmds_singleuse import (
Discretisation,
Domain,
OMPThreads,

查看文件

@@ -30,7 +30,7 @@ class Material:
their properties and update coefficients.
"""
def __init__(self, numID, ID):
def __init__(self, numID: int, ID: str):
"""
Args:
numID: int for numeric I of the material.
@@ -49,6 +49,82 @@ class Material:
self.mr = 1.0
self.sm = 0.0
def __eq__(self, value: object) -> bool:
if isinstance(value, Material):
return self.ID == value.ID
else:
raise TypeError(
f"'==' not supported between instances of 'Material' and '{type(value)}'"
)
def __lt__(self, value: "Material") -> bool:
"""Less than comparator for two Materials.
Only non-compound materials (i.e. default or user added
materials) are guaranteed to have the same numID for the same
material across MPI ranks. Therefore compound materials are
sorted by ID and non-compound materials are always less than
compound materials.
"""
if not isinstance(value, Material):
raise TypeError(
f"'<' not supported between instances of 'Material' and '{type(value)}'"
)
elif self.is_compound_material() and value.is_compound_material():
return self.ID < value.ID
else:
return value.is_compound_material() or self.numID < value.numID
def __gt__(self, value: "Material") -> bool:
"""Greater than comparator for two Materials.
Only non-compound materials (i.e. default or user added
materials) are guaranteed to have the same numID for the same
material across MPI ranks. Therefore compound materials are
sorted by ID and are always greater than non-compound materials.
"""
if not isinstance(value, Material):
raise TypeError(
f"'>' not supported between instances of 'Material' and '{type(value)}'"
)
elif self.is_compound_material() and value.is_compound_material():
return self.ID > value.ID
else:
return self.is_compound_material() or self.numID > value.numID
def is_compound_material(self) -> bool:
"""Check if a material is a compound material.
The ID of a compound material comprises of the component
material IDs joined by a '+' symbol. Therefore we check for a
compound material by looking for a '+' symbol in the material
ID.
Returns:
is_compound_material: True if material is a compound
material. False otherwise.
"""
return self.ID.count("+") > 0
@staticmethod
def create_compound_id(*materials: "Material") -> str:
"""Create a compound ID from existing materials.
The new ID will be the IDs of the existing materials joined by a
'+' symbol. The component IDs will be sorted alphabetically and
if two materials are provided, the compound ID will contain each
material twice.
Args:
*materials: Materials to use to create the compound ID.
Returns:
compound_id: New compound id.
"""
if len(materials) == 2:
materials += materials
return "+".join(sorted([material.ID for material in materials]))
def calculate_update_coeffsH(self, G):
"""Calculates the magnetic update coefficients of the material.
@@ -168,9 +244,7 @@ class DispersiveMaterial(Material):
# tau for Lorentz materials are pole frequencies
# alpha for Lorentz materials are the damping coefficients
wp2 = (2 * np.pi * self.tau[x]) ** 2
self.w[x] = -1j * (
(wp2 * self.deltaer[x]) / np.sqrt(wp2 - self.alpha[x] ** 2)
)
self.w[x] = -1j * ((wp2 * self.deltaer[x]) / np.sqrt(wp2 - self.alpha[x] ** 2))
self.q[x] = -self.alpha[x] + (1j * np.sqrt(wp2 - self.alpha[x] ** 2))
elif "drude" in self.type:
# tau for Drude materials are pole frequencies
@@ -242,13 +316,7 @@ class PeplinskiSoil:
"""
def __init__(
self,
ID,
sandfraction,
clayfraction,
bulkdensity,
sandpartdensity,
watervolfraction,
self, ID, sandfraction, clayfraction, bulkdensity, sandpartdensity, watervolfraction
):
"""
Args:
@@ -292,9 +360,7 @@ class PeplinskiSoil:
erealw = watereri + (waterdeltaer / (1 + (w * watertau) ** 2))
a = 0.65 # Experimentally derived constant
es = (
1.01 + 0.44 * self.rs
) ** 2 - 0.062 #  Relative permittivity of sand particles
es = (1.01 + 0.44 * self.rs) ** 2 - 0.062 #  Relative permittivity of sand particles
b1 = 1.2748 - 0.519 * self.S - 0.152 * self.C
b2 = 1.33797 - 0.603 * self.S - 0.166 * self.C
@@ -330,9 +396,7 @@ class PeplinskiSoil:
eri = er - (muiter[0] ** (b2 / a) * waterdeltaer)
# Effective conductivity
sig = muiter[0] ** (b2 / a) * (
(sigf * (self.rs - self.rb)) / (self.rs * muiter[0])
)
sig = muiter[0] ** (b2 / a) * ((sigf * (self.rs - self.rb)) / (self.rs * muiter[0]))
# Create individual materials
m = DispersiveMaterial(len(G.materials), None)
@@ -425,9 +489,7 @@ class RangeMaterial:
sm = romaterials[iter]
# Check to see if the material already exists before creating a new one
requiredID = (
f"|{float(er):.4f}+{float(se):.4f}+{float(mr):.4f}+{float(sm):.4f}|"
)
requiredID = f"|{float(er):.4f}+{float(se):.4f}+{float(mr):.4f}+{float(sm):.4f}|"
material = next((x for x in G.materials if x.ID == requiredID), None)
if iter == 0 and material:
self.matID.append(material.numID)
@@ -477,9 +539,7 @@ class ListMaterial:
self.matID.append(material.numID)
if not material:
logger.exception(
self.__str__() + f" material(s) {material} do not exist"
)
logger.exception(self.__str__() + f" material(s) {material} do not exist")
raise ValueError
@@ -518,9 +578,7 @@ def calculate_water_properties(T=25, S=0):
# Properties of water from: https://doi.org/10.1109/JOE.1977.1145319
eri = 4.9
er = 88.045 - 0.4147 * T + 6.295e-4 * T**2 + 1.075e-5 * T**3
tau = (1 / (2 * np.pi)) * (
1.1109e-10 - 3.824e-12 * T + 6.938e-14 * T**2 - 5.096e-16 * T**3
)
tau = (1 / (2 * np.pi)) * (1.1109e-10 - 3.824e-12 * T + 6.938e-14 * T**2 - 5.096e-16 * T**3)
delta = 25 - T
beta = (
@@ -672,15 +730,11 @@ def process_materials(G):
]
if config.get_model_config().materials["maxpoles"] > 0:
if "debye" in material.type:
materialtext.append(
"\n".join(f"{deltaer:g}" for deltaer in material.deltaer)
)
materialtext.append("\n".join(f"{deltaer:g}" for deltaer in material.deltaer))
materialtext.append("\n".join(f"{tau:g}" for tau in material.tau))
materialtext.extend(["", "", ""])
elif "lorentz" in material.type:
materialtext.append(
", ".join(f"{deltaer:g}" for deltaer in material.deltaer)
)
materialtext.append(", ".join(f"{deltaer:g}" for deltaer in material.deltaer))
materialtext.append("")
materialtext.append(", ".join(f"{tau:g}" for tau in material.tau))
materialtext.append(", ".join(f"{alpha:g}" for alpha in material.alpha))
@@ -693,9 +747,7 @@ def process_materials(G):
else:
materialtext.extend(["", "", "", "", ""])
materialtext.extend(
(f"{material.mr:g}", f"{material.sm:g}", material.averagable)
)
materialtext.extend((f"{material.mr:g}", f"{material.sm:g}", material.averagable))
materialsdata.append(materialtext)
return materialsdata

544
gprMax/model.py 普通文件
查看文件

@@ -0,0 +1,544 @@
# 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 datetime
import logging
import sys
from typing import Dict, List, Optional, Sequence
import humanize
import numpy as np
import numpy.typing as npt
import psutil
from colorama import Fore, Style, init
from gprMax.grid.cuda_grid import CUDAGrid
from gprMax.grid.opencl_grid import OpenCLGrid
from gprMax.output_controllers.geometry_objects import GeometryObject
from gprMax.output_controllers.geometry_view_lines import GeometryViewLines
from gprMax.output_controllers.geometry_view_voxels import GeometryViewVoxels
from gprMax.output_controllers.geometry_views import GeometryView, save_geometry_views
from gprMax.subgrids.grid import SubGridBaseGrid
init()
from tqdm import tqdm
import gprMax.config as config
from gprMax.fields_outputs import write_hdf5_outputfile
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.snapshots import Snapshot, save_snapshots
from gprMax.utilities.host_info import mem_check_build_all, mem_check_run_all, set_omp_threads
from gprMax.utilities.utilities import get_terminal_width
logger = logging.getLogger(__name__)
class Model:
"""Builds and runs (solves) a model."""
def __init__(self):
self.title = ""
self.dt_mod = 1.0 # Time step stability factor
self.iteration = 0 # Current iteration number
self.G = self._create_grid()
self.subgrids: List[SubGridBaseGrid] = []
self.geometryviews: List[GeometryView] = []
self.geometryobjects: List[GeometryObject] = []
# Monitor memory usage
self.p = None
# Set number of OpenMP threads to physical threads at this point to be
# used with threaded model building methods, e.g. fractals. Can be
# changed by the user via #num_threads command in input file or via API
# later for use with CPU solver.
config.get_model_config().ompthreads = set_omp_threads(config.get_model_config().ompthreads)
@property
def nx(self) -> int:
return self.G.nx
@nx.setter
def nx(self, value: int):
self.G.nx = value
@property
def ny(self) -> int:
return self.G.ny
@ny.setter
def ny(self, value: int):
self.G.ny = value
@property
def nz(self) -> int:
return self.G.nz
@nz.setter
def nz(self, value: int):
self.G.nz = value
@property
def dx(self) -> float:
return self.G.dl[0]
@dx.setter
def dx(self, value: float):
self.G.dl[0] = value
@property
def dy(self) -> float:
return self.G.dl[0]
@dy.setter
def dy(self, value: float):
self.G.dl[1] = value
@property
def dz(self) -> float:
return self.G.dl[0]
@dz.setter
def dz(self, value: float):
self.G.dl[2] = value
@property
def dl(self) -> npt.NDArray[np.float64]:
return self.G.dl
@dl.setter
def dl(self, value: npt.NDArray[np.float64]):
self.G.dl = value
@property
def dt(self) -> float:
return self.G.dt
@dt.setter
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
@property
def srcsteps(self) -> npt.NDArray[np.int32]:
return self.G.srcsteps
@srcsteps.setter
def srcsteps(self, value: npt.NDArray[np.int32]):
self.G.srcsteps = value
@property
def rxsteps(self) -> npt.NDArray[np.int32]:
return self.G.rxsteps
@rxsteps.setter
def rxsteps(self, value: npt.NDArray[np.int32]):
self.G.rxsteps = value
def _create_grid(self) -> FDTDGrid:
"""Create grid object according to solver.
Returns:
grid: FDTDGrid class describing a grid in a model.
"""
if config.sim_config.general["solver"] == "cpu":
grid = FDTDGrid()
elif config.sim_config.general["solver"] == "cuda":
grid = CUDAGrid()
elif config.sim_config.general["solver"] == "opencl":
grid = OpenCLGrid()
return grid
def set_size(self, size: npt.NDArray[np.int32]):
"""Set size of the model.
Args:
size: Array to set the size (3 dimensional).
"""
self.nx, self.ny, self.nz = size
def add_geometry_object(
self,
grid: FDTDGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
basefilename: str,
) -> Optional[GeometryObject]:
"""Add a geometry object to the model.
Args:
grid: Grid to create a geometry object for.
start: Lower extent of the geometry object (x, y, z).
stop: Upper extent of the geometry object (x, y, z).
basefilename: Output filename of the geometry object.
Returns:
geometry_object: The created geometry object.
"""
geometry_object = GeometryObject(
grid, start[0], start[1], start[2], stop[0], stop[1], stop[2], basefilename
)
self.geometryobjects.append(geometry_object)
return geometry_object
def add_geometry_view_voxels(
self,
grid: FDTDGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
dl: npt.NDArray[np.int32],
filename: str,
) -> Optional[GeometryViewVoxels]:
"""Add a voxel geometry view to the model.
Args:
grid: Grid to create a geometry view for.
start: Lower extent of the geometry view (x, y, z).
stop: Upper extent of the geometry view (x, y, z).
dl: Discritisation of the geometry view (x, y, z).
filename: Output filename of the geometry view.
Returns:
geometry_view: The created geometry view.
"""
geometry_view = GeometryViewVoxels(
start[0],
start[1],
start[2],
stop[0],
stop[1],
stop[2],
dl[0],
dl[1],
dl[2],
filename,
grid,
)
self.geometryviews.append(geometry_view)
return geometry_view
def add_geometry_view_lines(
self,
grid: FDTDGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
filename: str,
) -> Optional[GeometryViewLines]:
"""Add a lines geometry view to the model.
Args:
grid: Grid to create a geometry view for.
start: Lower extent of the geometry view (x, y, z).
stop: Upper extent of the geometry view (x, y, z).
filename: Output filename of the geometry view.
Returns:
geometry_view: The created geometry view.
"""
geometry_view = GeometryViewLines(
start[0],
start[1],
start[2],
stop[0],
stop[1],
stop[2],
filename,
grid,
)
self.geometryviews.append(geometry_view)
return geometry_view
def add_snapshot(
self,
grid: FDTDGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
dl: npt.NDArray[np.int32],
time: int,
filename: str,
fileext: str,
outputs: Dict[str, bool],
) -> Optional[Snapshot]:
"""Add a snapshot to the provided grid.
Args:
grid: Grid to create a snapshot for.
start: Lower extent of the snapshot (x, y, z).
stop: Upper extent of the snapshot (x, y, z).
dl: Discritisation of the snapshot (x, y, z).
time: Iteration number to take the snapshot on
filename: Output filename of the snapshot.
fileext: File extension of the snapshot.
outputs: Fields to use in the snapshot.
Returns:
snapshot: The created snapshot.
"""
snapshot = Snapshot(
start[0],
start[1],
start[2],
stop[0],
stop[1],
stop[2],
dl[0],
dl[1],
dl[2],
time,
filename,
fileext,
outputs,
grid,
)
# TODO: Move snapshots into the Model
grid.snapshots.append(snapshot)
return snapshot
def build(self):
"""Builds the Yee cells for a model."""
# Monitor memory usage
self.p = psutil.Process()
# Normal model reading/building process; bypassed if geometry information to be reused
if config.get_model_config().reuse_geometry():
self.reuse_geometry()
else:
self.build_geometry()
logger.info(
f"Output directory: {config.get_model_config().output_file_path.parent.resolve()}\n"
)
self.G.update_sources_and_recievers()
self._output_geometry()
def _output_geometry(self):
# Write files for any geometry views and geometry object outputs
if (
not self.geometryviews
and not self.geometryobjects
and config.sim_config.args.geometry_only
):
logger.warning(
"Geometry only run specified, but no geometry views or geometry objects found."
)
return
save_geometry_views(self.geometryviews)
if self.geometryobjects:
logger.info("")
for i, go in enumerate(self.geometryobjects):
pbar = tqdm(
total=go.datawritesize,
unit="byte",
unit_scale=True,
desc=f"Writing geometry object file {i + 1}/{len(self.geometryobjects)}, "
+ f"{go.filename_hdf5.name}",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
go.write_hdf5(self.title, pbar)
pbar.close()
logger.info("")
def build_geometry(self):
logger.info(config.get_model_config().inputfilestr)
# Print info on any subgrids
for subgrid in self.subgrids:
subgrid.print_info()
# Combine available grids
grids = [self.G] + self.subgrids
self._check_for_dispersive_materials(grids)
self._check_memory_requirements(grids)
for grid in grids:
grid.build()
grid.dispersion_analysis(self.iterations)
def _check_for_dispersive_materials(self, grids: Sequence[FDTDGrid]):
# Check for dispersive materials (and specific type)
if config.get_model_config().materials["maxpoles"] != 0:
# TODO: This sets materials["drudelorentz"] based only the
# last grid/subgrid. Is that correct?
for grid in grids:
config.get_model_config().materials["drudelorentz"] = any(
[m for m in grid.materials if "drude" in m.type or "lorentz" in m.type]
)
# Set data type if any dispersive materials (must be done before memory checks)
config.get_model_config().set_dispersive_material_types()
def _check_memory_requirements(self, grids: Sequence[FDTDGrid]):
# Check memory requirements to build model/scene (different to memory
# requirements to run model when FractalVolumes/FractalSurfaces are
# used as these can require significant additional memory)
total_mem_build, mem_strs_build = mem_check_build_all(grids)
# Check memory requirements to run model
total_mem_run, mem_strs_run = mem_check_run_all(grids)
if total_mem_build > total_mem_run:
logger.info(
f'Memory required (estimated): {" + ".join(mem_strs_build)} + '
f"~{humanize.naturalsize(config.get_model_config().mem_overhead)} "
f"overhead = {humanize.naturalsize(total_mem_build)}\n"
)
else:
logger.info(
f'Memory required (estimated): {" + ".join(mem_strs_run)} + '
f"~{humanize.naturalsize(config.get_model_config().mem_overhead)} "
f"overhead = {humanize.naturalsize(total_mem_run)}\n"
)
def reuse_geometry(self):
s = (
f"\n--- Model {config.get_model_config().appendmodelnumber}/{config.sim_config.model_end}, "
f"input file (not re-processed, i.e. geometry fixed): "
f"{config.sim_config.input_file_path}"
)
config.get_model_config().inputfilestr = (
Fore.GREEN + f"{s} {'-' * (get_terminal_width() - 1 - len(s))}\n\n" + Style.RESET_ALL
)
logger.basic(config.get_model_config().inputfilestr)
self.iteration = 0 # Reset current iteration number
for grid in [self.G] + self.subgrids:
grid.reset_fields()
def write_output_data(self):
"""Writes output data, i.e. field data for receivers and snapshots to
file(s).
"""
# Write output data to file if they are any receivers in any grids
sg_rxs = [True for sg in self.subgrids if sg.rxs]
sg_tls = [True for sg in self.subgrids if sg.transmissionlines]
if self.G.rxs or sg_rxs or self.G.transmissionlines or sg_tls:
write_hdf5_outputfile(config.get_model_config().output_file_path_ext, self.title, self)
# Write any snapshots to file for each grid
for grid in [self.G] + self.subgrids:
if grid.snapshots:
save_snapshots(grid.snapshots)
def solve(self, solver):
"""Solve using FDTD method.
Args:
solver: solver object.
"""
# Print information about and check OpenMP threads
if config.sim_config.general["solver"] == "cpu":
logger.basic(
f"Model {config.sim_config.current_model + 1}/{config.sim_config.model_end} "
f"on {config.sim_config.hostinfo['hostname']} "
f"with OpenMP backend using {config.get_model_config().ompthreads} thread(s)"
)
if config.get_model_config().ompthreads > config.sim_config.hostinfo["physicalcores"]:
logger.warning(
f"You have specified more threads ({config.get_model_config().ompthreads}) "
f"than available physical CPU cores ({config.sim_config.hostinfo['physicalcores']}). "
f"This may lead to degraded performance."
)
elif config.sim_config.general["solver"] in ["cuda", "opencl"]:
if config.sim_config.general["solver"] == "opencl":
solvername = "OpenCL"
platformname = (
" ".join(config.get_model_config().device["dev"].platform.name.split())
+ " with "
)
devicename = (
f'Device {config.get_model_config().device["deviceID"]}: '
f'{" ".join(config.get_model_config().device["dev"].name.split())}'
)
else:
solvername = "CUDA"
platformname = ""
devicename = (
f'Device {config.get_model_config().device["deviceID"]}: '
f'{" ".join(config.get_model_config().device["dev"].name().split())}'
)
logger.basic(
f"\nModel {config.sim_config.current_model + 1}/{config.sim_config.model_end} "
f"solving on {config.sim_config.hostinfo['hostname']} "
f"with {solvername} backend using {platformname}{devicename}"
)
# Prepare iterator
if config.sim_config.general["progressbars"]:
iterator = tqdm(
range(self.iterations),
desc="|--->",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
else:
iterator = range(self.iterations)
# Run solver
solver.solve(iterator)
# Write output data, i.e. field data for receivers and snapshots to file(s)
self.write_output_data()
# Print information about memory usage and solving time for a model
# Add a string on device (GPU) memory usage if applicable
mem_str = ""
if config.sim_config.general["solver"] == "cuda":
mem_str = f" host + ~{humanize.naturalsize(solver.memused)} device"
elif config.sim_config.general["solver"] == "opencl":
mem_str = f" host + unknown for device"
logger.info(
f"Memory used (estimated): "
+ f"~{humanize.naturalsize(self.p.memory_full_info().uss)}{mem_str}"
)
logger.info(
f"Time taken: "
+ f"{humanize.precisedelta(datetime.timedelta(seconds=solver.solvetime), format='%0.4f')}\n"
)

查看文件

@@ -1,477 +0,0 @@
# Copyright (C) 2015-2025: 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 datetime
import itertools
import logging
import sys
import humanize
import numpy as np
import psutil
from colorama import Fore, Style, init
init()
from terminaltables import SingleTable
from tqdm import tqdm
import gprMax.config as config
from .cython.yee_cell_build import build_electric_components, build_magnetic_components
from .fields_outputs import write_hdf5_outputfile
from .geometry_outputs import save_geometry_views
from .grid import dispersion_analysis
from .hash_cmds_file import parse_hash_commands
from .materials import process_materials
from .pml import CFS, build_pml, print_pml_info
from .scene import Scene
from .snapshots import save_snapshots
from .utilities.host_info import mem_check_build_all, mem_check_run_all, set_omp_threads
from .utilities.utilities import get_terminal_width
logger = logging.getLogger(__name__)
class ModelBuildRun:
"""Builds and runs (solves) a model."""
def __init__(self, G):
self.G = G
# Monitor memory usage
self.p = None
# Set number of OpenMP threads to physical threads at this point to be
# used with threaded model building methods, e.g. fractals. Can be
# changed by the user via #num_threads command in input file or via API
# later for use with CPU solver.
config.get_model_config().ompthreads = set_omp_threads(
config.get_model_config().ompthreads
)
def build(self):
"""Builds the Yee cells for a model."""
G = self.G
# Monitor memory usage
self.p = psutil.Process()
# Normal model reading/building process; bypassed if geometry information to be reused
self.reuse_geometry() if config.get_model_config().reuse_geometry else self.build_geometry()
logger.info(
f"\nOutput directory: {config.get_model_config().output_file_path.parent.resolve()}"
)
# Adjust position of simple sources and receivers if required
if G.srcsteps[0] != 0 or G.srcsteps[1] != 0 or G.srcsteps[2] != 0:
for source in itertools.chain(G.hertziandipoles, G.magneticdipoles):
if config.model_num == 0:
if (
source.xcoord + G.srcsteps[0] * config.sim_config.model_end < 0
or source.xcoord + G.srcsteps[0] * config.sim_config.model_end
> G.nx
or source.ycoord + G.srcsteps[1] * config.sim_config.model_end
< 0
or source.ycoord + G.srcsteps[1] * config.sim_config.model_end
> G.ny
or source.zcoord + G.srcsteps[2] * config.sim_config.model_end
< 0
or source.zcoord + G.srcsteps[2] * config.sim_config.model_end
> G.nz
):
logger.exception(
"Source(s) will be stepped to a position outside the domain."
)
raise ValueError
source.xcoord = source.xcoordorigin + config.model_num * G.srcsteps[0]
source.ycoord = source.ycoordorigin + config.model_num * G.srcsteps[1]
source.zcoord = source.zcoordorigin + config.model_num * G.srcsteps[2]
if G.rxsteps[0] != 0 or G.rxsteps[1] != 0 or G.rxsteps[2] != 0:
for receiver in G.rxs:
if config.model_num == 0:
if (
receiver.xcoord + G.rxsteps[0] * config.sim_config.model_end < 0
or receiver.xcoord + G.rxsteps[0] * config.sim_config.model_end
> G.nx
or receiver.ycoord + G.rxsteps[1] * config.sim_config.model_end
< 0
or receiver.ycoord + G.rxsteps[1] * config.sim_config.model_end
> G.ny
or receiver.zcoord + G.rxsteps[2] * config.sim_config.model_end
< 0
or receiver.zcoord + G.rxsteps[2] * config.sim_config.model_end
> G.nz
):
logger.exception(
"Receiver(s) will be stepped to a position outside the domain."
)
raise ValueError
receiver.xcoord = (
receiver.xcoordorigin + config.model_num * G.rxsteps[0]
)
receiver.ycoord = (
receiver.ycoordorigin + config.model_num * G.rxsteps[1]
)
receiver.zcoord = (
receiver.zcoordorigin + config.model_num * G.rxsteps[2]
)
# Write files for any geometry views and geometry object outputs
gvs = G.geometryviews + [gv for sg in G.subgrids for gv in sg.geometryviews]
if (
not gvs
and not G.geometryobjectswrite
and config.sim_config.args.geometry_only
):
logger.exception("\nNo geometry views or geometry objects found.")
raise ValueError
save_geometry_views(gvs)
if G.geometryobjectswrite:
logger.info("")
for i, go in enumerate(G.geometryobjectswrite):
pbar = tqdm(
total=go.datawritesize,
unit="byte",
unit_scale=True,
desc=f"Writing geometry object file {i + 1}/{len(G.geometryobjectswrite)}, "
+ f"{go.filename_hdf5.name}",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
go.write_hdf5(G, pbar)
pbar.close()
logger.info("")
def build_geometry(self):
G = self.G
logger.info(config.get_model_config().inputfilestr)
# Build objects in the scene and check memory for building
self.build_scene()
# Print info on any subgrids
for sg in G.subgrids:
sg.print_info()
# Combine available grids
grids = [G] + G.subgrids
# Check for dispersive materials (and specific type)
for grid in grids:
if config.get_model_config().materials["maxpoles"] != 0:
config.get_model_config().materials["drudelorentz"] = any(
[
m
for m in grid.materials
if "drude" in m.type or "lorentz" in m.type
]
)
# Set data type if any dispersive materials (must be done before memory checks)
if config.get_model_config().materials["maxpoles"] != 0:
config.get_model_config().set_dispersive_material_types()
# Check memory requirements to build model/scene (different to memory
# requirements to run model when FractalVolumes/FractalSurfaces are
# used as these can require significant additional memory)
total_mem_build, mem_strs_build = mem_check_build_all(grids)
# Check memory requirements to run model
total_mem_run, mem_strs_run = mem_check_run_all(grids)
if total_mem_build > total_mem_run:
logger.info(
f"\nMemory required (estimated): {' + '.join(mem_strs_build)} + "
f"~{humanize.naturalsize(config.get_model_config().mem_overhead)} "
f"overhead = {humanize.naturalsize(total_mem_build)}"
)
else:
logger.info(
f"\nMemory required (estimated): {' + '.join(mem_strs_run)} + "
f"~{humanize.naturalsize(config.get_model_config().mem_overhead)} "
f"overhead = {humanize.naturalsize(total_mem_run)}"
)
# Build grids
gridbuilders = [GridBuilder(grid) for grid in grids]
for gb in gridbuilders:
# Set default CFS parameter for PMLs if not user provided
if not gb.grid.pmls["cfs"]:
gb.grid.pmls["cfs"] = [CFS()]
logger.info(print_pml_info(gb.grid))
if not all(value == 0 for value in gb.grid.pmls["thickness"].values()):
gb.build_pmls()
if gb.grid.averagevolumeobjects:
gb.build_components()
gb.tm_grid_update()
gb.update_voltage_source_materials()
gb.grid.initialise_field_arrays()
gb.grid.initialise_std_update_coeff_arrays()
if config.get_model_config().materials["maxpoles"] > 0:
gb.grid.initialise_dispersive_arrays()
gb.grid.initialise_dispersive_update_coeff_array()
gb.build_materials()
# Check to see if numerical dispersion might be a problem
results = dispersion_analysis(gb.grid)
if results["error"]:
logger.warning(
f"\nNumerical dispersion analysis [{gb.grid.name}] "
f"not carried out as {results['error']}"
)
elif (
results["N"]
< config.get_model_config().numdispersion["mingridsampling"]
):
logger.exception(
f"\nNon-physical wave propagation in [{gb.grid.name}] "
f"detected. Material '{results['material'].ID}' "
f"has wavelength sampled by {results['N']} cells, "
f"less than required minimum for physical wave "
f"propagation. Maximum significant frequency "
f"estimated as {results['maxfreq']:g}Hz"
)
raise ValueError
elif (
results["deltavp"]
and np.abs(results["deltavp"])
> config.get_model_config().numdispersion["maxnumericaldisp"]
):
logger.warning(
f"\n[{gb.grid.name}] has potentially significant "
f"numerical dispersion. Estimated largest physical "
f"phase-velocity error is {results['deltavp']:.2f}% "
f"in material '{results['material'].ID}' whose "
f"wavelength sampled by {results['N']} cells. "
f"Maximum significant frequency estimated as "
f"{results['maxfreq']:g}Hz"
)
elif results["deltavp"]:
logger.info(
f"\nNumerical dispersion analysis [{gb.grid.name}]: "
f"estimated largest physical phase-velocity error is "
f"{results['deltavp']:.2f}% in material '{results['material'].ID}' "
f"whose wavelength sampled by {results['N']} cells. "
f"Maximum significant frequency estimated as "
f"{results['maxfreq']:g}Hz"
)
def reuse_geometry(self):
s = (
f"\n--- Model {config.get_model_config().appendmodelnumber}/{config.sim_config.model_end}, "
f"input file (not re-processed, i.e. geometry fixed): "
f"{config.sim_config.input_file_path}"
)
config.get_model_config().inputfilestr = (
Fore.GREEN
+ f"{s} {'-' * (get_terminal_width() - 1 - len(s))}\n"
+ Style.RESET_ALL
)
logger.basic(config.get_model_config().inputfilestr)
for grid in [self.G] + self.G.subgrids:
grid.iteration = 0 # Reset current iteration number
grid.reset_fields()
def build_scene(self):
# API for multiple scenes / model runs
scene = config.get_model_config().get_scene()
# If there is no scene, process the hash commands
if not scene:
scene = Scene()
config.sim_config.scenes.append(scene)
# Parse the input file into user objects and add them to the scene
scene = parse_hash_commands(scene)
# Creates the internal simulation objects
scene.create_internal_objects(self.G)
return scene
def write_output_data(self):
"""Writes output data, i.e. field data for receivers and snapshots to
file(s).
"""
# Write output data to file if they are any receivers in any grids
sg_rxs = [True for sg in self.G.subgrids if sg.rxs]
sg_tls = [True for sg in self.G.subgrids if sg.transmissionlines]
if self.G.rxs or sg_rxs or self.G.transmissionlines or sg_tls:
write_hdf5_outputfile(
config.get_model_config().output_file_path_ext, self.G
)
# Write any snapshots to file for each grid
for grid in [self.G] + self.G.subgrids:
if grid.snapshots:
save_snapshots(grid)
def solve(self, solver):
"""Solve using FDTD method.
Args:
solver: solver object.
"""
# Print information about and check OpenMP threads
if config.sim_config.general["solver"] == "cpu":
logger.basic(
f"\nModel {config.model_num + 1}/{config.sim_config.model_end} "
f"on {config.sim_config.hostinfo['hostname']} "
f"with OpenMP backend using {config.get_model_config().ompthreads} thread(s)"
)
if (
config.get_model_config().ompthreads
> config.sim_config.hostinfo["physicalcores"]
):
logger.warning(
f"You have specified more threads ({config.get_model_config().ompthreads}) "
f"than available physical CPU cores ({config.sim_config.hostinfo['physicalcores']}). "
f"This may lead to degraded performance."
)
elif config.sim_config.general["solver"] in ["cuda", "opencl"]:
if config.sim_config.general["solver"] == "opencl":
solvername = "OpenCL"
platformname = (
" ".join(
config.get_model_config().device["dev"].platform.name.split()
)
+ " with "
)
devicename = (
f"Device {config.get_model_config().device['deviceID']}: "
f"{' '.join(config.get_model_config().device['dev'].name.split())}"
)
else:
solvername = "CUDA"
platformname = ""
devicename = (
f"Device {config.get_model_config().device['deviceID']}: "
f"{' '.join(config.get_model_config().device['dev'].name().split())}"
)
logger.basic(
f"\nModel {config.model_num + 1}/{config.sim_config.model_end} "
f"solving on {config.sim_config.hostinfo['hostname']} "
f"with {solvername} backend using {platformname}{devicename}"
)
# Prepare iterator
if config.sim_config.general["progressbars"]:
iterator = tqdm(
range(self.G.iterations),
desc="|--->",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
else:
iterator = range(self.G.iterations)
# Run solver
solver.solve(iterator)
# Write output data, i.e. field data for receivers and snapshots to file(s)
self.write_output_data()
# Print information about memory usage and solving time for a model
# Add a string on device (GPU) memory usage if applicable
mem_str = ""
if config.sim_config.general["solver"] == "cuda":
mem_str = f" host + ~{humanize.naturalsize(solver.memused)} device"
elif config.sim_config.general["solver"] == "opencl":
mem_str = f" host + unknown for device"
logger.info(
f"\nMemory used (estimated): "
+ f"~{humanize.naturalsize(self.p.memory_full_info().uss)}{mem_str}"
)
logger.info(
f"Time taken: "
+ f"{humanize.precisedelta(datetime.timedelta(seconds=solver.solvetime), format='%0.4f')}"
)
class GridBuilder:
def __init__(self, grid):
self.grid = grid
def build_pmls(self):
pbar = tqdm(
total=sum(1 for value in self.grid.pmls["thickness"].values() if value > 0),
desc=f"Building PML boundaries [{self.grid.name}]",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
for pml_id, thickness in self.grid.pmls["thickness"].items():
if thickness > 0:
build_pml(self.grid, pml_id, thickness)
pbar.update()
pbar.close()
def build_components(self):
# Build the model, i.e. set the material properties (ID) for every edge
# of every Yee cell
logger.info("")
pbar = tqdm(
total=2,
desc=f"Building Yee cells [{self.grid.name}]",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
build_electric_components(
self.grid.solid, self.grid.rigidE, self.grid.ID, self.grid
)
pbar.update()
build_magnetic_components(
self.grid.solid, self.grid.rigidH, self.grid.ID, self.grid
)
pbar.update()
pbar.close()
def tm_grid_update(self):
if config.get_model_config().mode == "2D TMx":
self.grid.tmx()
elif config.get_model_config().mode == "2D TMy":
self.grid.tmy()
elif config.get_model_config().mode == "2D TMz":
self.grid.tmz()
def update_voltage_source_materials(self):
# Process any voltage sources (that have resistance) to create a new
# material at the source location
for voltagesource in self.grid.voltagesources:
voltagesource.create_material(self.grid)
def build_materials(self):
# Process complete list of materials - calculate update coefficients,
# store in arrays, and build text list of materials/properties
materialsdata = process_materials(self.grid)
materialstable = SingleTable(materialsdata)
materialstable.outer_border = False
materialstable.justify_columns[0] = "right"
logger.info(f"\nMaterials [{self.grid.name}]:")
logger.info(materialstable.table)

255
gprMax/mpi_model.py 普通文件
查看文件

@@ -0,0 +1,255 @@
import logging
from typing import Dict, Optional
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from gprMax import config
from gprMax.fields_outputs import write_hdf5_outputfile
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.model import Model
from gprMax.output_controllers.geometry_objects import MPIGeometryObject
from gprMax.output_controllers.geometry_view_lines import MPIGeometryViewLines
from gprMax.output_controllers.geometry_view_voxels import MPIGeometryViewVoxels
from gprMax.snapshots import MPISnapshot, Snapshot, save_snapshots
logger = logging.getLogger(__name__)
class MPIModel(Model):
def __init__(self, comm: Optional[MPI.Intracomm] = None):
if comm is None:
self.comm = MPI.COMM_WORLD
else:
self.comm = comm
self.rank = self.comm.Get_rank()
self.G = self._create_grid()
return super().__init__()
@property
def nx(self) -> float:
return self.G.global_size[0]
@nx.setter
def nx(self, value: float):
self.G.global_size[0] = value
@property
def ny(self) -> float:
return self.G.global_size[1]
@ny.setter
def ny(self, value: float):
self.G.global_size[1] = value
@property
def nz(self) -> float:
return self.G.global_size[2]
@nz.setter
def nz(self, value: float):
self.G.global_size[2] = value
def is_coordinator(self):
return self.rank == 0
def set_size(self, size: npt.NDArray[np.int32]):
super().set_size(size)
self.G.calculate_local_extents()
def add_geometry_object(
self,
grid: MPIGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
basefilename: str,
) -> Optional[MPIGeometryObject]:
"""Add a geometry object to the model.
Args:
grid: Grid to create a geometry object for
start: Lower extent of the geometry object (x, y, z)
stop: Upper extent of the geometry object (x, y, z)
basefilename: Output filename of the geometry object
Returns:
geometry_object: The new geometry object or None if no
geometry object was created.
"""
if grid.local_bounds_overlap_grid(start, stop):
geometry_object = MPIGeometryObject(
grid, start[0], start[1], start[2], stop[0], stop[1], stop[2], basefilename
)
self.geometryobjects.append(geometry_object)
return geometry_object
else:
# The MPIGridView created by the MPIGeometryObject will
# create a new communicator using MPI_Split. Calling this
# here prevents deadlock if not all ranks create the new
# MPIGeometryObject.
grid.comm.Split(MPI.UNDEFINED)
return None
def add_geometry_view_voxels(
self,
grid: MPIGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
dl: npt.NDArray[np.int32],
filename: str,
) -> Optional[MPIGeometryViewVoxels]:
"""Add a voxel geometry view to the model.
Args:
grid: Grid to create a geometry view for.
start: Lower extent of the geometry view (x, y, z).
stop: Upper extent of the geometry view (x, y, z).
dl: Discritisation of the geometry view (x, y, z).
filename: Output filename of the geometry view.
Returns:
geometry_view: The new geometry view or None if no geometry
view was created.
"""
if grid.local_bounds_overlap_grid(start, stop):
geometry_view = MPIGeometryViewVoxels(
start[0],
start[1],
start[2],
stop[0],
stop[1],
stop[2],
dl[0],
dl[1],
dl[2],
filename,
grid,
)
self.geometryviews.append(geometry_view)
return geometry_view
else:
# The MPIGridView created by MPIGeometryViewVoxels will
# create a new communicator using MPI_Split. Calling this
# here prevents deadlock if not all ranks create the new
# MPIGeometryViewVoxels.
grid.comm.Split(MPI.UNDEFINED)
return None
def add_geometry_view_lines(
self,
grid: MPIGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
filename: str,
) -> Optional[MPIGeometryViewLines]:
"""Add a lines geometry view to the model.
Args:
grid: Grid to create a geometry view for.
start: Lower extent of the geometry view (x, y, z).
stop: Upper extent of the geometry view (x, y, z).
filename: Output filename of the geometry view.
Returns:
geometry_view: The new geometry view or None if no geometry
view was created.
"""
if grid.local_bounds_overlap_grid(start, stop):
geometry_view = MPIGeometryViewLines(
start[0],
start[1],
start[2],
stop[0],
stop[1],
stop[2],
filename,
grid,
)
self.geometryviews.append(geometry_view)
return geometry_view
else:
# The MPIGridView created by MPIGeometryViewLines will
# create a new communicator using MPI_Split. Calling this
# here prevents deadlock if not all ranks create the new
# MPIGeometryViewLines.
grid.comm.Split(MPI.UNDEFINED)
return None
def add_snapshot(
self,
grid: MPIGrid,
start: npt.NDArray[np.int32],
stop: npt.NDArray[np.int32],
dl: npt.NDArray[np.int32],
time: int,
filename: str,
fileext: str,
outputs: Dict[str, bool],
) -> Optional[MPISnapshot]:
"""Add a snapshot to the provided grid.
Args:
grid: Grid to create a snapshot for.
start: Lower extent of the snapshot (x, y, z).
stop: Upper extent of the snapshot (x, y, z).
dl: Discritisation of the snapshot (x, y, z).
time: Iteration number to take the snapshot on
filename: Output filename of the snapshot.
fileext: File extension of the snapshot.
outputs: Fields to use in the snapshot.
Returns:
snapshot: The new snapshot or None if no snapshot was
created.
"""
if grid.local_bounds_overlap_grid(start, stop):
snapshot = MPISnapshot(
start[0],
start[1],
start[2],
stop[0],
stop[1],
stop[2],
dl[0],
dl[1],
dl[2],
time,
filename,
fileext,
outputs,
grid,
)
# TODO: Move snapshots into the Model
grid.snapshots.append(snapshot)
return snapshot
else:
# The MPIGridView created by MPISnapshot will create a new
# communicator using MPI_Split. Calling this here prevents
# deadlock if not all ranks create the new MPISnapshot.
grid.comm.Split(MPI.UNDEFINED)
return None
def write_output_data(self):
"""Writes output data, i.e. field data for receivers and snapshots to
file(s).
"""
# Write any snapshots to file for each grid
if self.G.snapshots:
save_snapshots(self.G.snapshots)
# TODO: Output sources and receivers using parallel I/O
self.G.gather_grid_objects()
# Write output data to file if they are any receivers in any grids
if self.is_coordinator() and (self.G.rxs or self.G.transmissionlines):
self.G.size = self.G.global_size
write_hdf5_outputfile(config.get_model_config().output_file_path_ext, self.title, self)
def _create_grid(self) -> MPIGrid:
cart_comm = MPI.COMM_WORLD.Create_cart(config.sim_config.mpi)
return MPIGrid(cart_comm)

查看文件

@@ -0,0 +1,175 @@
from io import TextIOWrapper
from pathlib import Path
from typing import Generic
import h5py
import numpy as np
from mpi4py import MPI
from tqdm import tqdm
from gprMax import config
from gprMax._version import __version__
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.materials import Material
from gprMax.output_controllers.grid_view import GridType, GridView, MPIGridView
class GeometryObject(Generic[GridType]):
"""Geometry objects to be written to file."""
@property
def GRID_VIEW_TYPE(self) -> type[GridView]:
return GridView
def __init__(
self, grid: GridType, xs: int, ys: int, zs: int, xf: int, yf: int, zf: int, filename: str
):
"""
Args:
xs, xf, ys, yf, zs, zf: ints for extent of the volume in cells.
filename: string for filename.
"""
self.grid_view = self.GRID_VIEW_TYPE(grid, xs, ys, zs, xf, yf, zf)
# Set filenames
parts = config.sim_config.input_file_path.with_suffix("").parts
self.filename = Path(*parts[:-1], filename)
self.filename_hdf5 = self.filename.with_suffix(".h5")
self.filename_materials = Path(f"{self.filename}_materials")
self.filename_materials = self.filename_materials.with_suffix(".txt")
# Sizes of arrays to write necessary to update progress bar
self.solidsize = (float)(np.prod(self.grid_view.size) * np.dtype(np.uint32).itemsize)
self.rigidsize = (float)(18 * np.prod(self.grid_view.size) * np.dtype(np.int8).itemsize)
self.IDsize = (float)(6 * np.prod(self.grid_view.size + 1) * np.dtype(np.uint32).itemsize)
self.datawritesize = self.solidsize + self.rigidsize + self.IDsize
@property
def grid(self) -> GridType:
return self.grid_view.grid
def write_metadata(self, file_handler: h5py.File, title: str):
file_handler.attrs["gprMax"] = __version__
file_handler.attrs["Title"] = title
file_handler.attrs["dx_dy_dz"] = (self.grid.dx, self.grid.dy, self.grid.dz)
def output_material(self, material: Material, file: TextIOWrapper):
file.write(
f"#material: {material.er:g} {material.se:g} "
f"{material.mr:g} {material.sm:g} {material.ID}\n"
)
if hasattr(material, "poles"):
if "debye" in material.type:
dispersionstr = "#add_dispersion_debye: " f"{material.poles:g} "
for pole in range(material.poles):
dispersionstr += f"{material.deltaer[pole]:g} " f"{material.tau[pole]:g} "
elif "lorenz" in material.type:
dispersionstr = f"#add_dispersion_lorenz: " f"{material.poles:g} "
for pole in range(material.poles):
dispersionstr += (
f"{material.deltaer[pole]:g} "
f"{material.tau[pole]:g} "
f"{material.alpha[pole]:g} "
)
elif "drude" in material.type:
dispersionstr = f"#add_dispersion_drude: " f"{material.poles:g} "
for pole in range(material.poles):
dispersionstr += f"{material.tau[pole]:g} " f"{material.alpha[pole]:g} "
dispersionstr += material.ID
file.write(dispersionstr + "\n")
def write_hdf5(self, title: str, pbar: tqdm):
"""Writes a geometry objects file in HDF5 format.
Args:
G: FDTDGrid class describing a grid in a model.
pbar: Progress bar class instance.
"""
self.grid_view.initialise_materials()
ID = self.grid_view.get_ID()
data = self.grid_view.get_solid().astype(np.int16)
rigidE = self.grid_view.get_rigidE()
rigidH = self.grid_view.get_rigidH()
ID = self.grid_view.map_to_view_materials(ID)
data = self.grid_view.map_to_view_materials(data)
with h5py.File(self.filename_hdf5, "w") as fdata:
self.write_metadata(fdata, title)
fdata["/data"] = data
pbar.update(self.solidsize)
fdata["/rigidE"] = rigidE
fdata["/rigidH"] = rigidH
pbar.update(self.rigidsize)
fdata["/ID"] = ID
pbar.update(self.IDsize)
# Write materials list to a text file
with open(self.filename_materials, "w") as fmaterials:
for material in self.grid_view.materials:
self.output_material(material, fmaterials)
class MPIGeometryObject(GeometryObject[MPIGrid]):
@property
def GRID_VIEW_TYPE(self) -> type[MPIGridView]:
return MPIGridView
def write_hdf5(self, title: str, pbar: tqdm):
"""Writes a geometry objects file in HDF5 format.
Args:
G: FDTDGrid class describing a grid in a model.
pbar: Progress bar class instance.
"""
assert isinstance(self.grid_view, self.GRID_VIEW_TYPE)
self.grid_view.initialise_materials()
ID = self.grid_view.get_ID()
data = self.grid_view.get_solid().astype(np.int16)
rigidE = self.grid_view.get_rigidE()
rigidH = self.grid_view.get_rigidH()
ID = self.grid_view.map_to_view_materials(ID)
data = self.grid_view.map_to_view_materials(data)
with h5py.File(self.filename_hdf5, "w", driver="mpio", comm=self.grid_view.comm) as fdata:
self.write_metadata(fdata, title)
dset_slice = self.grid_view.get_3d_output_slice()
dset = fdata.create_dataset("/data", self.grid_view.global_size, dtype=data.dtype)
dset[dset_slice] = data
pbar.update(self.solidsize)
rigid_E_dataset = fdata.create_dataset(
"/rigidE", (12, *self.grid_view.global_size), dtype=rigidE.dtype
)
rigid_E_dataset[:, dset_slice[0], dset_slice[1], dset_slice[2]] = rigidE
rigid_H_dataset = fdata.create_dataset(
"/rigidH", (6, *self.grid_view.global_size), dtype=rigidH.dtype
)
rigid_H_dataset[:, dset_slice[0], dset_slice[1], dset_slice[2]] = rigidH
pbar.update(self.rigidsize)
dset_slice = self.grid_view.get_3d_output_slice(upper_bound_exclusive=False)
dset = fdata.create_dataset(
"/ID", (6, *(self.grid_view.global_size + 1)), dtype=ID.dtype
)
dset[:, dset_slice[0], dset_slice[1], dset_slice[2]] = ID
pbar.update(self.IDsize)
# Write materials list to a text file
if self.grid_view.materials is not None:
with open(self.filename_materials, "w") as materials_file:
for material in self.grid_view.materials:
self.output_material(material, materials_file)

查看文件

@@ -0,0 +1,185 @@
# 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
import numpy as np
from gprMax._version import __version__
from gprMax.cython.geometry_outputs import get_line_properties
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.output_controllers.geometry_views import GeometryView, Metadata, MPIMetadata
from gprMax.output_controllers.grid_view import GridType, MPIGridView
from gprMax.subgrids.grid import SubGridBaseGrid
from gprMax.vtkhdf_filehandlers.vtk_unstructured_grid import VtkUnstructuredGrid
from gprMax.vtkhdf_filehandlers.vtkhdf import VtkCellType
logger = logging.getLogger(__name__)
class GeometryViewLines(GeometryView[GridType]):
"""Unstructured grid for a per-cell-edge geometry view."""
def __init__(
self,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
filename: str,
grid: GridType,
):
super().__init__(xs, ys, zs, xf, yf, zf, 1, 1, 1, filename, grid)
def prep_vtk(self):
"""Prepares data for writing to VTKHDF file."""
self.grid_view.initialise_materials(filter_materials=False)
ID = self.grid_view.get_ID()
x = np.arange(self.grid_view.nx + 1, dtype=np.float64)
y = np.arange(self.grid_view.ny + 1, dtype=np.float64)
z = np.arange(self.grid_view.nz + 1, dtype=np.float64)
coords = np.meshgrid(x, y, z, indexing="ij")
self.points = np.vstack(list(map(np.ravel, coords))).T
self.points += self.grid_view.start
self.points *= self.grid_view.step * self.grid.dl
# Add offset to subgrid geometry to correctly locate within main grid
if isinstance(self.grid, SubGridBaseGrid):
offset = [self.grid.i0, self.grid.j0, self.grid.k0]
self.points += offset * self.grid.dl * self.grid.ratio
# Each point is the 'source' for 3 lines.
# NB: Excluding points at the far edge of the geometry as those
# are the 'source' for no lines
n_lines = 3 * np.prod(self.grid_view.size)
self.cell_types = np.full(n_lines, VtkCellType.LINE)
self.cell_offsets = np.arange(0, 2 * n_lines + 2, 2, dtype=np.intc)
self.connectivity, self.material_data = get_line_properties(
n_lines, *self.grid_view.size, ID
)
assert isinstance(self.connectivity, np.ndarray)
assert isinstance(self.material_data, np.ndarray)
# Write information about any PMLs, sources, receivers
self.metadata = Metadata(self.grid_view, averaged_materials=True, materials_only=True)
# Number of bytes of data to be written to file
self.nbytes = (
self.points.nbytes
+ self.cell_types.nbytes
+ self.connectivity.nbytes
+ self.cell_offsets.nbytes
+ self.material_data.nbytes
)
# Use sorted material IDs rather than default ordering
self.material_data = self.grid_view.map_to_view_materials(self.material_data)
def write_vtk(self):
"""Writes geometry information to a VTKHDF file."""
# Write the VTK file
with VtkUnstructuredGrid(
self.filename,
self.points,
self.cell_types,
self.connectivity,
self.cell_offsets,
) as f:
f.add_cell_data("Material", self.material_data)
self.metadata.write_to_vtkhdf(f)
class MPIGeometryViewLines(GeometryViewLines[MPIGrid]):
"""Image data for a per-cell geometry view."""
@property
def GRID_VIEW_TYPE(self) -> type[MPIGridView]:
return MPIGridView
def prep_vtk(self):
"""Prepares data for writing to VTKHDF file."""
assert isinstance(self.grid_view, MPIGridView)
self.grid_view.initialise_materials(filter_materials=False)
ID = self.grid_view.get_ID()
x = np.arange(self.grid_view.gx + 1, dtype=np.float64)
y = np.arange(self.grid_view.gy + 1, dtype=np.float64)
z = np.arange(self.grid_view.gz + 1, dtype=np.float64)
coords = np.meshgrid(x, y, z, indexing="ij")
self.points = np.vstack(list(map(np.ravel, coords))).T
self.points += self.grid_view.global_start
self.points *= self.grid_view.step * self.grid.dl
# Each point is the 'source' for 3 lines.
# NB: Excluding points at the far edge of the geometry as those
# are the 'source' for no lines
n_lines = 3 * np.prod(self.grid_view.global_size)
self.cell_types = np.full(n_lines, VtkCellType.LINE)
self.cell_offsets = np.arange(0, 2 * n_lines + 2, 2, dtype=np.intc)
self.connectivity, self.material_data = get_line_properties(
n_lines, *self.grid_view.size, ID
)
assert isinstance(self.connectivity, np.ndarray)
assert isinstance(self.material_data, np.ndarray)
# Write information about any PMLs, sources, receivers
self.metadata = MPIMetadata(self.grid_view, averaged_materials=True, materials_only=True)
# Number of bytes of data to be written to file
self.nbytes = (
self.points.nbytes
+ self.cell_types.nbytes
+ self.connectivity.nbytes
+ self.cell_offsets.nbytes
+ self.material_data.nbytes
)
# Use global material IDs rather than local IDs
self.material_data = self.grid_view.map_to_view_materials(self.material_data)
def write_vtk(self):
"""Writes geometry information to a VTKHDF file."""
assert isinstance(self.grid_view, MPIGridView)
with VtkUnstructuredGrid(
self.filename,
self.points,
self.cell_types,
self.connectivity,
self.cell_offsets,
comm=self.grid_view.comm,
) as f:
self.metadata.write_to_vtkhdf(f)
f.add_cell_data("Material", self.material_data, self.grid_view.offset)

查看文件

@@ -0,0 +1,102 @@
# 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
import numpy as np
from gprMax._version import __version__
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.output_controllers.geometry_views import GeometryView, Metadata, MPIMetadata
from gprMax.output_controllers.grid_view import GridType, MPIGridView
from gprMax.subgrids.grid import SubGridBaseGrid
from gprMax.vtkhdf_filehandlers.vtk_image_data import VtkImageData
logger = logging.getLogger(__name__)
class GeometryViewVoxels(GeometryView[GridType]):
"""Image data for a per-cell geometry view."""
def prep_vtk(self):
"""Prepares data for writing to VTKHDF file."""
self.material_data = self.grid_view.get_solid()
if isinstance(self.grid, SubGridBaseGrid):
self.origin = np.array(
[
(self.grid.i0 * self.grid.dx * self.grid.ratio),
(self.grid.j0 * self.grid.dy * self.grid.ratio),
(self.grid.k0 * self.grid.dz * self.grid.ratio),
]
)
else:
self.origin = self.grid_view.start * self.grid.dl
self.spacing = self.grid_view.step * self.grid.dl
self.nbytes = self.material_data.nbytes
# Write information about any PMLs, sources, receivers
self.metadata = Metadata(self.grid_view)
def write_vtk(self):
"""Writes geometry information to a VTKHDF file."""
with VtkImageData(self.filename, self.grid_view.size, self.origin, self.spacing) as f:
f.add_cell_data("Material", self.material_data)
self.metadata.write_to_vtkhdf(f)
class MPIGeometryViewVoxels(GeometryViewVoxels[MPIGrid]):
"""Image data for a per-cell geometry view."""
@property
def GRID_VIEW_TYPE(self) -> type[MPIGridView]:
return MPIGridView
def prep_vtk(self):
"""Prepares data for writing to VTKHDF file."""
assert isinstance(self.grid_view, self.GRID_VIEW_TYPE)
self.material_data = self.grid_view.get_solid()
self.origin = self.grid_view.global_start * self.grid.dl
self.spacing = self.grid_view.step * self.grid.dl
self.nbytes = self.material_data.nbytes
# Write information about any PMLs, sources, receivers
self.metadata = MPIMetadata(self.grid_view)
def write_vtk(self):
"""Writes geometry information to a VTKHDF file."""
assert isinstance(self.grid_view, self.GRID_VIEW_TYPE)
with VtkImageData(
self.filename,
self.grid_view.global_size,
self.origin,
self.spacing,
comm=self.grid_view.comm,
) as f:
self.metadata.write_to_vtkhdf(f)
f.add_cell_data("Material", self.material_data, self.grid_view.offset)

查看文件

@@ -0,0 +1,312 @@
# 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
import sys
from abc import abstractmethod
from pathlib import Path
from typing import Dict, Generic, List, Optional, Sequence, Tuple, Union
import h5py
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from tqdm import tqdm
import gprMax.config as config
from gprMax._version import __version__
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.output_controllers.grid_view import GridType, GridView, MPIGridView
from gprMax.receivers import Rx
from gprMax.sources import Source
from gprMax.utilities.utilities import get_terminal_width
from gprMax.vtkhdf_filehandlers.vtkhdf import VtkHdfFile
logger = logging.getLogger(__name__)
def save_geometry_views(gvs: "List[GeometryView]"):
"""Creates and saves geometryviews.
Args:
gvs: list of all GeometryViews.
"""
logger.info("")
for i, gv in enumerate(gvs):
gv.set_filename()
gv.prep_vtk()
pbar = tqdm(
total=gv.nbytes,
unit="byte",
unit_scale=True,
desc=f"Writing geometry view file {i + 1}/{len(gvs)}, {gv.filename.name}",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
gv.write_vtk()
pbar.update(gv.nbytes)
pbar.close()
logger.info("")
class GeometryView(Generic[GridType]):
"""Base class for Geometry Views."""
FILE_EXTENSION = ".vtkhdf"
@property
def GRID_VIEW_TYPE(self) -> type[GridView]:
return GridView
def __init__(
self,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
dx: int,
dy: int,
dz: int,
filename: str,
grid: GridType,
):
"""
Args:
xs, xf, ys, yf, zs, zf: ints for extent of geometry view in cells.
dx, dy, dz: ints for spatial discretisation of geometry view in cells.
filename: string for filename.
grid: FDTDGrid class describing a grid in a model.
"""
self.grid_view = self.GRID_VIEW_TYPE(grid, xs, ys, zs, xf, yf, zf, dx, dy, dz)
self.filenamebase = filename
self.nbytes = None
self.material_data = None
self.materials = None
@property
def grid(self) -> GridType:
return self.grid_view.grid
def set_filename(self):
"""Construct filename from user-supplied name and model number."""
parts = config.get_model_config().output_file_path.parts
self.filename = Path(
*parts[:-1], self.filenamebase + config.get_model_config().appendmodelnumber
).with_suffix(self.FILE_EXTENSION)
@abstractmethod
def prep_vtk(self):
pass
@abstractmethod
def write_vtk(self):
pass
class Metadata(Generic[GridType]):
"""Comments can be strings included in the header of XML VTK file, and are
used to hold extra (gprMax) information about the VTK data.
"""
def __init__(
self,
grid_view: GridView[GridType],
averaged_materials: bool = False,
materials_only: bool = False,
):
self.grid_view = grid_view
self.averaged_materials = averaged_materials
self.materials_only = materials_only
self.gprmax_version = __version__
self.dx_dy_dz = self.dx_dy_dz_comment()
self.nx_ny_nz = self.nx_ny_nz_comment()
self.materials = self.materials_comment()
# Write information on PMLs, sources, and receivers
if not self.materials_only:
# Information on PML thickness
self.pml_thickness = self.pml_gv_comment()
sources = (
self.grid.hertziandipoles
+ self.grid.magneticdipoles
+ self.grid.voltagesources
+ self.grid.transmissionlines
)
sources_comment = self.srcs_rx_gv_comment(sources)
if sources_comment is None:
self.source_ids = self.source_positions = None
else:
self.source_ids, self.source_positions = sources_comment
receivers_comment = self.srcs_rx_gv_comment(self.grid.rxs)
if receivers_comment is None:
self.receiver_ids = self.receiver_positions = None
else:
self.receiver_ids, self.receiver_positions = receivers_comment
@property
def grid(self) -> GridType:
return self.grid_view.grid
def write_to_vtkhdf(self, file_handler: VtkHdfFile):
file_handler.add_field_data("gprMax_version", self.gprmax_version)
file_handler.add_field_data("dx_dy_dz", self.dx_dy_dz)
file_handler.add_field_data("nx_ny_nz", self.nx_ny_nz)
self.write_material_ids(file_handler)
if not self.materials_only:
if self.pml_thickness is not None:
file_handler.add_field_data("pml_thickness", self.pml_thickness)
if self.source_ids is not None and self.source_positions is not None:
file_handler.add_field_data("source_ids", self.source_ids)
file_handler.add_field_data("sources", self.source_positions)
if self.receiver_ids is not None and self.receiver_positions is not None:
file_handler.add_field_data("receiver_ids", self.receiver_ids)
file_handler.add_field_data("receivers", self.receiver_positions)
def write_material_ids(self, file_handler: VtkHdfFile):
file_handler.add_field_data("material_ids", self.materials)
def pml_gv_comment(self) -> Optional[npt.NDArray[np.int64]]:
grid = self.grid
if not grid.pmls["slabs"]:
return None
# Only render PMLs if they are in the geometry view
thickness: Dict[str, int] = grid.pmls["thickness"]
gv_pml_depth = dict.fromkeys(thickness, 0)
if self.grid_view.xs < thickness["x0"]:
gv_pml_depth["x0"] = thickness["x0"] - self.grid_view.xs
if self.grid_view.ys < thickness["y0"]:
gv_pml_depth["y0"] = thickness["y0"] - self.grid_view.ys
if thickness["z0"] - self.grid_view.zs > 0:
gv_pml_depth["z0"] = thickness["z0"] - self.grid_view.zs
if self.grid_view.xf > grid.nx - thickness["xmax"]:
gv_pml_depth["xmax"] = self.grid_view.xf - (grid.nx - thickness["xmax"])
if self.grid_view.yf > grid.ny - thickness["ymax"]:
gv_pml_depth["ymax"] = self.grid_view.yf - (grid.ny - thickness["ymax"])
if self.grid_view.zf > grid.nz - thickness["zmax"]:
gv_pml_depth["zmax"] = self.grid_view.zf - (grid.nz - thickness["zmax"])
return np.array(list(gv_pml_depth.values()), dtype=np.int64)
def srcs_rx_gv_comment(
self, srcs: Union[Sequence[Source], List[Rx]]
) -> Optional[Tuple[List[str], npt.NDArray[np.float64]]]:
"""Used to name sources and/or receivers."""
if not srcs:
return None
names: List[str] = []
positions = np.empty((len(srcs), 3))
for index, src in enumerate(srcs):
position = src.coord * self.grid.dl
names.append(src.ID)
positions[index] = position
return names, positions
def dx_dy_dz_comment(self) -> npt.NDArray[np.float64]:
return self.grid.dl
def nx_ny_nz_comment(self) -> npt.NDArray[np.int32]:
return self.grid.size
def materials_comment(self) -> Optional[List[str]]:
if hasattr(self.grid_view, "materials"):
materials = self.grid_view.materials
else:
materials = self.grid.materials
if materials is None:
return None
if not self.averaged_materials:
return [m.ID for m in materials if m.type != "dielectric-smoothed"]
else:
return [m.ID for m in materials]
class MPIMetadata(Metadata[MPIGrid]):
def nx_ny_nz_comment(self) -> npt.NDArray[np.int32]:
return self.grid.global_size
def pml_gv_comment(self) -> Optional[npt.NDArray[np.int64]]:
gv_pml_depth = super().pml_gv_comment()
if gv_pml_depth is None:
gv_pml_depth = np.zeros(6, dtype=np.int64)
assert isinstance(self.grid_view, MPIGridView)
recv_buffer = np.empty((self.grid_view.comm.size, 6), dtype=np.int64)
self.grid_view.comm.Allgather(gv_pml_depth, recv_buffer)
gv_pml_depth = np.max(recv_buffer, axis=0)
return None if all(gv_pml_depth == 0) else gv_pml_depth
def srcs_rx_gv_comment(
self, srcs: Union[Sequence[Source], List[Rx]]
) -> Optional[Tuple[List[str], npt.NDArray[np.float64]]]:
objects: Dict[str, npt.NDArray[np.float64]] = {}
for src in srcs:
position = self.grid.local_to_global_coordinate(src.coord) * self.grid.dl
objects[src.ID] = position
assert isinstance(self.grid_view, MPIGridView)
global_objects: List[Dict[str, npt.NDArray[np.float64]]] = self.grid_view.comm.allgather(
objects
)
objects = {k: v for d in global_objects for k, v in d.items()}
objects = dict(sorted(objects.items()))
return (list(objects.keys()), np.array(list(objects.values()))) if objects else None
def write_material_ids(self, file_handler: VtkHdfFile):
assert isinstance(self.grid_view, MPIGridView)
# Only rank 0 has all the material data. However, creating the
# 'material_ids' dataset is a collective operation, so all ranks
# need to know the shape and datatype of the dataset.
if self.materials is None:
buffer = np.empty(2, dtype=np.int32)
else:
shape = len(self.materials)
max_length = max([len(m) for m in self.materials])
buffer = np.array([shape, max_length], dtype=np.int32)
self.grid_view.comm.Bcast([buffer, MPI.INT32_T])
shape, max_length = buffer
dtype = h5py.string_dtype(length=int(max_length))
file_handler.add_field_data("material_ids", self.materials, shape=(shape,), dtype=dtype)

查看文件

@@ -0,0 +1,531 @@
import logging
from itertools import chain
from typing import Generic, Tuple
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from typing_extensions import TypeVar
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.materials import Material
logger = logging.getLogger(__name__)
GridType = TypeVar("GridType", bound=FDTDGrid)
class GridView(Generic[GridType]):
def __init__(
self,
grid: GridType,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
dx: int = 1,
dy: int = 1,
dz: int = 1,
):
"""Create a new GridView.
A grid view provides an interface to allow easy access to a
subsection of an FDTDGrid.
Args:
grid: Grid to create a view of.
xs: Start x coordinate of the grid view.
ys: Start y coordinate of the grid view.
zs: Start z coordinate of the grid view.
xf: End x coordinate of the grid view.
yf: End y coordinate of the grid view.
zf: End z coordinate of the grid view.
dx: Optional step size of the grid view in the x dimension. Defaults to 1.
dy: Optional step size of the grid view in the y dimension. Defaults to 1.
dz: Optional step size of the grid view in the z dimension. Defaults to 1.
"""
self.start = np.array([xs, ys, zs], dtype=np.int32)
self.stop = np.array([xf, yf, zf], dtype=np.int32)
self.step = np.array([dx, dy, dz], dtype=np.int32)
self.size = np.ceil((self.stop - self.start) / self.step).astype(np.int32)
self.grid = grid
self._ID = None
logger.debug(
f"Created GridView for grid '{self.grid.name}' (start={self.start}, stop={self.stop},"
f" step={self.step}, size={self.size})"
)
@property
def xs(self) -> int:
return self.start[0]
@property
def ys(self) -> int:
return self.start[1]
@property
def zs(self) -> int:
return self.start[2]
@property
def xf(self) -> int:
return self.stop[0]
@property
def yf(self) -> int:
return self.stop[1]
@property
def zf(self) -> int:
return self.stop[2]
@property
def nx(self) -> int:
return self.size[0]
@property
def ny(self) -> int:
return self.size[1]
@property
def nz(self) -> int:
return self.size[2]
def get_slice(self, dimension: int, upper_bound_exclusive: bool = True) -> slice:
"""Create a slice object for the specified dimension.
Args:
dimension: Dimension to create the slice object for. Values
0, 1, and 2 map to the x, y, and z dimensions
respectively.
upper_bound_exclusive: Optionally specify if the upper bound
of the slice should be exclusive or inclusive. Defaults
to True.
Returns:
slice: Slice object
"""
if upper_bound_exclusive:
stop = self.stop[dimension]
else:
stop = self.stop[dimension] + self.step[dimension]
return slice(self.start[dimension], stop, self.step[dimension])
def slice_array(self, array: npt.NDArray, upper_bound_exclusive: bool = True) -> npt.NDArray:
"""Slice an array according to the dimensions of the grid view.
It is assumed the last 3 dimensions of the provided array
represent the x, y, z spacial information. Other dimensions will
not be sliced.
E.g. For an array of shape (10, 100, 50, 50) this function would
return an array of shape (10, x, y, z) where x, y, and z are
specified by the size/shape of the grid view.
Args:
array: Array to slice. Must have at least 3 dimensions.
upper_bound_exclusive: Optionally specify if the upper bound
of the slice should be exclusive or inclusive. Defaults
to True.
Returns:
array: Sliced array
"""
return np.ascontiguousarray(
array[
...,
self.get_slice(0, upper_bound_exclusive),
self.get_slice(1, upper_bound_exclusive),
self.get_slice(2, upper_bound_exclusive),
]
)
def initialise_materials(self, filter_materials: bool = True):
"""Create a new ID map for materials in the grid view.
Rather than using the default material IDs (as per the main grid
object), we may want to create a new index for materials inside
this grid view. Unlike using the default material IDs, this new
index will be continuous from 0 - number of materials for the
materials in the grid view.
This function should be called before calling the
map_to_view_materials() function.
"""
# Get unique materials in the grid view
if filter_materials:
ID = self.get_ID(force_refresh=True)
materials_in_grid_view = np.unique(ID)
# Get actual Material objects
self.materials = np.array(self.grid.materials, dtype=Material)[materials_in_grid_view]
else:
self.materials = np.array(self.grid.materials, dtype=Material)
# Sort materials
self.materials.sort()
# Create map from material ID to 0 - number of materials
materials_map = {material.numID: index for index, material in enumerate(self.materials)}
self.map_materials_func = np.vectorize(lambda id: materials_map[id])
NDArrayType = TypeVar("NDArrayType", bound=npt.NDArray)
def map_to_view_materials(self, array: NDArrayType) -> NDArrayType:
"""Map from the main grid material IDs to the grid view IDs.
Ensure initialise_materials() has been called before using this
function.
Args:
array: Array to map.
Returns:
array: Mapped array.
"""
return self.map_materials_func(array).astype(array.dtype)
def get_ID(self, force_refresh=False) -> npt.NDArray[np.uint32]:
"""Get a view of the ID array.
By default, the slice of the ID array is cached to prevent
unnecessary reconstruction of the view on repeat calls. E.g.
from the initialise_materials() function as well as a user call
to get_ID().
Args:
force_refresh: Optionally force reloading the ID array from
the main grid object. Defaults to False.
Returns:
ID: View of the ID array.
"""
if self._ID is None or force_refresh:
self._ID = self.slice_array(self.grid.ID, upper_bound_exclusive=False)
return self._ID
def get_solid(self) -> npt.NDArray[np.uint32]:
"""Get a view of the solid array.
Returns:
solid: View of the solid array
"""
return self.slice_array(self.grid.solid)
def get_rigidE(self) -> npt.NDArray[np.int8]:
"""Get a view of the rigidE array.
Returns:
rigidE: View of the rigidE array
"""
return self.slice_array(self.grid.rigidE)
def get_rigidH(self) -> npt.NDArray[np.int8]:
"""Get a view of the rigidH array.
Returns:
rigidH: View of the rigidH array
"""
return self.slice_array(self.grid.rigidH)
def get_Ex(self) -> npt.NDArray[np.float32]:
"""Get a view of the Ex array.
Returns:
Ex: View of the Ex array
"""
return self.slice_array(self.grid.Ex, upper_bound_exclusive=False)
def get_Ey(self) -> npt.NDArray[np.float32]:
"""Get a view of the Ey array.
Returns:
Ey: View of the Ey array
"""
return self.slice_array(self.grid.Ey, upper_bound_exclusive=False)
def get_Ez(self) -> npt.NDArray[np.float32]:
"""Get a view of the Ez array.
Returns:
Ez: View of the Ez array
"""
return self.slice_array(self.grid.Ez, upper_bound_exclusive=False)
def get_Hx(self) -> npt.NDArray[np.float32]:
"""Get a view of the Hx array.
Returns:
Hx: View of the Hx array
"""
return self.slice_array(self.grid.Hx, upper_bound_exclusive=False)
def get_Hy(self) -> npt.NDArray[np.float32]:
"""Get a view of the Hy array.
Returns:
Hy: View of the Hy array
"""
return self.slice_array(self.grid.Hy, upper_bound_exclusive=False)
def get_Hz(self) -> npt.NDArray[np.float32]:
"""Get a view of the Hz array.
Returns:
Hz: View of the Hz array
"""
return self.slice_array(self.grid.Hz, upper_bound_exclusive=False)
class MPIGridView(GridView[MPIGrid]):
def __init__(
self,
grid: MPIGrid,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
dx: int = 1,
dy: int = 1,
dz: int = 1,
):
"""Create a new MPIGridView.
An MPI grid view provides an interface to allow easy access to a
subsection of an MPIGrid.
Args:
grid: MPI grid to create a view of.
xs: Start x coordinate of the grid view.
ys: Start y coordinate of the grid view.
zs: Start z coordinate of the grid view.
xf: End x coordinate of the grid view.
yf: End y coordinate of the grid view.
zf: End z coordinate of the grid view.
dx: Optional step size of the grid view in the x dimension. Defaults to 1.
dy: Optional step size of the grid view in the y dimension. Defaults to 1.
dz: Optional step size of the grid view in the z dimension. Defaults to 1.
"""
super().__init__(grid, xs, ys, zs, xf, yf, zf, dx, dy, dz)
comm = grid.comm.Split()
assert isinstance(comm, MPI.Intracomm)
# Calculate start, stop and size for the global grid view
self.global_start = self.grid.local_to_global_coordinate(self.start)
self.global_stop = self.grid.local_to_global_coordinate(self.stop)
self.global_size = self.size
# Create new cartesean communicator by finding MPI grid coords
# for the start and end of the grid view.
# Subtract 1 from global_stop as the upper extent is exclusive
# meaning the last coordinate included in the grid view is
# actually (global_stop - 1).
start_grid_coord = grid.get_grid_coord_from_coordinate(self.global_start)
stop_grid_coord = grid.get_grid_coord_from_coordinate(self.global_stop - 1) + 1
self.comm = comm.Create_cart((stop_grid_coord - start_grid_coord).tolist())
self.has_negative_neighbour = self.start < self.grid.negative_halo_offset
# Bring start into the local grid (and not in the negative halo)
# start must still be aligned with the provided step.
self.start = np.where(
self.has_negative_neighbour,
self.grid.negative_halo_offset
+ ((self.start - self.grid.negative_halo_offset) % self.step),
self.start,
)
self.has_positive_neighbour = self.stop > self.grid.size
# Limit stop such that it is at most one step beyond the max
# index of the grid. As stop is the upper bound, it is
# exclusive, meaning when used to slice an array (with the
# provided step), the last element accessed will one step below
# stop.
# Note: using self.grid.size as an index in any dimension would
# fall in the positive halo (this counts as outside the local
# grid).
self.stop = np.where(
self.has_positive_neighbour,
self.grid.size + ((self.stop - self.grid.size) % self.step),
self.stop,
)
# Calculate offset for the local grid view
self.offset = self.grid.local_to_global_coordinate(self.start) - self.global_start
# Update local size
self.size = np.ceil((self.stop - self.start) / self.step).astype(np.int32)
logger.debug(
f"Created MPIGridView for grid '{self.grid.name}' (global_start={self.global_start},"
f" global_stop={self.global_stop}, global_size={self.global_size}, start={self.start},"
f" stop={self.stop}, step={self.step}, size={self.size}, offset={self.offset})"
)
@property
def gx(self) -> int:
return self.global_size[0]
@property
def gy(self) -> int:
return self.global_size[1]
@property
def gz(self) -> int:
return self.global_size[2]
def get_slice(self, dimension: int, upper_bound_exclusive: bool = True) -> slice:
"""Create a slice object for the specified dimension.
Args:
dimension: Dimension to create the slice object for. Values
0, 1, and 2 map to the x, y, and z dimensions
respectively.
upper_bound_exclusive: Optionally specify if the upper bound
of the slice should be exclusive or inclusive. Defaults
to True.
Returns:
slice: Slice object
"""
if upper_bound_exclusive or self.has_positive_neighbour[dimension]:
stop = self.stop[dimension]
else:
# Make slice of array one step larger if this rank does not
# have a positive neighbour
stop = self.stop[dimension] + self.step[dimension]
return slice(self.start[dimension], stop, self.step[dimension])
def get_output_slice(self, dimension: int, upper_bound_exclusive: bool = True) -> slice:
"""Create an output slice object for the specified dimension.
This provides a slice of the grid view for the section of the
grid view managed by this rank. This can be used when writing
out arrays provided by the grid view as part of a collective
operation.
For example:
```
dset_slice = (
grid_view.get_output_slice(0),
grid_view.get_output_slice(1),
grid_view.get_output_slice(2),
)
dset[dset_slice] = grid_view.get_solid()
```
Args:
dimension: Dimension to create the slice object for. Values
0, 1, and 2 map to the x, y, and z dimensions
respectively.
upper_bound_exclusive: Optionally specify if the upper bound
of the slice should be exclusive or inclusive. Defaults
to True.
Returns:
slice: Slice object
"""
if upper_bound_exclusive or self.has_positive_neighbour[dimension]:
size = self.size[dimension]
else:
# Make slice of array one step larger if this rank does not
# have a positive neighbour
size = self.size[dimension] + 1
offset = self.offset[dimension] // self.step[dimension]
return slice(offset, offset + size)
def get_3d_output_slice(self, upper_bound_exclusive: bool = True) -> Tuple[slice, slice, slice]:
"""Create a 3D output slice object.
This provides a slice of the grid view for the section of the
grid view managed by this rank. This can be used when writing
out arrays provided by the grid view as part of a collective
operation.
For example:
`dset[grid_view.get_3d_output_slice()] = grid_view.get_solid()`
Args:
upper_bound_exclusive: Optionally specify if the upper bound
of the slice should be exclusive or inclusive. Defaults
to True.
Returns:
slice: 3D Slice object
"""
return (
self.get_output_slice(0, upper_bound_exclusive),
self.get_output_slice(1, upper_bound_exclusive),
self.get_output_slice(2, upper_bound_exclusive),
)
def initialise_materials(self, filter_materials: bool = True):
"""Create a new ID map for materials in the grid view.
Rather than using the default material IDs (as per the main grid
object), we may want to create a new index for materials inside
this grid view. Unlike using the default material IDs, this new
index will be continuous from 0 - number of materials for the
materials in the grid view.
This function should only be called if required as it needs MPI
communication to construct the new map. It should also be called
before the map_to_view_materials() function.
"""
if filter_materials:
ID = self.get_ID(force_refresh=True)
local_material_ids = np.unique(ID)
local_materials = np.array(self.grid.materials, dtype=Material)[local_material_ids]
else:
local_materials = np.array(self.grid.materials, dtype=Material)
local_materials.sort()
local_material_ids = [m.numID for m in local_materials]
# Send all materials to the coordinating rank
materials_by_rank = self.comm.gather(local_materials, root=0)
if materials_by_rank is not None:
# Filter out duplicate materials and sort by material ID
all_materials = np.fromiter(chain.from_iterable(materials_by_rank), dtype=Material)
self.materials = np.unique(all_materials)
# The new material IDs corespond to each material's index in
# the sorted self.materials array. For each rank, get the
# new IDs of each material it sent to send back
for rank in range(1, self.comm.size):
new_material_ids = np.where(np.isin(self.materials, materials_by_rank[rank]))[0]
self.comm.Isend([new_material_ids.astype(np.int32), MPI.INT], rank)
new_material_ids = np.where(np.isin(self.materials, materials_by_rank[0]))[0]
new_material_ids = new_material_ids.astype(np.int32)
else:
self.materials = None
# Get list of global IDs for this rank's local materials
new_material_ids = np.empty(len(local_materials), dtype=np.int32)
self.comm.Recv([new_material_ids, MPI.INT], 0)
# Create map from local material ID to global material ID
materials_map = {
local_material_ids[index]: new_id for index, new_id in enumerate(new_material_ids)
}
# Create map from material ID to 0 - number of materials
self.map_materials_func = np.vectorize(lambda id: materials_map[id])

查看文件

@@ -18,12 +18,14 @@
import logging
from importlib import import_module
from typing import List
import numpy as np
from mpi4py import MPI
import gprMax.config as config
from .cython.pml_build import pml_average_er_mr
from .cython.pml_build import pml_average_er_mr, pml_sum_er_mr
logger = logging.getLogger(__name__)
@@ -90,7 +92,7 @@ class CFS:
self.kappa = CFSParameter(ID="kappa", scalingprofile="constant", min=1, max=1)
self.sigma = CFSParameter(ID="sigma", scalingprofile="quartic", min=0, max=None)
def calculate_sigmamax(self, d, er, mr, G):
def calculate_sigmamax(self, d, er, mr):
"""Calculates an optimum value for sigma max based on underlying
material properties.
@@ -126,8 +128,7 @@ class CFS:
"""
tmp = (
np.linspace(0, (len(Evalues) - 1) + 0.5, num=2 * len(Evalues))
/ (len(Evalues) - 1)
np.linspace(0, (len(Evalues) - 1) + 0.5, num=2 * len(Evalues)) / (len(Evalues) - 1)
) ** order
Evalues = tmp[0:-1:2]
Hvalues = tmp[1::2]
@@ -151,12 +152,8 @@ class CFS:
# Extra cell of thickness added to allow correct scaling of electric and
# magnetic values
Evalues = np.zeros(
thickness + 1, dtype=config.sim_config.dtypes["float_or_double"]
)
Hvalues = np.zeros(
thickness + 1, dtype=config.sim_config.dtypes["float_or_double"]
)
Evalues = np.zeros(thickness + 1, dtype=config.sim_config.dtypes["float_or_double"])
Hvalues = np.zeros(thickness + 1, dtype=config.sim_config.dtypes["float_or_double"])
if parameter.scalingprofile == "constant":
Evalues += parameter.max
@@ -209,7 +206,7 @@ class PML:
# x-axis, y-axis, or z-axis
directions = ["xminus", "yminus", "zminus", "xplus", "yplus", "zplus"]
def __init__(self, G, ID=None, direction=None, xs=0, xf=0, ys=0, yf=0, zs=0, zf=0):
def __init__(self, G, ID: str, direction: str, xs=0, xf=0, ys=0, yf=0, zs=0, zf=0):
"""
Args:
G: FDTDGrid class describing a grid in a model.
@@ -242,7 +239,7 @@ class PML:
self.d = self.G.dz
self.thickness = self.nz
self.CFS = self.G.pmls["cfs"]
self.CFS: List[CFS] = self.G.pmls["cfs"]
self.check_kappamin()
self.initialise_field_arrays()
@@ -255,8 +252,7 @@ class PML:
kappamin = sum(cfs.kappa.min for cfs in self.CFS)
if kappamin < 1:
logger.exception(
f"Sum of kappamin value(s) for PML is {kappamin} "
"and must be greater than one."
f"Sum of kappamin value(s) for PML is {kappamin} and must be greater than one."
)
raise ValueError
@@ -324,41 +320,36 @@ class PML:
"""
self.ERA = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.ERB = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.ERE = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.ERF = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.HRA = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.HRB = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.HRE = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
self.HRF = np.zeros(
(len(self.CFS), self.thickness),
dtype=config.sim_config.dtypes["float_or_double"],
(len(self.CFS), self.thickness), dtype=config.sim_config.dtypes["float_or_double"]
)
for x, cfs in enumerate(self.CFS):
if not cfs.sigma.max:
cfs.calculate_sigmamax(self.d, er, mr, self.G)
cfs.calculate_sigmamax(self.d, er, mr)
logger.debug(
f"PML {self.ID}: sigma.max set to {cfs.sigma.max} for {'first' if x == 0 else 'second'} order CFS parameter"
)
Ealpha, Halpha = cfs.calculate_values(self.thickness, cfs.alpha)
Ekappa, Hkappa = cfs.calculate_values(self.thickness, cfs.kappa)
Esigma, Hsigma = cfs.calculate_values(self.thickness, cfs.sigma)
@@ -369,9 +360,7 @@ class PML:
tmp = (2 * config.sim_config.em_consts["e0"] * Ekappa) + self.G.dt * (
Ealpha * Ekappa + Esigma
)
self.ERA[x, :] = (
2 * config.sim_config.em_consts["e0"] + self.G.dt * Ealpha
) / tmp
self.ERA[x, :] = (2 * config.sim_config.em_consts["e0"] + self.G.dt * Ealpha) / tmp
self.ERB[x, :] = (2 * config.sim_config.em_consts["e0"] * Ekappa) / tmp
self.ERE[x, :] = (
(2 * config.sim_config.em_consts["e0"] * Ekappa)
@@ -383,9 +372,7 @@ class PML:
tmp = (2 * config.sim_config.em_consts["e0"] * Hkappa) + self.G.dt * (
Halpha * Hkappa + Hsigma
)
self.HRA[x, :] = (
2 * config.sim_config.em_consts["e0"] + self.G.dt * Halpha
) / tmp
self.HRA[x, :] = (2 * config.sim_config.em_consts["e0"] + self.G.dt * Halpha) / tmp
self.HRB[x, :] = (2 * config.sim_config.em_consts["e0"] * Hkappa) / tmp
self.HRE[x, :] = (
(2 * config.sim_config.em_consts["e0"] * Hkappa)
@@ -419,8 +406,7 @@ class PML:
pmlmodule = "gprMax.cython.pml_updates_electric_" + self.G.pmls["formulation"]
func = getattr(
import_module(pmlmodule),
"order" + str(len(self.CFS)) + "_" + self.direction,
import_module(pmlmodule), "order" + str(len(self.CFS)) + "_" + self.direction
)
func(
self.xs,
@@ -454,8 +440,7 @@ class PML:
pmlmodule = "gprMax.cython.pml_updates_magnetic_" + self.G.pmls["formulation"]
func = getattr(
import_module(pmlmodule),
"order" + str(len(self.CFS)) + "_" + self.direction,
import_module(pmlmodule), "order" + str(len(self.CFS)) + "_" + self.direction
)
func(
self.xs,
@@ -701,6 +686,39 @@ class OpenCLPML(PML):
event.wait()
class MPIPML(PML):
comm: MPI.Cartcomm
global_comm: MPI.Comm
COORDINATOR_RANK = 0
def calculate_update_coeffs(self, er: float, mr: float):
"""Calculates electric and magnetic update coefficients for the PML.
Args:
er: float of average permittivity of underlying material
mr: float of average permeability of underlying material
"""
for cfs in self.CFS:
if not cfs.sigma.max:
if self.global_comm.rank == self.COORDINATOR_RANK:
cfs.calculate_sigmamax(self.d, er, mr)
buffer = np.array([cfs.sigma.max])
else:
buffer = np.empty(1)
# Needs to be non-blocking because some ranks will
# contain multiple PMLs, but the material properties for
# a PML cannot be calculated until all ranks have
# completed that stage. Therefore a blocking broadcast
# would wait for ranks that are stuck calculating the
# material properties of the PML.
self.global_comm.Ibcast(buffer, self.COORDINATOR_RANK).Wait()
cfs.sigma.max = buffer[0]
super().calculate_update_coeffs(er, mr)
def print_pml_info(G):
"""Prints information about PMLs.
@@ -709,11 +727,9 @@ def print_pml_info(G):
"""
# No PML
if all(value == 0 for value in G.pmls["thickness"].values()):
return f"\nPML boundaries [{G.name}]: switched off"
return f"PML boundaries [{G.name}]: switched off\n"
if all(
value == G.pmls["thickness"]["x0"] for value in G.pmls["thickness"].values()
):
if all(value == G.pmls["thickness"]["x0"] for value in G.pmls["thickness"].values()):
pmlinfo = str(G.pmls["thickness"]["x0"])
else:
pmlinfo = ""
@@ -722,138 +738,6 @@ def print_pml_info(G):
pmlinfo = pmlinfo[:-2]
return (
f"\nPML boundaries [{G.name}]: {{formulation: {G.pmls['formulation']}, "
f"order: {len(G.pmls['cfs'])}, thickness (cells): {pmlinfo}}}"
f"PML boundaries [{G.name}]: {{formulation: {G.pmls['formulation']}, "
f"order: {len(G.pmls['cfs'])}, thickness (cells): {pmlinfo}}}\n"
)
def build_pml(G, pml_ID, thickness):
"""Builds instances of the PML and calculates the initial parameters and
coefficients including setting profile (based on underlying material
er and mr from solid array).
Args:
G: FDTDGrid class describing a grid in a model.
pml_ID: string identifier of PML slab.
thickness: int with thickness of PML slab in cells.
"""
# Arrays to hold values of permittivity and permeability (avoids accessing
# Material class in Cython.)
ers = np.zeros(len(G.materials))
mrs = np.zeros(len(G.materials))
for i, m in enumerate(G.materials):
ers[i] = m.er
mrs[i] = m.mr
if config.sim_config.general["solver"] == "cpu":
pml_type = PML
elif config.sim_config.general["solver"] == "cuda":
pml_type = CUDAPML
elif config.sim_config.general["solver"] == "opencl":
pml_type = OpenCLPML
if pml_ID == "x0":
pml = pml_type(
G,
ID=pml_ID,
direction="xminus",
xs=0,
xf=thickness,
ys=0,
yf=G.ny,
zs=0,
zf=G.nz,
)
elif pml_ID == "xmax":
pml = pml_type(
G,
ID=pml_ID,
direction="xplus",
xs=G.nx - thickness,
xf=G.nx,
ys=0,
yf=G.ny,
zs=0,
zf=G.nz,
)
elif pml_ID == "y0":
pml = pml_type(
G,
ID=pml_ID,
direction="yminus",
xs=0,
xf=G.nx,
ys=0,
yf=thickness,
zs=0,
zf=G.nz,
)
elif pml_ID == "ymax":
pml = pml_type(
G,
ID=pml_ID,
direction="yplus",
xs=0,
xf=G.nx,
ys=G.ny - thickness,
yf=G.ny,
zs=0,
zf=G.nz,
)
elif pml_ID == "z0":
pml = pml_type(
G,
ID=pml_ID,
direction="zminus",
xs=0,
xf=G.nx,
ys=0,
yf=G.ny,
zs=0,
zf=thickness,
)
elif pml_ID == "zmax":
pml = pml_type(
G,
ID=pml_ID,
direction="zplus",
xs=0,
xf=G.nx,
ys=0,
yf=G.ny,
zs=G.nz - thickness,
zf=G.nz,
)
if pml_ID[0] == "x":
averageer, averagemr = pml_average_er_mr(
G.ny,
G.nz,
config.get_model_config().ompthreads,
G.solid[pml.xs, :, :],
ers,
mrs,
)
elif pml_ID[0] == "y":
averageer, averagemr = pml_average_er_mr(
G.nx,
G.nz,
config.get_model_config().ompthreads,
G.solid[:, pml.ys, :],
ers,
mrs,
)
elif pml_ID[0] == "z":
averageer, averagemr = pml_average_er_mr(
G.nx,
G.ny,
config.get_model_config().ompthreads,
G.solid[:, :, pml.zs],
ers,
mrs,
)
pml.calculate_update_coeffs(averageer, averagemr)
G.pmls["slabs"].append(pml)

查看文件

@@ -29,14 +29,58 @@ class Rx:
allowableoutputs_dev = allowableoutputs[:-3]
def __init__(self):
self.ID = None
self.ID: str
self.outputs = {}
self.xcoord = None
self.ycoord = None
self.zcoord = None
self.xcoordorigin = None
self.ycoordorigin = None
self.zcoordorigin = None
self.coord = np.zeros(3, dtype=np.int32)
self.coordorigin = np.zeros(3, dtype=np.int32)
@property
def xcoord(self) -> int:
return self.coord[0]
@xcoord.setter
def xcoord(self, value: int):
self.coord[0] = value
@property
def ycoord(self) -> int:
return self.coord[1]
@ycoord.setter
def ycoord(self, value: int):
self.coord[1] = value
@property
def zcoord(self) -> int:
return self.coord[2]
@zcoord.setter
def zcoord(self, value: int):
self.coord[2] = value
@property
def xcoordorigin(self) -> int:
return self.coordorigin[0]
@xcoordorigin.setter
def xcoordorigin(self, value: int):
self.coordorigin[0] = value
@property
def ycoordorigin(self) -> int:
return self.coordorigin[1]
@ycoordorigin.setter
def ycoordorigin(self, value: int):
self.coordorigin[1] = value
@property
def zcoordorigin(self) -> int:
return self.coordorigin[2]
@zcoordorigin.setter
def zcoordorigin(self, value: int):
self.coordorigin[2] = value
def htod_rx_arrays(G, queue=None):
@@ -103,6 +147,4 @@ def dtoh_rx_array(rxs_dev, rxcoords_dev, G):
and rx.zcoord == rxcoords_dev[rxd, 2]
):
for output in rx.outputs.keys():
rx.outputs[output] = rxs_dev[
Rx.allowableoutputs_dev.index(output), :, rxd
]
rx.outputs[output] = rxs_dev[Rx.allowableoutputs_dev.index(output), :, rxd]

查看文件

@@ -15,19 +15,26 @@
#
# 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, Sequence
from gprMax.cmds_geometry.add_grass import AddGrass
from gprMax.cmds_geometry.add_surface_roughness import AddSurfaceRoughness
from gprMax.cmds_geometry.add_surface_water import AddSurfaceWater
from gprMax.cmds_geometry.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.mpi_model import MPIModel
from gprMax.subgrids.user_objects import SubGridBase as SubGridUserBase
from gprMax.user_inputs import create_user_input_points
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__)
@@ -35,101 +42,85 @@ logger = logging.getLogger(__name__)
class Scene:
"""Scene stores all of the user created objects."""
def __init__(self):
self.multiple_cmds = []
self.single_cmds = []
self.geometry_cmds = []
self.essential_cmds = [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_obj(self, obj, grid):
"""Builds objects.
def build_model_objects(self, objects: Sequence[ModelUserObject], model: Model):
"""Builds objects in models.
Args:
obj: user object
grid: FDTDGrid class describing a grid in a model.
model: Model being built
"""
uip = create_user_input_points(grid, obj)
try:
obj.build(grid, 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):
"""Process all commands in any sub-grids."""
def build_grid_objects(self, objects: Sequence[GridUserObject], grid: FDTDGrid):
"""Builds objects in FDTDGrids.
def func(obj):
if isinstance(obj, SubGridUserBase):
return True
else:
return False
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
# Subgrid user objects
subgrid_cmds = list(filter(func, self.multiple_cmds))
def build_output_objects(
self, objects: Sequence[OutputUserObject], model: Model, grid: FDTDGrid
):
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
# Iterate through the user command objects under the subgrid user object
for sg_cmd in subgrid_cmds:
# When the subgrid is created its reference is attached to its user
# object. This reference allows the multi and geo user objects
# to build in the correct subgrid.
sg = sg_cmd.subgrid
self.process_cmds(sg_cmd.children_multiple, sg)
self.process_geocmds(sg_cmd.children_geometry, sg)
def process_cmds(self, commands, grid):
"""Process list of commands."""
cmds_sorted = sorted(commands, key=lambda cmd: cmd.order)
for obj in cmds_sorted:
self.build_obj(obj, grid)
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_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_obj(obj, grid)
return self
def process_singlecmds(self, G):
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 "
@@ -138,29 +129,61 @@ class Scene:
)
raise ValueError
self.process_cmds(cmds_unique, G)
self.build_model_objects(unique_commands, model)
def create_internal_objects(self, G):
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
presents the user with UserObjects in order to build the internal
Rx(), Cylinder() etc... objects.
"""
# Create pre-defined (built-in) materials
create_built_in_materials(G)
create_built_in_materials(model.G)
# Process commands that can only have a single instance
self.process_singlecmds(G)
self.process_single_use_objects(model)
# Process main grid multiple commands
self.process_cmds(self.multiple_cmds, G)
# Process multiple commands
self.process_multi_use_objects(model)
# Initialise geometry arrays for main and subgrids
for grid in [G] + G.subgrids:
for grid in [model.G] + model.subgrids:
grid.initialise_geometry_arrays()
# Process the main grid geometry commands
self.process_geocmds(self.geometry_cmds, G)
self.process_geometry_objects(self.geometry_objects, model.G)
# Process all the commands for subgrids
self.process_subgrid_cmds()
self.process_subgrid_objects(model)

查看文件

@@ -18,23 +18,28 @@
import logging
import sys
from enum import IntEnum, unique
from pathlib import Path
from typing import Dict, Generic, List
import h5py
import numpy as np
from evtk.hl import imageToVTK
from mpi4py import MPI
from tqdm import tqdm
import gprMax.config as config
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.output_controllers.grid_view import GridType, GridView, MPIGridView
from ._version import __version__
from .cython.snapshots import calculate_snapshot_fields
from .utilities.utilities import get_terminal_width, round_value
from .utilities.utilities import get_terminal_width
logger = logging.getLogger(__name__)
def save_snapshots(grid):
def save_snapshots(snapshots: List["Snapshot"]):
"""Saves snapshots to file(s).
Args:
@@ -47,27 +52,25 @@ def save_snapshots(grid):
logger.info("")
logger.info(f"Snapshot directory: {snapshotdir.resolve()}")
for i, snap in enumerate(grid.snapshots):
fn = snapshotdir / Path(snap.filename)
for i, snap in enumerate(snapshots):
fn = snapshotdir / snap.filename
snap.filename = fn.with_suffix(snap.fileext)
pbar = tqdm(
total=snap.nbytes,
leave=True,
unit="byte",
unit_scale=True,
desc=f"Writing snapshot file {i + 1} "
f"of {len(grid.snapshots)}, "
f"{snap.filename.name}",
desc=f"Writing snapshot file {i + 1} of {len(snapshots)}, {snap.filename.name}",
ncols=get_terminal_width() - 1,
file=sys.stdout,
disable=not config.sim_config.general["progressbars"],
)
snap.write_file(pbar, grid)
snap.write_file(pbar)
pbar.close()
logger.info("")
class Snapshot:
class Snapshot(Generic[GridType]):
"""Snapshots of the electric and magnetic field values."""
allowableoutputs = {
@@ -93,21 +96,26 @@ class Snapshot:
# GPU - blocks per grid - set according to largest requested snapshot
bpg = None
@property
def GRID_VIEW_TYPE(self) -> type[GridView]:
return GridView
def __init__(
self,
xs=None,
ys=None,
zs=None,
xf=None,
yf=None,
zf=None,
dx=None,
dy=None,
dz=None,
time=None,
filename=None,
fileext=None,
outputs=None,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
dx: int,
dy: int,
dz: int,
time: int,
filename: str,
fileext: str,
outputs: Dict[str, bool],
grid: GridType,
):
"""
Args:
@@ -120,32 +128,62 @@ class Snapshot:
"""
self.fileext = fileext
self.filename = filename
self.filename = Path(filename)
self.time = time
self.outputs = outputs
self.xs = xs
self.ys = ys
self.zs = zs
self.xf = xf
self.yf = yf
self.zf = zf
self.dx = dx
self.dy = dy
self.dz = dz
self.nx = round_value((self.xf - self.xs) / self.dx)
self.ny = round_value((self.yf - self.ys) / self.dy)
self.nz = round_value((self.zf - self.zs) / self.dz)
self.sx = slice(self.xs, self.xf + self.dx, self.dx)
self.sy = slice(self.ys, self.yf + self.dy, self.dy)
self.sz = slice(self.zs, self.zf + self.dz, self.dz)
self.grid_view = self.GRID_VIEW_TYPE(grid, xs, ys, zs, xf, yf, zf, dx, dy, dz)
self.nbytes = 0
# Create arrays to hold the field data for snapshot
self.snapfields = {}
@property
def grid(self) -> GridType:
return self.grid_view.grid
# Properties for backwards compatibility
@property
def xs(self) -> int:
return self.grid_view.xs
@property
def ys(self) -> int:
return self.grid_view.ys
@property
def zs(self) -> int:
return self.grid_view.zs
@property
def xf(self) -> int:
return self.grid_view.xf
@property
def yf(self) -> int:
return self.grid_view.yf
@property
def zf(self) -> int:
return self.grid_view.zf
@property
def nx(self) -> int:
return self.grid_view.nx
@property
def ny(self) -> int:
return self.grid_view.ny
@property
def nz(self) -> int:
return self.grid_view.nz
def initialise_snapfields(self):
for k, v in self.outputs.items():
if v:
self.snapfields[k] = np.zeros(
(self.nx, self.ny, self.nz),
self.grid_view.size,
dtype=config.sim_config.dtypes["float_or_double"],
)
self.nbytes += self.snapfields[k].nbytes
@@ -156,7 +194,7 @@ class Snapshot:
(1, 1, 1), dtype=config.sim_config.dtypes["float_or_double"]
)
def store(self, G):
def store(self):
"""Store (in memory) electric and magnetic field values for snapshot.
Args:
@@ -164,12 +202,12 @@ class Snapshot:
"""
# Memory views of field arrays to dimensions required for the snapshot
Exslice = np.ascontiguousarray(G.Ex[self.sx, self.sy, self.sz])
Eyslice = np.ascontiguousarray(G.Ey[self.sx, self.sy, self.sz])
Ezslice = np.ascontiguousarray(G.Ez[self.sx, self.sy, self.sz])
Hxslice = np.ascontiguousarray(G.Hx[self.sx, self.sy, self.sz])
Hyslice = np.ascontiguousarray(G.Hy[self.sx, self.sy, self.sz])
Hzslice = np.ascontiguousarray(G.Hz[self.sx, self.sy, self.sz])
Exslice = self.grid_view.get_Ex()
Eyslice = self.grid_view.get_Ey()
Ezslice = self.grid_view.get_Ez()
Hxslice = self.grid_view.get_Hx()
Hyslice = self.grid_view.get_Hy()
Hzslice = self.grid_view.get_Hz()
# Calculate field values at points (comes from averaging field
# components in cells)
@@ -198,7 +236,7 @@ class Snapshot:
self.snapfields["Hz"],
)
def write_file(self, pbar, G):
def write_file(self, pbar: tqdm):
"""Writes snapshot file either as VTK ImageData (.vti) format
or HDF5 format (.h5) files
@@ -208,16 +246,15 @@ class Snapshot:
"""
if self.fileext == ".vti":
self.write_vtk(pbar, G)
self.write_vtk(pbar)
elif self.fileext == ".h5":
self.write_hdf5(pbar, G)
self.write_hdf5(pbar)
def write_vtk(self, pbar, G):
def write_vtk(self, pbar: tqdm):
"""Writes snapshot file in VTK ImageData (.vti) format.
Args:
pbar: Progress bar class instance.
G: FDTDGrid class describing a grid in a model.
"""
celldata = {
@@ -226,14 +263,13 @@ class Snapshot:
if self.outputs.get(k)
}
origin = self.grid_view.start * self.grid.dl
spacing = self.grid_view.step * self.grid.dl
imageToVTK(
str(self.filename.with_suffix("")),
origin=(
(self.xs * self.dx * G.dx),
(self.ys * self.dy * G.dy),
(self.zs * self.dz * G.dz),
),
spacing=((self.dx * G.dx), (self.dy * G.dy), (self.dz * G.dz)),
origin=tuple(origin),
spacing=tuple(spacing),
cellData=celldata,
)
@@ -245,20 +281,20 @@ class Snapshot:
* np.dtype(config.sim_config.dtypes["float_or_double"]).itemsize
)
def write_hdf5(self, pbar, G):
def write_hdf5(self, pbar: tqdm):
"""Writes snapshot file in HDF5 (.h5) format.
Args:
pbar: Progress bar class instance.
G: FDTDGrid class describing a grid in a model.
"""
f = h5py.File(self.filename, "w")
f.attrs["gprMax"] = __version__
f.attrs["Title"] = G.title
f.attrs["nx_ny_nz"] = (self.nx, self.ny, self.nz)
f.attrs["dx_dy_dz"] = (self.dx * G.dx, self.dy * G.dy, self.dz * G.dz)
f.attrs["time"] = self.time * G.dt
# TODO: Output model name (title) and grid name? in snapshot output
# f.attrs["Title"] = G.title
f.attrs["nx_ny_nz"] = tuple(self.grid_view.size)
f.attrs["dx_dy_dz"] = self.grid_view.step * self.grid.dl
f.attrs["time"] = self.time * self.grid.dt
for key in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]:
if self.outputs[key]:
@@ -268,7 +304,270 @@ class Snapshot:
f.close()
def htod_snapshot_array(G, queue=None):
@unique
class Dim(IntEnum):
X = 0
Y = 1
Z = 2
@unique
class Dir(IntEnum):
NEG = 0
POS = 1
class MPISnapshot(Snapshot[MPIGrid]):
H_TAG = 0
EX_TAG = 1
EY_TAG = 2
EZ_TAG = 3
@property
def GRID_VIEW_TYPE(self) -> type[MPIGridView]:
return MPIGridView
def __init__(
self,
xs: int,
ys: int,
zs: int,
xf: int,
yf: int,
zf: int,
dx: int,
dy: int,
dz: int,
time: int,
filename: str,
fileext: str,
outputs: Dict[str, bool],
grid: MPIGrid,
):
super().__init__(xs, ys, zs, xf, yf, zf, dx, dy, dz, time, filename, fileext, outputs, grid)
assert isinstance(self.grid_view, self.GRID_VIEW_TYPE)
self.comm = self.grid_view.comm
# Get neighbours
self.neighbours = np.full((3, 2), -1, dtype=int)
self.neighbours[Dim.X] = self.comm.Shift(direction=Dim.X, disp=1)
self.neighbours[Dim.Y] = self.comm.Shift(direction=Dim.Y, disp=1)
self.neighbours[Dim.Z] = self.comm.Shift(direction=Dim.Z, disp=1)
def has_neighbour(self, dimension: Dim, direction: Dir) -> bool:
return self.neighbours[dimension][direction] != -1
def store(self):
"""Store (in memory) electric and magnetic field values for snapshot.
Args:
G: FDTDGrid class describing a grid in a model.
"""
logger.debug(f"Saving snapshot for iteration: {self.time}")
# Memory views of field arrays to dimensions required for the snapshot
Exslice = self.grid_view.get_Ex()
Eyslice = self.grid_view.get_Ey()
Ezslice = self.grid_view.get_Ez()
Hxslice = self.grid_view.get_Hx()
Hyslice = self.grid_view.get_Hy()
Hzslice = self.grid_view.get_Hz()
"""
Halos required by each field to average field components:
Exslice - y + z halo
Eyslice - x + z halo
Ezslice - x + y halo
Hxslice - x halo
Hyslice - y halo
Hzslice - z halo
"""
# Shape and dtype should be the same for all field array slices
shape = Hxslice.shape
dtype = Hxslice.dtype
Hxhalo = np.empty((1, shape[Dim.Y], shape[Dim.Z]), dtype=dtype)
Hyhalo = np.empty((shape[Dim.X], 1, shape[Dim.Z]), dtype=dtype)
Hzhalo = np.empty((shape[Dim.X], shape[Dim.Y], 1), dtype=dtype)
Exyhalo = np.empty((shape[Dim.X], 1, shape[Dim.Z]), dtype=dtype)
Eyzhalo = np.empty((shape[Dim.X], shape[Dim.Y], 1), dtype=dtype)
Ezxhalo = np.empty((1, shape[Dim.Y], shape[Dim.Z]), dtype=dtype)
x_offset = self.has_neighbour(Dim.X, Dir.POS)
y_offset = self.has_neighbour(Dim.Y, Dir.POS)
z_offset = self.has_neighbour(Dim.Z, Dir.POS)
Exzhalo = np.empty((shape[Dim.X], shape[Dim.Y] + y_offset, 1), dtype=dtype)
Eyxhalo = np.empty((1, shape[Dim.Y], shape[Dim.Z] + z_offset), dtype=dtype)
Ezyhalo = np.empty((shape[Dim.X] + x_offset, 1, shape[Dim.Z]), dtype=dtype)
blocking_requests: List[MPI.Request] = []
requests: List[MPI.Request] = []
if self.has_neighbour(Dim.X, Dir.NEG):
requests += [
self.comm.Isend(Hxslice[0, :, :], self.neighbours[Dim.X][Dir.NEG], self.H_TAG),
self.comm.Isend(Ezslice[0, :, :], self.neighbours[Dim.X][Dir.NEG], self.EZ_TAG),
]
if self.has_neighbour(Dim.X, Dir.POS):
blocking_requests.append(
self.comm.Irecv(Ezxhalo, self.neighbours[Dim.X][Dir.POS], self.EZ_TAG),
)
requests += [
self.comm.Irecv(Hxhalo, self.neighbours[Dim.X][Dir.POS], self.H_TAG),
self.comm.Irecv(Eyxhalo, self.neighbours[Dim.X][Dir.POS], self.EY_TAG),
]
if self.has_neighbour(Dim.Y, Dir.NEG):
requests += [
self.comm.Isend(
np.ascontiguousarray(Hyslice[:, 0, :]),
self.neighbours[Dim.Y][Dir.NEG],
self.H_TAG,
),
self.comm.Isend(
np.ascontiguousarray(Exslice[:, 0, :]),
self.neighbours[Dim.Y][Dir.NEG],
self.EX_TAG,
),
]
if self.has_neighbour(Dim.Y, Dir.POS):
blocking_requests.append(
self.comm.Irecv(Exyhalo, self.neighbours[Dim.Y][Dir.POS], self.EX_TAG),
)
requests += [
self.comm.Irecv(Hyhalo, self.neighbours[Dim.Y][Dir.POS], self.H_TAG),
self.comm.Irecv(Ezyhalo, self.neighbours[Dim.Y][Dir.POS], self.EZ_TAG),
]
if self.has_neighbour(Dim.Z, Dir.NEG):
requests += [
self.comm.Isend(
np.ascontiguousarray(Hzslice[:, :, 0]),
self.neighbours[Dim.Z][Dir.NEG],
self.H_TAG,
),
self.comm.Isend(
np.ascontiguousarray(Eyslice[:, :, 0]),
self.neighbours[Dim.Z][Dir.NEG],
self.EY_TAG,
),
]
if self.has_neighbour(Dim.Z, Dir.POS):
blocking_requests.append(
self.comm.Irecv(Eyzhalo, self.neighbours[Dim.Z][Dir.POS], self.EY_TAG),
)
requests += [
self.comm.Irecv(Hzhalo, self.neighbours[Dim.Z][Dir.POS], self.H_TAG),
self.comm.Irecv(Exzhalo, self.neighbours[Dim.Z][Dir.POS], self.EX_TAG),
]
if len(blocking_requests) > 0:
blocking_requests[0].Waitall(blocking_requests)
logger.debug(f"Initial halo exchanges complete")
if self.has_neighbour(Dim.X, Dir.POS):
Ezslice = np.concatenate((Ezslice, Ezxhalo), axis=Dim.X)
if self.has_neighbour(Dim.Y, Dir.POS):
Exslice = np.concatenate((Exslice, Exyhalo), axis=Dim.Y)
if self.has_neighbour(Dim.Z, Dir.POS):
Eyslice = np.concatenate((Eyslice, Eyzhalo), axis=Dim.Z)
if self.has_neighbour(Dim.X, Dir.NEG):
requests.append(
self.comm.Isend(Eyslice[0, :, :], self.neighbours[Dim.X][Dir.NEG], self.EY_TAG),
)
if self.has_neighbour(Dim.Y, Dir.NEG):
requests.append(
self.comm.Isend(
np.ascontiguousarray(Ezslice[:, 0, :]),
self.neighbours[Dim.Y][Dir.NEG],
self.EZ_TAG,
),
)
if self.has_neighbour(Dim.Z, Dir.NEG):
requests.append(
self.comm.Isend(
np.ascontiguousarray(Exslice[:, :, 0]),
self.neighbours[Dim.Z][Dir.NEG],
self.EX_TAG,
),
)
if len(requests) > 0:
requests[0].Waitall(requests)
logger.debug(f"All halo exchanges complete")
if self.has_neighbour(Dim.X, Dir.POS):
Eyslice = np.concatenate((Eyslice, Eyxhalo), axis=Dim.X)
Hxslice = np.concatenate((Hxslice, Hxhalo), axis=Dim.X)
if self.has_neighbour(Dim.Y, Dir.POS):
Ezslice = np.concatenate((Ezslice, Ezyhalo), axis=Dim.Y)
Hyslice = np.concatenate((Hyslice, Hyhalo), axis=Dim.Y)
if self.has_neighbour(Dim.Z, Dir.POS):
Exslice = np.concatenate((Exslice, Exzhalo), axis=Dim.Z)
Hzslice = np.concatenate((Hzslice, Hzhalo), axis=Dim.Z)
# Calculate field values at points (comes from averaging field
# components in cells)
calculate_snapshot_fields(
self.nx,
self.ny,
self.nz,
config.get_model_config().ompthreads,
self.outputs["Ex"],
self.outputs["Ey"],
self.outputs["Ez"],
self.outputs["Hx"],
self.outputs["Hy"],
self.outputs["Hz"],
Exslice,
Eyslice,
Ezslice,
Hxslice,
Hyslice,
Hzslice,
self.snapfields["Ex"],
self.snapfields["Ey"],
self.snapfields["Ez"],
self.snapfields["Hx"],
self.snapfields["Hy"],
self.snapfields["Hz"],
)
def write_hdf5(self, pbar: tqdm):
"""Writes snapshot file in HDF5 (.h5) format.
Args:
pbar: Progress bar class instance.
"""
assert isinstance(self.grid_view, self.GRID_VIEW_TYPE)
f = h5py.File(self.filename, "w", driver="mpio", comm=self.comm)
f.attrs["gprMax"] = __version__
# TODO: Output model name (title) and grid name? in snapshot output
# f.attrs["Title"] = G.title
f.attrs["nx_ny_nz"] = self.grid_view.global_size
f.attrs["dx_dy_dz"] = self.grid_view.step * self.grid.dl
f.attrs["time"] = self.time * self.grid.dt
dset_slice = self.grid_view.get_3d_output_slice()
for key in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]:
if self.outputs[key]:
dset = f.create_dataset(key, self.grid_view.global_size)
dset[dset_slice] = self.snapfields[key]
pbar.update(n=self.snapfields[key].nbytes)
f.close()
def htod_snapshot_array(snapshots: List[Snapshot], queue=None):
"""Initialises arrays on compute device to store field data for snapshots.
Args:
@@ -280,7 +579,7 @@ def htod_snapshot_array(G, queue=None):
"""
# Get dimensions of largest requested snapshot
for snap in G.snapshots:
for snap in snapshots:
if snap.nx > Snapshot.nx_max:
Snapshot.nx_max = snap.nx
if snap.ny > Snapshot.ny_max:
@@ -293,8 +592,7 @@ def htod_snapshot_array(G, queue=None):
Snapshot.bpg = (
int(
np.ceil(
((Snapshot.nx_max) * (Snapshot.ny_max) * (Snapshot.nz_max))
/ Snapshot.tpb[0]
((Snapshot.nx_max) * (Snapshot.ny_max) * (Snapshot.nz_max)) / Snapshot.tpb[0]
)
),
1,
@@ -311,9 +609,7 @@ def htod_snapshot_array(G, queue=None):
# 4D arrays to store snapshots on GPU, e.g. snapEx(time, x, y, z);
# if snapshots are not being stored on the GPU during the simulation then
# they are copied back to the host after each iteration, hence numsnaps = 1
numsnaps = (
1 if config.get_model_config().device["snapsgpu2cpu"] else len(G.snapshots)
)
numsnaps = 1 if config.get_model_config().device["snapsgpu2cpu"] else len(snapshots)
snapEx = np.zeros(
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
dtype=config.sim_config.dtypes["float_or_double"],
@@ -375,21 +671,9 @@ def dtoh_snapshot_array(
snap: Snapshot class instance
"""
snap.snapfields["Ex"] = snapEx_dev[
i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf
]
snap.snapfields["Ey"] = snapEy_dev[
i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf
]
snap.snapfields["Ez"] = snapEz_dev[
i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf
]
snap.snapfields["Hx"] = snapHx_dev[
i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf
]
snap.snapfields["Hy"] = snapHy_dev[
i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf
]
snap.snapfields["Hz"] = snapHz_dev[
i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf
]
snap.snapfields["Ex"] = snapEx_dev[i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf]
snap.snapfields["Ey"] = snapEy_dev[i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf]
snap.snapfields["Ez"] = snapEz_dev[i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf]
snap.snapfields["Hx"] = snapHx_dev[i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf]
snap.snapfields["Hy"] = snapHy_dev[i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf]
snap.snapfields["Hz"] = snapHz_dev[i, snap.xs : snap.xf, snap.ys : snap.yf, snap.zs : snap.zf]

查看文件

@@ -16,76 +16,30 @@
# 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
import gprMax.config as config
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.model import Model
from gprMax.updates.mpi_updates import MPIUpdates
from .grid import CUDAGrid, FDTDGrid, OpenCLGrid
from .grid.cuda_grid import CUDAGrid
from .grid.fdtd_grid import FDTDGrid
from .grid.opencl_grid import OpenCLGrid
from .subgrids.updates import SubgridUpdates
from .subgrids.updates import create_updates as create_subgrid_updates
from .updates import CPUUpdates, CUDAUpdates, OpenCLUpdates
from .updates.cpu_updates import CPUUpdates
from .updates.cuda_updates import CUDAUpdates
from .updates.opencl_updates import OpenCLUpdates
from .updates.updates import Updates
def create_G():
"""Create grid object according to solver.
Returns:
G: FDTDGrid class describing a grid in a model.
"""
if config.sim_config.general["solver"] == "cpu":
G = FDTDGrid()
elif config.sim_config.general["solver"] == "cuda":
G = CUDAGrid()
elif config.sim_config.general["solver"] == "opencl":
G = OpenCLGrid()
return G
def create_solver(G):
"""Create configured solver object.
N.B. A large range of different functions exist to advance the time step for
dispersive materials. The correct function is set by the
set_dispersive_updates method, based on the required numerical
precision and dispersive material type.
This is done for solvers running on CPU, i.e. where Cython is used.
CUDA and OpenCL dispersive material functions are handled through
templating and substitution at runtime.
Args:
G: FDTDGrid class describing a grid in a model.
Returns:
solver: Solver object.
"""
if config.sim_config.general["subgrid"]:
updates = create_subgrid_updates(G)
if config.get_model_config().materials["maxpoles"] != 0:
# Set dispersive update functions for both SubgridUpdates and
# SubgridUpdaters subclasses
updates.set_dispersive_updates()
for u in updates.updaters:
u.set_dispersive_updates()
solver = Solver(updates, hsg=True)
elif config.sim_config.general["solver"] == "cpu":
updates = CPUUpdates(G)
if config.get_model_config().materials["maxpoles"] != 0:
updates.set_dispersive_updates()
solver = Solver(updates)
elif config.sim_config.general["solver"] == "cuda":
updates = CUDAUpdates(G)
solver = Solver(updates)
elif config.sim_config.general["solver"] == "opencl":
updates = OpenCLUpdates(G)
solver = Solver(updates)
return solver
logger = logging.getLogger(__name__)
class Solver:
"""Generic solver for Update objects"""
def __init__(self, updates, hsg=False):
def __init__(self, updates: Updates):
"""
Args:
updates: Updates contains methods to run FDTD algorithm.
@@ -93,7 +47,6 @@ class Solver:
"""
self.updates = updates
self.hsg = hsg
self.solvetime = 0
self.memused = 0
@@ -107,23 +60,75 @@ class Solver:
self.updates.time_start()
for iteration in iterator:
self.updates.store_outputs()
self.updates.store_outputs(iteration)
self.updates.store_snapshots(iteration)
self.updates.update_magnetic()
self.updates.update_magnetic_pml()
self.updates.update_magnetic_sources()
#self.updates.update_plane_waves()
if self.hsg:
self.updates.update_magnetic_sources(iteration)
# self.updates.update_plane_waves()
if isinstance(self.updates, MPIUpdates):
self.updates.halo_swap_magnetic()
if isinstance(self.updates, SubgridUpdates):
self.updates.hsg_2()
self.updates.update_electric_a()
self.updates.update_electric_pml()
self.updates.update_electric_sources()
if self.hsg:
self.updates.update_electric_sources(iteration)
# TODO: Increment iteration here if add Model to Solver
if isinstance(self.updates, SubgridUpdates):
self.updates.hsg_1()
self.updates.update_electric_b()
if config.sim_config.general["solver"] == "cuda":
if isinstance(self.updates, MPIUpdates):
self.updates.halo_swap_electric()
if isinstance(self.updates, CUDAUpdates):
self.memused = self.updates.calculate_memory_used(iteration)
self.updates.finalise()
self.solvetime = self.updates.calculate_solve_time()
self.updates.cleanup()
def create_solver(model: Model) -> Solver:
"""Create configured solver object.
N.B. A large range of different functions exist to advance the time
step for dispersive materials. The correct function is set by the
set_dispersive_updates method, based on the required numerical
precision and dispersive material type. This is done for solvers
running on CPU, i.e. where Cython is used. CUDA and OpenCL
dispersive material functions are handled through templating and
substitution at runtime.
Args:
model: model containing the main grid and subgrids.
Returns:
solver: Solver object.
"""
grid = model.G
if config.sim_config.general["subgrid"]:
updates = create_subgrid_updates(model)
if config.get_model_config().materials["maxpoles"] != 0:
# Set dispersive update functions for both SubgridUpdates and
# SubgridUpdaters subclasses
updates.set_dispersive_updates()
for u in updates.updaters:
u.set_dispersive_updates()
elif type(grid) is FDTDGrid:
updates = CPUUpdates(grid)
if config.get_model_config().materials["maxpoles"] != 0:
updates.set_dispersive_updates()
elif type(grid) is MPIGrid:
updates = MPIUpdates(grid)
if config.get_model_config().materials["maxpoles"] != 0:
updates.set_dispersive_updates()
elif type(grid) is CUDAGrid:
updates = CUDAUpdates(grid)
elif type(grid) is OpenCLGrid:
updates = OpenCLUpdates(grid)
else:
logger.error("Cannot create Solver: Unknown grid type")
raise ValueError
solver = Solver(updates)
return solver

查看文件

@@ -19,41 +19,85 @@
from copy import deepcopy
import numpy as np
import numpy.typing as npt
import gprMax.config as config
from .fields_outputs import Ix, Iy, Iz
from .utilities.utilities import round_value
from gprMax.waveforms import Waveform
from .cython.plane_wave import (
calculate1DWaveformValues,
getIntegerForAngles,
getProjections,
calculate1DWaveformValues,
updatePlaneWave,
getSource,
updatePlaneWave,
)
from .utilities.utilities import round_value
class Source:
"""Super-class which describes a generic source."""
def __init__(self):
self.ID = None
self.ID: str
self.polarisation = None
self.xcoord = None
self.ycoord = None
self.zcoord = None
self.xcoordorigin = None
self.ycoordorigin = None
self.zcoordorigin = None
self.start = None
self.stop = None
self.coord = np.zeros(3, dtype=np.int32)
self.coordorigin = np.zeros(3, dtype=np.int32)
self.start = 0.0
self.stop = 0.0
self.waveformID = None
# Waveform values for sources that need to be calculated on whole timesteps
self.waveformvalues_wholedt = None
# Waveform values for sources that need to be calculated on half timesteps
self.waveformvalues_halfdt = None
@property
def xcoord(self) -> int:
return self.coord[0]
@xcoord.setter
def xcoord(self, value: int):
self.coord[0] = value
@property
def ycoord(self) -> int:
return self.coord[1]
@ycoord.setter
def ycoord(self, value: int):
self.coord[1] = value
@property
def zcoord(self) -> int:
return self.coord[2]
@zcoord.setter
def zcoord(self, value: int):
self.coord[2] = value
@property
def xcoordorigin(self) -> int:
return self.coordorigin[0]
@xcoordorigin.setter
def xcoordorigin(self, value: int):
self.coordorigin[0] = value
@property
def ycoordorigin(self) -> int:
return self.coordorigin[1]
@ycoordorigin.setter
def ycoordorigin(self, value: int):
self.coordorigin[1] = value
@property
def zcoordorigin(self) -> int:
return self.coordorigin[2]
@zcoordorigin.setter
def zcoordorigin(self, value: int):
self.coordorigin[2] = value
class VoltageSource(Source):
"""A voltage source can be a hard source if it's resistance is zero,
@@ -103,9 +147,7 @@ class VoltageSource(Source):
self.waveformvalues_halfdt[iteration] = waveform.calculate_value(
time + 0.5 * G.dt, G.dt
)
self.waveformvalues_wholedt[iteration] = waveform.calculate_value(
time, G.dt
)
self.waveformvalues_wholedt[iteration] = waveform.calculate_value(time, G.dt)
def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G):
"""Updates electric field values for a voltage source.
@@ -177,9 +219,7 @@ class VoltageSource(Source):
newmaterial.ID = f"{material.ID}+{self.ID}"
newmaterial.numID = len(G.materials)
newmaterial.averagable = False
newmaterial.type += (
",\nvoltage-source" if newmaterial.type else "voltage-source"
)
newmaterial.type += ",\nvoltage-source" if newmaterial.type else "voltage-source"
# Add conductivity of voltage source to underlying conductivity
if self.polarisation == "x":
@@ -198,7 +238,7 @@ class HertzianDipole(Source):
def __init__(self):
super().__init__()
self.dl = None
self.dl = 0.0
def calculate_waveform_values(self, G):
"""Calculates all waveform values for source for duration of simulation.
@@ -310,9 +350,7 @@ class MagneticDipole(Source):
# Set the time of the waveform evaluation to account for any
# delay in the start
time -= self.start
self.waveformvalues_wholedt[iteration] = waveform.calculate_value(
time, G.dt
)
self.waveformvalues_wholedt[iteration] = waveform.calculate_value(time, G.dt)
def update_magnetic(self, iteration, updatecoeffsH, ID, Hx, Hy, Hz, G):
"""Updates magnetic field values for a magnetic dipole.
@@ -373,9 +411,7 @@ def htod_src_arrays(sources, G, queue=None):
"""
srcinfo1 = np.zeros((len(sources), 4), dtype=np.int32)
srcinfo2 = np.zeros(
(len(sources)), dtype=config.sim_config.dtypes["float_or_double"]
)
srcinfo2 = np.zeros((len(sources)), dtype=config.sim_config.dtypes["float_or_double"])
srcwaves = np.zeros(
(len(sources), G.iterations), dtype=config.sim_config.dtypes["float_or_double"]
)
@@ -428,14 +464,16 @@ class TransmissionLine(Source):
which is attached virtually to a grid cell.
"""
def __init__(self, G):
def __init__(self, iterations: int, dt: float):
"""
Args:
G: FDTDGrid class describing a grid in a model.
iterations: number of iterations
dt: time step of the grid
"""
super().__init__()
self.resistance = None
self.iterations = iterations
# Coefficients for ABC termination of end of the transmission line
self.abcv0 = 0
@@ -443,11 +481,11 @@ class TransmissionLine(Source):
# Spatial step of transmission line (N.B if the magic time step is
# used it results in instabilities for certain impedances)
self.dl = np.sqrt(3) * config.c * G.dt
self.dl = np.sqrt(3) * config.c * dt
# Number of cells in the transmission line (initially a long line to
# calculate incident voltage and current); consider putting ABCs/PML at end
self.nl = round_value(0.667 * G.iterations)
self.nl = round_value(0.667 * self.iterations)
# Cell position of the one-way injector excitation in the transmission line
self.srcpos = 5
@@ -455,24 +493,12 @@ class TransmissionLine(Source):
# Cell position of where line connects to antenna/main grid
self.antpos = 10
self.voltage = np.zeros(
self.nl, dtype=config.sim_config.dtypes["float_or_double"]
)
self.current = np.zeros(
self.nl, dtype=config.sim_config.dtypes["float_or_double"]
)
self.Vinc = np.zeros(
G.iterations, dtype=config.sim_config.dtypes["float_or_double"]
)
self.Iinc = np.zeros(
G.iterations, dtype=config.sim_config.dtypes["float_or_double"]
)
self.Vtotal = np.zeros(
G.iterations, dtype=config.sim_config.dtypes["float_or_double"]
)
self.Itotal = np.zeros(
G.iterations, dtype=config.sim_config.dtypes["float_or_double"]
)
self.voltage = np.zeros(self.nl, dtype=config.sim_config.dtypes["float_or_double"])
self.current = np.zeros(self.nl, dtype=config.sim_config.dtypes["float_or_double"])
self.Vinc = np.zeros(self.iterations, dtype=config.sim_config.dtypes["float_or_double"])
self.Iinc = np.zeros(self.iterations, dtype=config.sim_config.dtypes["float_or_double"])
self.Vtotal = np.zeros(self.iterations, dtype=config.sim_config.dtypes["float_or_double"])
self.Itotal = np.zeros(self.iterations, dtype=config.sim_config.dtypes["float_or_double"])
def calculate_waveform_values(self, G):
"""Calculates all waveform values for source for duration of simulation.
@@ -508,9 +534,7 @@ class TransmissionLine(Source):
# Set the time of the waveform evaluation to account for any
# delay in the start
time -= self.start
self.waveformvalues_wholedt[iteration] = waveform.calculate_value(
time, G.dt
)
self.waveformvalues_wholedt[iteration] = waveform.calculate_value(time, G.dt)
self.waveformvalues_halfdt[iteration] = waveform.calculate_value(
time + 0.5 * G.dt, G.dt
)
@@ -524,7 +548,7 @@ class TransmissionLine(Source):
G: FDTDGrid class describing a grid in a model.
"""
for iteration in range(G.iterations):
for iteration in range(self.iterations):
self.Iinc[iteration] = self.current[self.antpos]
self.Vinc[iteration] = self.voltage[self.antpos]
self.update_current(iteration, G)
@@ -562,9 +586,9 @@ class TransmissionLine(Source):
)
# Update the voltage at the position of the one-way injector excitation
self.voltage[self.srcpos] += (
config.c * G.dt / self.dl
) * self.waveformvalues_halfdt[iteration]
self.voltage[self.srcpos] += (config.c * G.dt / self.dl) * self.waveformvalues_halfdt[
iteration
]
# Update ABC before updating current
self.update_abc(G)
@@ -621,6 +645,7 @@ class TransmissionLine(Source):
elif self.polarisation == "z":
Ez[i, j, k] = -self.voltage[self.antpos] / G.dz
# TODO: Add type information (if can avoid circular dependency)
def update_magnetic(self, iteration, updatecoeffsH, ID, Hx, Hy, Hz, G):
"""Updates current value in transmission line from magnetic field values
in the main grid.
@@ -641,13 +666,13 @@ class TransmissionLine(Source):
k = self.zcoord
if self.polarisation == "x":
self.current[self.antpos] = Ix(i, j, k, G.Hx, G.Hy, G.Hz, G)
self.current[self.antpos] = G.calculate_Ix(i, j, k)
elif self.polarisation == "y":
self.current[self.antpos] = Iy(i, j, k, G.Hx, G.Hy, G.Hz, G)
self.current[self.antpos] = G.calculate_Iy(i, j, k)
elif self.polarisation == "z":
self.current[self.antpos] = Iz(i, j, k, G.Hx, G.Hy, G.Hz, G)
self.current[self.antpos] = G.calculate_Iz(i, j, k)
self.update_current(iteration, G)
@@ -681,9 +706,7 @@ class DiscretePlaneWave(Source):
self.m = np.zeros(3 + 1, dtype=np.int32) # +1 to store the max(m_x, m_y, m_z)
self.directions = np.zeros(3, dtype=np.int32)
self.length = 0
self.projections = np.zeros(
3, dtype=config.sim_config.dtypes["float_or_double"]
)
self.projections = np.zeros(3, dtype=config.sim_config.dtypes["float_or_double"])
self.corners = None
self.materialID = 1
self.ds = 0
@@ -793,11 +816,7 @@ class DiscretePlaneWave(Source):
G.dt * iteration
- (
r
+ (
self.m[(dimension + 1) % 3]
+ self.m[(dimension + 2) % 3]
)
* 0.5
+ (self.m[(dimension + 1) % 3] + self.m[(dimension + 2) % 3]) * 0.5
)
* self.ds
/ config.c
@@ -806,9 +825,9 @@ class DiscretePlaneWave(Source):
# Set the time of the waveform evaluation to account for any
# delay in the start
time -= self.start
self.waveformvalues_wholedt[iteration, dimension, r] = (
waveform.calculate_value(time, G.dt)
)
self.waveformvalues_wholedt[
iteration, dimension, r
] = waveform.calculate_value(time, G.dt)
def update_plane_wave(
self,
@@ -876,18 +895,9 @@ class DiscretePlaneWave(Source):
for dimension in range(3):
for r in range(self.m[3]):
# Assign source values of magnetic field to first few gridpoints
self.H_fields[dimension, r] = self.projections[
dimension
] * getSource(
self.H_fields[dimension, r] = self.projections[dimension] * getSource(
G.iteration * G.dt
- (
r
+ (
self.m[(dimension + 1) % 3]
+ self.m[(dimension + 2) % 3]
)
* 0.5
)
- (r + (self.m[(dimension + 1) % 3] + self.m[(dimension + 2) % 3]) * 0.5)
* self.ds
/ config.c,
waveform.freq,
@@ -995,16 +1005,16 @@ class DiscretePlaneWave(Source):
for j in range(self.corners[1], self.corners[4] + 1):
for k in range(self.corners[2], self.corners[5]):
# correct Hy at firstX-1/2 by subtracting Ez_inc
G.Hy[i - 1, j, k] -= G.updatecoeffsH[
G.ID[4, i, j, k], 1
] * self.getField(i, j, k, self.E_fields, self.m, 2)
G.Hy[i - 1, j, k] -= G.updatecoeffsH[G.ID[4, i, j, k], 1] * self.getField(
i, j, k, self.E_fields, self.m, 2
)
for j in range(self.corners[1], self.corners[4]):
for k in range(self.corners[2], self.corners[5] + 1):
# correct Hz at firstX-1/2 by adding Ey_inc
G.Hz[i - 1, j, k] += G.updatecoeffsH[
G.ID[5, i, j, k], 1
] * self.getField(i, j, k, self.E_fields, self.m, 1)
G.Hz[i - 1, j, k] += G.updatecoeffsH[G.ID[5, i, j, k], 1] * self.getField(
i, j, k, self.E_fields, self.m, 1
)
i = self.corners[3]
for j in range(self.corners[1], self.corners[4] + 1):
@@ -1026,16 +1036,16 @@ class DiscretePlaneWave(Source):
for i in range(self.corners[0], self.corners[3] + 1):
for k in range(self.corners[2], self.corners[5]):
# correct Hx at firstY-1/2 by adding Ez_inc
G.Hx[i, j - 1, k] += G.updatecoeffsH[
G.ID[3, i, j, k], 2
] * self.getField(i, j, k, self.E_fields, self.m, 2)
G.Hx[i, j - 1, k] += G.updatecoeffsH[G.ID[3, i, j, k], 2] * self.getField(
i, j, k, self.E_fields, self.m, 2
)
for i in range(self.corners[0], self.corners[3]):
for k in range(self.corners[2], self.corners[5] + 1):
# correct Hz at firstY-1/2 by subtracting Ex_inc
G.Hz[i, j - 1, k] -= G.updatecoeffsH[
G.ID[5, i, j, k], 2
] * self.getField(i, j, k, self.E_fields, self.m, 0)
G.Hz[i, j - 1, k] -= G.updatecoeffsH[G.ID[5, i, j, k], 2] * self.getField(
i, j, k, self.E_fields, self.m, 0
)
j = self.corners[4]
for i in range(self.corners[0], self.corners[3] + 1):
@@ -1057,16 +1067,16 @@ class DiscretePlaneWave(Source):
for i in range(self.corners[0], self.corners[3]):
for j in range(self.corners[1], self.corners[4] + 1):
# correct Hy at firstZ-1/2 by adding Ex_inc
G.Hy[i, j, k - 1] += G.updatecoeffsH[
G.ID[4, i, j, k], 3
] * self.getField(i, j, k, self.E_fields, self.m, 0)
G.Hy[i, j, k - 1] += G.updatecoeffsH[G.ID[4, i, j, k], 3] * self.getField(
i, j, k, self.E_fields, self.m, 0
)
for i in range(self.corners[0], self.corners[3] + 1):
for j in range(self.corners[1], self.corners[4]):
# correct Hx at firstZ-1/2 by subtracting Ey_inc
G.Hx[i, j, k - 1] -= G.updatecoeffsH[
G.ID[3, i, j, k], 3
] * self.getField(i, j, k, self.E_fields, self.m, 1)
G.Hx[i, j, k - 1] -= G.updatecoeffsH[G.ID[3, i, j, k], 3] * self.getField(
i, j, k, self.E_fields, self.m, 1
)
k = self.corners[5]
for i in range(self.corners[0], self.corners[3]):

查看文件

@@ -17,13 +17,14 @@
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
import logging
from abc import ABC, abstractmethod
from ..grid import FDTDGrid
from gprMax.grid.fdtd_grid import FDTDGrid
logger = logging.getLogger(__name__)
class SubGridBaseGrid(FDTDGrid):
class SubGridBaseGrid(FDTDGrid, ABC):
def __init__(self, *args, **kwargs):
super().__init__()
@@ -35,6 +36,8 @@ class SubGridBaseGrid(FDTDGrid):
# Name of the grid
self.name = kwargs["id"]
self.parent_grid: FDTDGrid
self.iterations = 0
self.filter = kwargs["filter"]
@@ -62,3 +65,23 @@ class SubGridBaseGrid(FDTDGrid):
self.n_boundary_cells_z = d_to_pml + self.pmls["thickness"]["z0"]
self.interpolation = kwargs["interpolation"]
@abstractmethod
def update_magnetic_is(self, precursors):
pass
@abstractmethod
def update_electric_is(self, precursors):
pass
@abstractmethod
def update_electric_os(self, main_grid):
pass
@abstractmethod
def update_magnetic_os(self, main_grid):
pass
@abstractmethod
def print_info(self):
pass

查看文件

@@ -20,11 +20,7 @@ import logging
import gprMax.config as config
from ..cython.fields_updates_hsg import (
update_electric_os,
update_is,
update_magnetic_os,
)
from ..cython.fields_updates_hsg import update_electric_os, update_is, update_magnetic_os
from .grid import SubGridBaseGrid
logger = logging.getLogger(__name__)
@@ -695,8 +691,7 @@ class SubGridHSG(SubGridBaseGrid):
logger.debug(f"[{self.name}] Type: {self.__class__.__name__}")
logger.info(f"[{self.name}] Ratio: 1:{self.ratio}")
logger.info(
f"[{self.name}] Spatial discretisation: {self.dx:g} x "
+ f"{self.dy:g} x {self.dz:g}m"
f"[{self.name}] Spatial discretisation: {self.dx:g} x " + f"{self.dy:g} x {self.dz:g}m"
)
logger.info(
f"[{self.name}] Extent (working region): {xs}m, {ys}m, {zs}m to {xf}m, {yf}m, {zf}m "

查看文件

@@ -18,31 +18,35 @@
import logging
from ..updates import CPUUpdates
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.model import Model
from gprMax.subgrids.grid import SubGridBaseGrid
from ..updates.cpu_updates import CPUUpdates
from .precursor_nodes import PrecursorNodes, PrecursorNodesFiltered
from .subgrid_hsg import SubGridHSG
logger = logging.getLogger(__name__)
def create_updates(G):
def create_updates(model: Model):
"""Return the solver for the given subgrids."""
updaters = []
for sg in G.subgrids:
for sg in model.subgrids:
sg_type = type(sg)
if sg_type == SubGridHSG and sg.filter:
precursors = PrecursorNodesFiltered(G, sg)
precursors = PrecursorNodesFiltered(model.G, sg)
elif sg_type == SubGridHSG:
precursors = PrecursorNodes(G, sg)
precursors = PrecursorNodes(model.G, sg)
else:
logger.exception(f"{str(sg)} is not a subgrid type")
raise ValueError
sgu = SubgridUpdater(sg, precursors, G)
sgu = SubgridUpdater(sg, precursors, model.G)
updaters.append(sgu)
updates = SubgridUpdates(G, updaters)
updates = SubgridUpdates(model.G, updaters)
return updates
@@ -64,13 +68,13 @@ class SubgridUpdates(CPUUpdates):
sg_updater.hsg_2()
class SubgridUpdater(CPUUpdates):
class SubgridUpdater(CPUUpdates[SubGridBaseGrid]):
"""Handles updating the electric and magnetic fields of an HSG subgrid.
The IS, OS, subgrid region and the electric/magnetic sources are updated
using the precursor regions.
"""
def __init__(self, subgrid, precursors, G):
def __init__(self, subgrid: SubGridBaseGrid, precursors: PrecursorNodes, G: FDTDGrid):
"""
Args:
subgrid: SubGrid3d instance to be updated.
@@ -82,7 +86,17 @@ class SubgridUpdater(CPUUpdates):
super().__init__(subgrid)
self.precursors = precursors
self.G = G
self.source_iteration = 0
self.iteration = 0
def store_outputs(self):
return super().store_outputs(self.iteration)
def update_electric_sources(self):
super().update_electric_sources(self.iteration)
self.iteration += 1
def update_magnetic_sources(self):
return super().update_magnetic_sources(self.iteration)
def hsg_1(self):
"""First half of the subgrid update. Takes the time step up to the main

查看文件

@@ -18,43 +18,62 @@
import logging
from copy import copy
from typing import List, Tuple, Union
import numpy as np
from ..cmds_geometry.cmds_geometry import UserObjectGeometry
from ..cmds_multiuse import UserObjectMulti
from .subgrid_hsg import SubGridHSG as SubGridHSGUser
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.model import Model
from gprMax.subgrids.grid import SubGridBaseGrid
from gprMax.subgrids.subgrid_hsg import SubGridHSG as SubGridHSGUser
from gprMax.user_inputs import MainGridUserInput
from gprMax.user_objects.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 = []
self.children_geometry = []
self.children_grid: List[GridUserObject] = []
self.children_geometry: List[GeometryUserObject] = []
self.children_output: List[OutputUserObject] = []
def add(self, node):
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
def set_discretisation(self, sg, grid):
def set_discretisation(self, sg: SubGridBaseGrid, grid: FDTDGrid):
sg.dx = grid.dx / sg.ratio
sg.dy = grid.dy / sg.ratio
sg.dz = grid.dz / sg.ratio
sg.dl = np.array([sg.dx, sg.dy, sg.dz])
def set_main_grid_indices(self, sg, uip, p1, p2):
def set_main_grid_indices(
self, sg: SubGridBaseGrid, uip: MainGridUserInput, p1: Tuple[int], p2: Tuple[int]
):
"""Sets subgrid indices related to main grid placement."""
# Location of the IS
sg.i0, sg.j0, sg.k0 = p1
@@ -63,39 +82,40 @@ class SubGridBase(UserObjectMulti):
sg.x1, sg.y1, sg.z1 = uip.round_to_grid(p1)
sg.x2, sg.y2, sg.z2 = uip.round_to_grid(p2)
def set_name(self, sg):
def set_name(self, sg: SubGridBaseGrid):
sg.name = self.kwargs["id"]
def set_working_region_cells(self, sg):
def set_working_region_cells(self, sg: SubGridBaseGrid):
"""Number of cells in each dimension for the working region."""
sg.nwx = (sg.i1 - sg.i0) * sg.ratio
sg.nwy = (sg.j1 - sg.j0) * sg.ratio
sg.nwz = (sg.k1 - sg.k0) * sg.ratio
def set_total_cells(self, sg):
def set_total_cells(self, sg: SubGridBaseGrid):
"""Number of cells in each dimension for the whole region."""
sg.nx = 2 * sg.n_boundary_cells_x + sg.nwx
sg.ny = 2 * sg.n_boundary_cells_y + sg.nwy
sg.nz = 2 * sg.n_boundary_cells_z + sg.nwz
def set_iterations(self, sg, main):
def set_iterations(self, sg: SubGridBaseGrid, model: Model):
"""Sets number of iterations that will take place in the subgrid."""
sg.iterations = main.iterations * sg.ratio
sg.iterations = model.iterations * sg.ratio
def setup(self, sg, grid, uip):
def setup(self, sg: SubGridBaseGrid, model: Model):
""" "Common setup to both all subgrid types."""
p1 = self.kwargs["p1"]
p2 = self.kwargs["p2"]
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
uip = self._create_uip(model.G)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
self.set_discretisation(sg, grid)
self.set_discretisation(sg, model.G)
# Set temporal discretisation including any inherited time step
# stability factor from the main grid
sg.calculate_dt()
if grid.dt_mod:
sg.dt = sg.dt * grid.dt_mod
if model.dt_mod:
sg.dt = sg.dt * model.dt_mod
# Set the indices related to the subgrids main grid placement
self.set_main_grid_indices(sg, uip, p1, p2)
@@ -113,31 +133,27 @@ class SubGridBase(UserObjectMulti):
self.set_working_region_cells(sg)
self.set_total_cells(sg)
self.set_iterations(sg, grid)
self.set_iterations(sg, model)
self.set_name(sg)
# Copy a reference for the main grid to the sub grid
sg.parent_grid = grid
sg.timewindow = grid.timewindow
sg.parent_grid = model.G
# Copy a subgrid reference to self so that children.build(grid, uip)
# can access the correct grid.
self.subgrid = sg
# Copy over built in materials
sg.materials = [copy(m) for m in grid.materials if m.type == "builtin"]
sg.materials = [copy(m) for m in model.G.materials if m.type == "builtin"]
# Don't mix and match different subgrid types
for sg_made in grid.subgrids:
for sg_made in model.subgrids:
if type(sg) != type(sg_made):
logger.exception(
f"{self.__str__()} please only use one type of subgrid"
)
logger.exception(f"{self.__str__()} please only use one type of subgrid")
raise ValueError
# Reference the subgrid under the main grid to which it belongs
grid.subgrids.append(sg)
model.subgrids.append(sg)
class SubGridHSG(SubGridBase):
@@ -164,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,
@@ -191,10 +215,8 @@ class SubGridHSG(SubGridBase):
kwargs["filter"] = filter
super().__init__(**kwargs)
self.order = 18
self.hash = "#subgrid_hsg"
def build(self, grid, uip):
def build(self, model: Model) -> SubGridHSGUser:
sg = SubGridHSGUser(**self.kwargs)
self.setup(sg, grid, uip)
self.setup(sg, model)
return sg

查看文件

@@ -40,18 +40,18 @@ EXIT
Tags = IntEnum("Tags", "READY START DONE EXIT")
class MPIExecutor(object):
"""A generic parallel executor based on MPI.
class TaskfarmExecutor(object):
"""A generic parallel executor (taskfarm) based on MPI.
This executor can be used to run generic jobs on multiple
processes based on a master/worker pattern with MPI being used for
communication between the master and the workers.
Examples
--------
A basic example of how to use the `MPIExecutor` to run
A basic example of how to use the `TaskfarmExecutor` to run
`gprMax` models in parallel is given below.
>>> from mpi4py import MPI
>>> from gprMax.mpi import MPIExecutor
>>> from gprMax.model_build_run import run_model
>>> from gprMax.taskfarm import TaskfarmExecutor
>>> from gprMax.model import run_model
>>> # choose an MPI.Intracomm for communication (MPI.COMM_WORLD by default)
>>> comm = MPI.COMM_WORLD
>>> # choose a target function
@@ -68,7 +68,7 @@ class MPIExecutor(object):
>>> 'modelend': n_traces,
>>> 'numbermodelruns': n_traces
>>> })
>>> gpr = MPIExecutor(func, comm=comm)
>>> gpr = TaskfarmExecutor(func, comm=comm)
>>> # send the workers to their work loop
>>> gpr.start()
>>> if gpr.is_master():
@@ -78,10 +78,10 @@ class MPIExecutor(object):
>>> # and join the main loop again
>>> gpr.join()
A slightly more concise way is to use the context manager
interface of `MPIExecutor` that automatically takes care
interface of `TaskfarmExecutor` that automatically takes care
of calling `start()` and `join()` at the beginning and end
of the execution, respectively.
>>> with MPIExecutor(func, comm=comm) as executor:
>>> with TaskfarmExecutor(func, comm=comm) as executor:
>>> # executor will be None on all ranks except for the master
>>> if executor is not None:
>>> results = executor.submit(jobs)
@@ -89,7 +89,7 @@ class MPIExecutor(object):
Limitations
-----------
Because some popular MPI implementations (especially on HPC machines) do not
support concurrent MPI calls from multiple threads yet, the `MPIExecutor` does
support concurrent MPI calls from multiple threads yet, the `TaskfarmExecutor` does
not use a separate thread in the master to do the communication between the
master and the workers. Hence, the lowest thread level of MPI_THREAD_SINGLE
(no multi-threading) is enough.
@@ -143,7 +143,7 @@ class MPIExecutor(object):
self.rank = self.comm.rank
self.size = self.comm.size
if self.size < 2:
raise RuntimeError("MPIExecutor must run with at least 2 processes")
raise RuntimeError("TaskfarmExecutor must run with at least 2 processes")
self._up = False
@@ -165,9 +165,7 @@ class MPIExecutor(object):
self.busy = [False] * len(self.workers)
if self.is_master():
logger.basic(
f"\n({self.comm.name}) - Master: {self.master}, Workers: {self.workers}"
)
logger.basic(f"\n({self.comm.name}) - Master: {self.master}, Workers: {self.workers}")
def __enter__(self):
"""Context manager enter. Only the master returns an executor, all other
@@ -216,7 +214,7 @@ class MPIExecutor(object):
raise RuntimeError("Start has already been called")
self._up = True
logger.debug(f"({self.comm.name}) - Starting up MPIExecutor master/workers...")
logger.debug(f"({self.comm.name}) - Starting up TaskfarmExecutor master/workers...")
if self.is_worker():
self.__wait()
@@ -224,9 +222,7 @@ class MPIExecutor(object):
"""Joins the workers."""
if not self.is_master():
return
logger.debug(
f"({self.comm.name}) - Terminating. Sending sentinel to all workers."
)
logger.debug(f"({self.comm.name}) - Terminating. Sending sentinel to all workers.")
# Send sentinel to all workers
for worker in self.workers:
self.comm.send(None, dest=worker, tag=Tags.EXIT)
@@ -286,13 +282,9 @@ class MPIExecutor(object):
logger.debug(
f"({self.comm.name}) - Sending job {job_idx} to worker {worker:d}."
)
self.comm.send(
(job_idx, my_jobs.pop(0)), dest=worker, tag=Tags.START
)
self.comm.send((job_idx, my_jobs.pop(0)), dest=worker, tag=Tags.START)
elif self.comm.Iprobe(source=worker, tag=Tags.EXIT):
logger.debug(
f"({self.comm.name}) - Worker on rank {worker:d} has terminated."
)
logger.debug(f"({self.comm.name}) - Worker on rank {worker:d} has terminated.")
self.comm.recv(source=worker, tag=Tags.EXIT)
self.busy[i] = False
@@ -316,22 +308,16 @@ class MPIExecutor(object):
while True:
self.comm.send(None, dest=self.master, tag=Tags.READY)
logger.debug(
f"({self.comm.name}) - Worker on rank {self.rank} waiting for job."
)
logger.debug(f"({self.comm.name}) - Worker on rank {self.rank} waiting for job.")
data = self.comm.recv(source=self.master, tag=MPI.ANY_TAG, status=status)
tag = status.tag
if tag == Tags.START:
job_idx, work = data
logger.debug(
f"({self.comm.name}) - Received job {job_idx} (work={work})."
)
logger.debug(f"({self.comm.name}) - Received job {job_idx} (work={work}).")
result = self.__guarded_work(work)
logger.debug(
f"({self.comm.name}) - Finished job. Sending results to master."
)
logger.debug(f"({self.comm.name}) - Finished job. Sending results to master.")
self.comm.send((job_idx, result), dest=self.master, tag=Tags.DONE)
elif tag == Tags.EXIT:
logger.debug(f"({self.comm.name}) - Received sentinel from master.")

文件差异内容过多而无法显示 加载差异

查看文件

@@ -0,0 +1,230 @@
# 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/>.
from importlib import import_module
from typing_extensions import TypeVar
from gprMax import config
from gprMax.cython.fields_updates_normal import update_electric as update_electric_cpu
from gprMax.cython.fields_updates_normal import update_magnetic as update_magnetic_cpu
from gprMax.fields_outputs import store_outputs as store_outputs_cpu
from gprMax.updates.updates import GridType, Updates
from gprMax.utilities.utilities import timer
class CPUUpdates(Updates[GridType]):
"""Defines update functions for CPU-based solver."""
def __init__(self, G: GridType):
"""
Args:
G: FDTDGrid class describing a grid in a model.
"""
super().__init__(G)
def store_outputs(self, iteration):
"""Stores field component values for every receiver and transmission line."""
store_outputs_cpu(self.grid, iteration)
def store_snapshots(self, iteration):
"""Stores any snapshots.
Args:
iteration: int for iteration number.
"""
for snap in self.grid.snapshots:
if snap.time == iteration + 1:
snap.store()
def update_magnetic(self):
"""Updates magnetic field components."""
update_magnetic_cpu(
self.grid.nx,
self.grid.ny,
self.grid.nz,
config.get_model_config().ompthreads,
self.grid.updatecoeffsH,
self.grid.ID,
self.grid.Ex,
self.grid.Ey,
self.grid.Ez,
self.grid.Hx,
self.grid.Hy,
self.grid.Hz,
)
def update_magnetic_pml(self):
"""Updates magnetic field components with the PML correction."""
for pml in self.grid.pmls["slabs"]:
pml.update_magnetic()
def update_magnetic_sources(self, iteration):
"""Updates magnetic field components from sources."""
for source in self.grid.transmissionlines + self.grid.magneticdipoles:
source.update_magnetic(
iteration,
self.grid.updatecoeffsH,
self.grid.ID,
self.grid.Hx,
self.grid.Hy,
self.grid.Hz,
self.grid,
)
# Update the magnetic and electric field components for the discrete plane wave
for source in self.grid.discreteplanewaves:
source.update_plane_wave(
config.get_model_config().ompthreads,
self.grid.updatecoeffsE,
self.grid.updatecoeffsH,
self.grid.Ex,
self.grid.Ey,
self.grid.Ez,
self.grid.Hx,
self.grid.Hy,
self.grid.Hz,
iteration,
self.grid,
cythonize=True,
precompute=False,
)
def update_electric_a(self):
"""Updates electric field components."""
# All materials are non-dispersive so do standard update.
if config.get_model_config().materials["maxpoles"] == 0:
update_electric_cpu(
self.grid.nx,
self.grid.ny,
self.grid.nz,
config.get_model_config().ompthreads,
self.grid.updatecoeffsE,
self.grid.ID,
self.grid.Ex,
self.grid.Ey,
self.grid.Ez,
self.grid.Hx,
self.grid.Hy,
self.grid.Hz,
)
# If there are any dispersive materials do 1st part of dispersive update
# (it is split into two parts as it requires present and updated electric field values).
else:
self.dispersive_update_a(
self.grid.nx,
self.grid.ny,
self.grid.nz,
config.get_model_config().ompthreads,
config.get_model_config().materials["maxpoles"],
self.grid.updatecoeffsE,
self.grid.updatecoeffsdispersive,
self.grid.ID,
self.grid.Tx,
self.grid.Ty,
self.grid.Tz,
self.grid.Ex,
self.grid.Ey,
self.grid.Ez,
self.grid.Hx,
self.grid.Hy,
self.grid.Hz,
)
def update_electric_pml(self):
"""Updates electric field components with the PML correction."""
for pml in self.grid.pmls["slabs"]:
pml.update_electric()
def update_electric_sources(self, iteration):
"""Updates electric field components from sources -
update any Hertzian dipole sources last.
"""
for source in (
self.grid.voltagesources + self.grid.transmissionlines + self.grid.hertziandipoles
):
source.update_electric(
iteration,
self.grid.updatecoeffsE,
self.grid.ID,
self.grid.Ex,
self.grid.Ey,
self.grid.Ez,
self.grid,
)
def update_electric_b(self):
"""If there are any dispersive materials do 2nd part of dispersive
update - it is split into two parts as it requires present and
updated electric field values. Therefore it can only be completely
updated after the electric field has been updated by the PML and
source updates.
"""
if config.get_model_config().materials["maxpoles"] > 0:
self.dispersive_update_b(
self.grid.nx,
self.grid.ny,
self.grid.nz,
config.get_model_config().ompthreads,
config.get_model_config().materials["maxpoles"],
self.grid.updatecoeffsdispersive,
self.grid.ID,
self.grid.Tx,
self.grid.Ty,
self.grid.Tz,
self.grid.Ex,
self.grid.Ey,
self.grid.Ez,
)
def set_dispersive_updates(self):
"""Sets dispersive update functions."""
poles = "multi" if config.get_model_config().materials["maxpoles"] > 1 else "1"
precision = "float" if config.sim_config.general["precision"] == "single" else "double"
dispersion = (
"complex"
if config.get_model_config().materials["dispersivedtype"]
== config.sim_config.dtypes["complex"]
else "real"
)
update_f = "update_electric_dispersive_{}pole_{}_{}_{}"
disp_a = update_f.format(poles, "A", precision, dispersion)
disp_b = update_f.format(poles, "B", precision, dispersion)
disp_a_f = getattr(import_module("gprMax.cython.fields_updates_dispersive"), disp_a)
disp_b_f = getattr(import_module("gprMax.cython.fields_updates_dispersive"), disp_b)
self.dispersive_update_a = disp_a_f
self.dispersive_update_b = disp_b_f
def time_start(self):
"""Starts timer used to calculate solving time for model."""
self.timestart = timer()
def calculate_solve_time(self):
"""Calculates solving time for model."""
return timer() - self.timestart
def finalise(self):
pass
def cleanup(self):
pass

查看文件

@@ -0,0 +1,628 @@
# 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 importlib import import_module
import humanize
import numpy as np
from jinja2 import Environment, PackageLoader
from gprMax import config
from gprMax.cuda_opencl import (
knl_fields_updates,
knl_snapshots,
knl_source_updates,
knl_store_outputs,
)
from gprMax.grid.cuda_grid import CUDAGrid
from gprMax.receivers import dtoh_rx_array, htod_rx_arrays
from gprMax.snapshots import Snapshot, dtoh_snapshot_array, htod_snapshot_array
from gprMax.sources import htod_src_arrays
from gprMax.updates.updates import Updates
from gprMax.utilities.utilities import round32
logger = logging.getLogger(__name__)
class CUDAUpdates(Updates[CUDAGrid]):
"""Defines update functions for GPU-based (CUDA) solver."""
def __init__(self, G: CUDAGrid):
"""
Args:
G: CUDAGrid class describing a grid in a model.
"""
super().__init__(G)
# Import PyCUDA modules
self.drv = import_module("pycuda.driver")
self.source_module = getattr(import_module("pycuda.compiler"), "SourceModule")
self.drv.init()
# Create device handle and context on specific GPU device (and make it current context)
self.dev = config.get_model_config().device["dev"]
self.ctx = self.dev.make_context()
# Set common substitutions for use in kernels
# Substitutions in function arguments
self.subs_name_args = {
"REAL": config.sim_config.dtypes["C_float_or_double"],
"COMPLEX": config.get_model_config().materials["dispersiveCdtype"],
}
# Substitutions in function bodies
self.subs_func = {
"REAL": config.sim_config.dtypes["C_float_or_double"],
"CUDA_IDX": "int i = blockIdx.x * blockDim.x + threadIdx.x;",
"NX_FIELDS": self.grid.nx + 1,
"NY_FIELDS": self.grid.ny + 1,
"NZ_FIELDS": self.grid.nz + 1,
"NX_ID": self.grid.ID.shape[1],
"NY_ID": self.grid.ID.shape[2],
"NZ_ID": self.grid.ID.shape[3],
}
# Enviroment for templating kernels
self.env = Environment(loader=PackageLoader("gprMax", "cuda_opencl"))
# Initialise arrays on GPU, prepare kernels, and get kernel functions
self._set_macros()
self._set_field_knls()
if self.grid.pmls["slabs"]:
self._set_pml_knls()
if self.grid.rxs:
self._set_rx_knl()
if self.grid.voltagesources + self.grid.hertziandipoles + self.grid.magneticdipoles:
self._set_src_knls()
if self.grid.snapshots:
self._set_snapshot_knl()
def _build_knl(self, knl_func, subs_name_args, subs_func):
"""Builds a CUDA kernel from templates: 1) function name and args;
and 2) function (kernel) body.
Args:
knl_func: dict containing templates for function name and args,
and function body.
subs_name_args: dict containing substitutions to be used with
function name and args.
subs_func: dict containing substitutions to be used with function
(kernel) body.
Returns:
knl: string with complete kernel
"""
name_plus_args = knl_func["args_cuda"].substitute(subs_name_args)
func_body = knl_func["func"].substitute(subs_func)
knl = self.knl_common + "\n" + name_plus_args + "{" + func_body + "}"
return knl
def _set_macros(self):
"""Common macros to be used in kernels."""
# Set specific values for any dispersive materials
if config.get_model_config().materials["maxpoles"] > 0:
NY_MATDISPCOEFFS = self.grid.updatecoeffsdispersive.shape[1]
NX_T = self.grid.Tx.shape[1]
NY_T = self.grid.Tx.shape[2]
NZ_T = self.grid.Tx.shape[3]
else: # Set to one any substitutions for dispersive materials.
NY_MATDISPCOEFFS = 1
NX_T = 1
NY_T = 1
NZ_T = 1
self.knl_common = self.env.get_template("knl_common_cuda.tmpl").render(
REAL=config.sim_config.dtypes["C_float_or_double"],
N_updatecoeffsE=self.grid.updatecoeffsE.size,
N_updatecoeffsH=self.grid.updatecoeffsH.size,
NY_MATCOEFFS=self.grid.updatecoeffsE.shape[1],
NY_MATDISPCOEFFS=NY_MATDISPCOEFFS,
NX_FIELDS=self.grid.nx + 1,
NY_FIELDS=self.grid.ny + 1,
NZ_FIELDS=self.grid.nz + 1,
NX_ID=self.grid.ID.shape[1],
NY_ID=self.grid.ID.shape[2],
NZ_ID=self.grid.ID.shape[3],
NX_T=NX_T,
NY_T=NY_T,
NZ_T=NZ_T,
NY_RXCOORDS=3,
NX_RXS=6,
NY_RXS=self.grid.iterations,
NZ_RXS=len(self.grid.rxs),
NY_SRCINFO=4,
NY_SRCWAVES=self.grid.iterations,
NX_SNAPS=Snapshot.nx_max,
NY_SNAPS=Snapshot.ny_max,
NZ_SNAPS=Snapshot.nz_max,
)
def _set_field_knls(self):
"""Electric and magnetic field updates - prepares kernels, and
gets kernel functions.
"""
bld = self._build_knl(
knl_fields_updates.update_electric, self.subs_name_args, self.subs_func
)
knlE = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.update_electric_dev = knlE.get_function("update_electric")
bld = self._build_knl(
knl_fields_updates.update_magnetic, self.subs_name_args, self.subs_func
)
knlH = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.update_magnetic_dev = knlH.get_function("update_magnetic")
self._copy_mat_coeffs(knlE, knlH)
# If there are any dispersive materials (updates are split into two
# parts as they require present and updated electric field values).
if config.get_model_config().materials["maxpoles"] > 0:
self.subs_func.update(
{
"REAL": config.sim_config.dtypes["C_float_or_double"],
"REALFUNC": config.get_model_config().materials["crealfunc"],
"NX_T": self.grid.Tx.shape[1],
"NY_T": self.grid.Tx.shape[2],
"NZ_T": self.grid.Tx.shape[3],
}
)
bld = self._build_knl(
knl_fields_updates.update_electric_dispersive_A, self.subs_name_args, self.subs_func
)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.dispersive_update_a = knl.get_function("update_electric_dispersive_A")
self._copy_mat_coeffs(knl, knl)
bld = self._build_knl(
knl_fields_updates.update_electric_dispersive_B, self.subs_name_args, self.subs_func
)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.dispersive_update_b = knl.get_function("update_electric_dispersive_B")
self._copy_mat_coeffs(knl, knl)
# Set blocks per grid and initialise field arrays on GPU
self.grid.set_blocks_per_grid()
self.grid.htod_geometry_arrays()
self.grid.htod_field_arrays()
if config.get_model_config().materials["maxpoles"] > 0:
self.grid.htod_dispersive_arrays()
def _set_pml_knls(self):
"""PMLS - prepares kernels and gets kernel functions."""
knl_pml_updates_electric = import_module(
"gprMax.cuda_opencl.knl_pml_updates_electric_" + self.grid.pmls["formulation"]
)
knl_pml_updates_magnetic = import_module(
"gprMax.cuda_opencl.knl_pml_updates_magnetic_" + self.grid.pmls["formulation"]
)
# Initialise arrays on GPU, set block per grid, and get kernel functions
for pml in self.grid.pmls["slabs"]:
pml.htod_field_arrays()
pml.set_blocks_per_grid()
knl_name = f"order{len(pml.CFS)}_{pml.direction}"
self.subs_name_args["FUNC"] = knl_name
knl_electric = getattr(knl_pml_updates_electric, knl_name)
bld = self._build_knl(knl_electric, self.subs_name_args, self.subs_func)
knlE = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
pml.update_electric_dev = knlE.get_function(knl_name)
knl_magnetic = getattr(knl_pml_updates_magnetic, knl_name)
bld = self._build_knl(knl_magnetic, self.subs_name_args, self.subs_func)
knlH = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
pml.update_magnetic_dev = knlH.get_function(knl_name)
# Copy material coefficient arrays to constant memory of GPU - must
# be done for each kernel
self._copy_mat_coeffs(knlE, knlH)
def _set_rx_knl(self):
"""Receivers - initialises arrays on GPU, prepares kernel and gets kernel
function.
"""
self.rxcoords_dev, self.rxs_dev = htod_rx_arrays(self.grid)
self.subs_func.update(
{
"REAL": config.sim_config.dtypes["C_float_or_double"],
"NY_RXCOORDS": 3,
"NX_RXS": 6,
"NY_RXS": self.grid.iterations,
"NZ_RXS": len(self.grid.rxs),
}
)
bld = self._build_knl(knl_store_outputs.store_outputs, self.subs_name_args, self.subs_func)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.store_outputs_dev = knl.get_function("store_outputs")
def _set_src_knls(self):
"""Sources - initialises arrays on GPU, prepares kernel and gets kernel
function.
"""
self.subs_func.update({"NY_SRCINFO": 4, "NY_SRCWAVES": self.grid.iteration})
if self.grid.hertziandipoles:
(
self.srcinfo1_hertzian_dev,
self.srcinfo2_hertzian_dev,
self.srcwaves_hertzian_dev,
) = htod_src_arrays(self.grid.hertziandipoles, self.grid)
bld = self._build_knl(
knl_source_updates.update_hertzian_dipole, self.subs_name_args, self.subs_func
)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.update_hertzian_dipole_dev = knl.get_function("update_hertzian_dipole")
if self.grid.magneticdipoles:
(
self.srcinfo1_magnetic_dev,
self.srcinfo2_magnetic_dev,
self.srcwaves_magnetic_dev,
) = htod_src_arrays(self.grid.magneticdipoles, self.grid)
bld = self._build_knl(
knl_source_updates.update_magnetic_dipole, self.subs_name_args, self.subs_func
)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.update_magnetic_dipole_dev = knl.get_function("update_magnetic_dipole")
if self.grid.voltagesources:
(
self.srcinfo1_voltage_dev,
self.srcinfo2_voltage_dev,
self.srcwaves_voltage_dev,
) = htod_src_arrays(self.grid.voltagesources, self.grid)
bld = self._build_knl(
knl_source_updates.update_voltage_source, self.subs_name_args, self.subs_func
)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.update_voltage_source_dev = knl.get_function("update_voltage_source")
self._copy_mat_coeffs(knl, knl)
def _set_snapshot_knl(self):
"""Snapshots - initialises arrays on GPU, prepares kernel and gets kernel
function.
"""
(
self.snapEx_dev,
self.snapEy_dev,
self.snapEz_dev,
self.snapHx_dev,
self.snapHy_dev,
self.snapHz_dev,
) = htod_snapshot_array(self.grid)
self.subs_func.update(
{
"REAL": config.sim_config.dtypes["C_float_or_double"],
"NX_SNAPS": Snapshot.nx_max,
"NY_SNAPS": Snapshot.ny_max,
"NZ_SNAPS": Snapshot.nz_max,
}
)
bld = self._build_knl(knl_snapshots.store_snapshot, self.subs_name_args, self.subs_func)
knl = self.source_module(bld, options=config.sim_config.devices["nvcc_opts"])
self.store_snapshot_dev = knl.get_function("store_snapshot")
def _copy_mat_coeffs(self, knlE, knlH):
"""Copies material coefficient arrays to constant memory of GPU
(must be <64KB).
Args:
knlE: kernel for electric field.
knlH: kernel for magnetic field.
"""
# Check if coefficient arrays will fit on constant memory of GPU
if (
self.grid.updatecoeffsE.nbytes + self.grid.updatecoeffsH.nbytes
> config.get_model_config().device["dev"].total_constant_memory
):
device = config.get_model_config().device["dev"]
logger.exception(
f"Too many materials in the model to fit onto "
+ f"constant memory of size {humanize.naturalsize(device.total_constant_memory)} "
+ f"on {device.deviceID}: {' '.join(device.name().split())}"
)
raise ValueError
updatecoeffsE = knlE.get_global("updatecoeffsE")[0]
updatecoeffsH = knlH.get_global("updatecoeffsH")[0]
self.drv.memcpy_htod(updatecoeffsE, self.grid.updatecoeffsE)
self.drv.memcpy_htod(updatecoeffsH, self.grid.updatecoeffsH)
def store_outputs(self, iteration):
"""Stores field component values for every receiver."""
if self.grid.rxs:
self.store_outputs_dev(
np.int32(len(self.grid.rxs)),
np.int32(iteration),
self.rxcoords_dev.gpudata,
self.rxs_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
self.grid.Hx_dev.gpudata,
self.grid.Hy_dev.gpudata,
self.grid.Hz_dev.gpudata,
block=(1, 1, 1),
grid=(round32(len(self.grid.rxs)), 1, 1),
)
def store_snapshots(self, iteration):
"""Stores any snapshots.
Args:
iteration: int for iteration number.
"""
for i, snap in enumerate(self.grid.snapshots):
if snap.time == iteration + 1:
snapno = 0 if config.get_model_config().device["snapsgpu2cpu"] else i
self.store_snapshot_dev(
np.int32(snapno),
np.int32(snap.xs),
np.int32(snap.xf),
np.int32(snap.ys),
np.int32(snap.yf),
np.int32(snap.zs),
np.int32(snap.zf),
np.int32(snap.dx),
np.int32(snap.dy),
np.int32(snap.dz),
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
self.grid.Hx_dev.gpudata,
self.grid.Hy_dev.gpudata,
self.grid.Hz_dev.gpudata,
self.snapEx_dev.gpudata,
self.snapEy_dev.gpudata,
self.snapEz_dev.gpudata,
self.snapHx_dev.gpudata,
self.snapHy_dev.gpudata,
self.snapHz_dev.gpudata,
block=Snapshot.tpb,
grid=Snapshot.bpg,
)
if config.get_model_config().device["snapsgpu2cpu"]:
dtoh_snapshot_array(
self.snapEx_dev.get(),
self.snapEy_dev.get(),
self.snapEz_dev.get(),
self.snapHx_dev.get(),
self.snapHy_dev.get(),
self.snapHz_dev.get(),
0,
snap,
)
def update_magnetic(self):
"""Updates magnetic field components."""
self.update_magnetic_dev(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
self.grid.ID_dev.gpudata,
self.grid.Hx_dev.gpudata,
self.grid.Hy_dev.gpudata,
self.grid.Hz_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
block=self.grid.tpb,
grid=self.grid.bpg,
)
def update_magnetic_pml(self):
"""Updates magnetic field components with the PML correction."""
for pml in self.grid.pmls["slabs"]:
pml.update_magnetic()
def update_magnetic_sources(self, iteration):
"""Updates magnetic field components from sources."""
if self.grid.magneticdipoles:
self.update_magnetic_dipole_dev(
np.int32(len(self.grid.magneticdipoles)),
np.int32(iteration),
config.sim_config.dtypes["float_or_double"](self.grid.dx),
config.sim_config.dtypes["float_or_double"](self.grid.dy),
config.sim_config.dtypes["float_or_double"](self.grid.dz),
self.srcinfo1_magnetic_dev.gpudata,
self.srcinfo2_magnetic_dev.gpudata,
self.srcwaves_magnetic_dev.gpudata,
self.grid.ID_dev.gpudata,
self.grid.Hx_dev.gpudata,
self.grid.Hy_dev.gpudata,
self.grid.Hz_dev.gpudata,
block=(1, 1, 1),
grid=(round32(len(self.grid.magneticdipoles)), 1, 1),
)
def update_electric_a(self):
"""Updates electric field components."""
# All materials are non-dispersive so do standard update.
if config.get_model_config().materials["maxpoles"] == 0:
self.update_electric_dev(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
self.grid.ID_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
self.grid.Hx_dev.gpudata,
self.grid.Hy_dev.gpudata,
self.grid.Hz_dev.gpudata,
block=self.grid.tpb,
grid=self.grid.bpg,
)
# If there are any dispersive materials do 1st part of dispersive update
# (it is split into two parts as it requires present and updated electric field values).
else:
self.dispersive_update_a(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
np.int32(config.get_model_config().materials["maxpoles"]),
self.grid.updatecoeffsdispersive_dev.gpudata,
self.grid.Tx_dev.gpudata,
self.grid.Ty_dev.gpudata,
self.grid.Tz_dev.gpudata,
self.grid.ID_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
self.grid.Hx_dev.gpudata,
self.grid.Hy_dev.gpudata,
self.grid.Hz_dev.gpudata,
block=self.grid.tpb,
grid=self.grid.bpg,
)
def update_electric_pml(self):
"""Updates electric field components with the PML correction."""
for pml in self.grid.pmls["slabs"]:
pml.update_electric()
def update_electric_sources(self, iteration):
"""Updates electric field components from sources -
update any Hertzian dipole sources last.
"""
if self.grid.voltagesources:
self.update_voltage_source_dev(
np.int32(len(self.grid.voltagesources)),
np.int32(iteration),
config.sim_config.dtypes["float_or_double"](self.grid.dx),
config.sim_config.dtypes["float_or_double"](self.grid.dy),
config.sim_config.dtypes["float_or_double"](self.grid.dz),
self.srcinfo1_voltage_dev.gpudata,
self.srcinfo2_voltage_dev.gpudata,
self.srcwaves_voltage_dev.gpudata,
self.grid.ID_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
block=(1, 1, 1),
grid=(round32(len(self.grid.voltagesources)), 1, 1),
)
if self.grid.hertziandipoles:
self.update_hertzian_dipole_dev(
np.int32(len(self.grid.hertziandipoles)),
np.int32(self.grid.iteration),
config.sim_config.dtypes["float_or_double"](self.grid.dx),
config.sim_config.dtypes["float_or_double"](self.grid.dy),
config.sim_config.dtypes["float_or_double"](self.grid.dz),
self.srcinfo1_hertzian_dev.gpudata,
self.srcinfo2_hertzian_dev.gpudata,
self.srcwaves_hertzian_dev.gpudata,
self.grid.ID_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
block=(1, 1, 1),
grid=(round32(len(self.grid.hertziandipoles)), 1, 1),
)
self.grid.iteration += 1
def update_electric_b(self):
"""If there are any dispersive materials do 2nd part of dispersive
update - it is split into two parts as it requires present and
updated electric field values. Therefore it can only be completely
updated after the electric field has been updated by the PML and
source updates.
"""
if config.get_model_config().materials["maxpoles"] > 0:
self.dispersive_update_b(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
np.int32(config.get_model_config().materials["maxpoles"]),
self.grid.updatecoeffsdispersive_dev.gpudata,
self.grid.Tx_dev.gpudata,
self.grid.Ty_dev.gpudata,
self.grid.Tz_dev.gpudata,
self.grid.ID_dev.gpudata,
self.grid.Ex_dev.gpudata,
self.grid.Ey_dev.gpudata,
self.grid.Ez_dev.gpudata,
block=self.grid.tpb,
grid=self.grid.bpg,
)
def time_start(self):
"""Starts event timers used to calculate solving time for model."""
self.iterstart = self.drv.Event()
self.iterend = self.drv.Event()
self.iterstart.record()
self.iterstart.synchronize()
def calculate_memory_used(self, iteration):
"""Calculates memory used on last iteration.
Args:
iteration: int for iteration number.
Returns:
Memory (RAM) used on GPU.
"""
if iteration == self.grid.iterations - 1:
# Total minus free memory in current context
return self.drv.mem_get_info()[1] - self.drv.mem_get_info()[0]
def calculate_solve_time(self):
"""Calculates solving time for model."""
self.iterend.record()
self.iterend.synchronize()
return self.iterstart.time_till(self.iterend) * 1e-3
def finalise(self):
"""Copies data from GPU back to CPU to save to file(s)."""
# Copy output from receivers array back to correct receiver objects
if self.grid.rxs:
dtoh_rx_array(self.rxs_dev.get(), self.rxcoords_dev.get(), self.grid)
# Copy data from any snapshots back to correct snapshot objects
if self.grid.snapshots and not config.get_model_config().device["snapsgpu2cpu"]:
for i, snap in enumerate(self.grid.snapshots):
dtoh_snapshot_array(
self.snapEx_dev.get(),
self.snapEy_dev.get(),
self.snapEz_dev.get(),
self.snapHx_dev.get(),
self.snapHy_dev.get(),
self.snapHz_dev.get(),
i,
snap,
)
def cleanup(self):
"""Cleanup GPU context."""
# Remove context from top of stack and clear
self.ctx.pop()
self.ctx = None

查看文件

@@ -0,0 +1,30 @@
# 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/>.
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.updates.cpu_updates import CPUUpdates
class MPIUpdates(CPUUpdates[MPIGrid]):
"""Defines update functions for MPI CPU-based solver."""
def halo_swap_electric(self):
self.grid.halo_swap_electric()
def halo_swap_magnetic(self):
self.grid.halo_swap_magnetic()

查看文件

@@ -0,0 +1,614 @@
# 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 importlib import import_module
import numpy as np
from jinja2 import Environment, PackageLoader
from gprMax import config
from gprMax.cuda_opencl import (
knl_fields_updates,
knl_snapshots,
knl_source_updates,
knl_store_outputs,
)
from gprMax.grid.opencl_grid import OpenCLGrid
from gprMax.receivers import dtoh_rx_array, htod_rx_arrays
from gprMax.snapshots import Snapshot, dtoh_snapshot_array, htod_snapshot_array
from gprMax.sources import htod_src_arrays
from gprMax.updates.updates import Updates
logger = logging.getLogger(__name__)
class OpenCLUpdates(Updates[OpenCLGrid]):
"""Defines update functions for OpenCL-based solver."""
def __init__(self, G: OpenCLGrid):
"""
Args:
G: OpenCLGrid class describing a grid in a model.
"""
super().__init__(G)
# Import pyopencl module
self.cl = import_module("pyopencl")
self.elwiseknl = getattr(import_module("pyopencl.elementwise"), "ElementwiseKernel")
# Select device, create context and command queue
self.dev = config.get_model_config().device["dev"]
self.ctx = self.cl.Context(devices=[self.dev])
self.queue = self.cl.CommandQueue(
self.ctx, properties=self.cl.command_queue_properties.PROFILING_ENABLE
)
# Enviroment for templating kernels
self.env = Environment(loader=PackageLoader("gprMax", "cuda_opencl"))
# Initialise arrays on device, prepare kernels, and get kernel functions
self._set_macros()
self._set_field_knls()
if self.grid.pmls["slabs"]:
self._set_pml_knls()
if self.grid.rxs:
self._set_rx_knl()
if self.grid.voltagesources + self.grid.hertziandipoles + self.grid.magneticdipoles:
self._set_src_knls()
if self.grid.snapshots:
self._set_snapshot_knl()
def _set_macros(self):
"""Common macros to be used in kernels."""
# Set specific values for any dispersive materials
if config.get_model_config().materials["maxpoles"] > 0:
NY_MATDISPCOEFFS = self.grid.updatecoeffsdispersive.shape[1]
NX_T = self.grid.Tx.shape[1]
NY_T = self.grid.Tx.shape[2]
NZ_T = self.grid.Tx.shape[3]
else: # Set to one any substitutions for dispersive materials.
NY_MATDISPCOEFFS = 1
NX_T = 1
NY_T = 1
NZ_T = 1
self.knl_common = self.env.get_template("knl_common_opencl.tmpl").render(
updatecoeffsE=self.grid.updatecoeffsE.ravel(),
updatecoeffsH=self.grid.updatecoeffsH.ravel(),
REAL=config.sim_config.dtypes["C_float_or_double"],
N_updatecoeffsE=self.grid.updatecoeffsE.size,
N_updatecoeffsH=self.grid.updatecoeffsH.size,
NY_MATCOEFFS=self.grid.updatecoeffsE.shape[1],
NY_MATDISPCOEFFS=NY_MATDISPCOEFFS,
NX_FIELDS=self.grid.nx + 1,
NY_FIELDS=self.grid.ny + 1,
NZ_FIELDS=self.grid.nz + 1,
NX_ID=self.grid.ID.shape[1],
NY_ID=self.grid.ID.shape[2],
NZ_ID=self.grid.ID.shape[3],
NX_T=NX_T,
NY_T=NY_T,
NZ_T=NZ_T,
NY_RXCOORDS=3,
NX_RXS=6,
NY_RXS=self.grid.iterations,
NZ_RXS=len(self.grid.rxs),
NY_SRCINFO=4,
NY_SRCWAVES=self.grid.iterations,
NX_SNAPS=Snapshot.nx_max,
NY_SNAPS=Snapshot.ny_max,
NZ_SNAPS=Snapshot.nz_max,
)
def _set_field_knls(self):
"""Electric and magnetic field updates - prepares kernels, and
gets kernel functions.
"""
subs = {
"CUDA_IDX": "",
"NX_FIELDS": self.grid.nx + 1,
"NY_FIELDS": self.grid.ny + 1,
"NZ_FIELDS": self.grid.nz + 1,
"NX_ID": self.grid.ID.shape[1],
"NY_ID": self.grid.ID.shape[2],
"NZ_ID": self.grid.ID.shape[3],
}
self.update_electric_dev = self.elwiseknl(
self.ctx,
knl_fields_updates.update_electric["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_fields_updates.update_electric["func"].substitute(subs),
"update_electric",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
self.update_magnetic_dev = self.elwiseknl(
self.ctx,
knl_fields_updates.update_magnetic["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_fields_updates.update_magnetic["func"].substitute(subs),
"update_magnetic",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
# If there are any dispersive materials (updates are split into two
# parts as they require present and updated electric field values).
if config.get_model_config().materials["maxpoles"] > 0:
subs = {
"CUDA_IDX": "",
"REAL": config.sim_config.dtypes["C_float_or_double"],
"REALFUNC": config.get_model_config().materials["crealfunc"],
"NX_FIELDS": self.grid.nx + 1,
"NY_FIELDS": self.grid.ny + 1,
"NZ_FIELDS": self.grid.nz + 1,
"NX_ID": self.grid.ID.shape[1],
"NY_ID": self.grid.ID.shape[2],
"NZ_ID": self.grid.ID.shape[3],
"NX_T": self.grid.Tx.shape[1],
"NY_T": self.grid.Tx.shape[2],
"NZ_T": self.grid.Tx.shape[3],
}
self.dispersive_update_a = self.elwiseknl(
self.ctx,
knl_fields_updates.update_electric_dispersive_A["args_opencl"].substitute(
{
"REAL": config.sim_config.dtypes["C_float_or_double"],
"COMPLEX": config.get_model_config().materials["dispersiveCdtype"],
}
),
knl_fields_updates.update_electric_dispersive_A["func"].substitute(subs),
"update_electric_dispersive_A",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
self.dispersive_update_b = self.elwiseknl(
self.ctx,
knl_fields_updates.update_electric_dispersive_B["args_opencl"].substitute(
{
"REAL": config.sim_config.dtypes["C_float_or_double"],
"COMPLEX": config.get_model_config().materials["dispersiveCdtype"],
}
),
knl_fields_updates.update_electric_dispersive_B["func"].substitute(subs),
"update_electric_dispersive_B",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
# Initialise field arrays on compute device
self.grid.htod_geometry_arrays(self.queue)
self.grid.htod_field_arrays(self.queue)
if config.get_model_config().materials["maxpoles"] > 0:
self.grid.htod_dispersive_arrays(self.queue)
def _set_pml_knls(self):
"""PMLS - prepares kernels and gets kernel functions."""
knl_pml_updates_electric = import_module(
"gprMax.cuda_opencl.knl_pml_updates_electric_" + self.grid.pmls["formulation"]
)
knl_pml_updates_magnetic = import_module(
"gprMax.cuda_opencl.knl_pml_updates_magnetic_" + self.grid.pmls["formulation"]
)
subs = {
"CUDA_IDX": "",
"REAL": config.sim_config.dtypes["C_float_or_double"],
"NX_FIELDS": self.grid.nx + 1,
"NY_FIELDS": self.grid.ny + 1,
"NZ_FIELDS": self.grid.nz + 1,
"NX_ID": self.grid.ID.shape[1],
"NY_ID": self.grid.ID.shape[2],
"NZ_ID": self.grid.ID.shape[3],
}
# Set workgroup size, initialise arrays on compute device, and get
# kernel functions
for pml in self.grid.pmls["slabs"]:
pml.set_queue(self.queue)
pml.htod_field_arrays()
knl_name = f"order{len(pml.CFS)}_{pml.direction}"
knl_electric_name = getattr(knl_pml_updates_electric, knl_name)
knl_magnetic_name = getattr(knl_pml_updates_magnetic, knl_name)
pml.update_electric_dev = self.elwiseknl(
self.ctx,
knl_electric_name["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_electric_name["func"].substitute(subs),
f"pml_updates_electric_{knl_name}",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
pml.update_magnetic_dev = self.elwiseknl(
self.ctx,
knl_magnetic_name["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_magnetic_name["func"].substitute(subs),
f"pml_updates_magnetic_{knl_name}",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
def _set_rx_knl(self):
"""Receivers - initialises arrays on compute device, prepares kernel and
gets kernel function.
"""
self.rxcoords_dev, self.rxs_dev = htod_rx_arrays(self.grid, self.queue)
self.store_outputs_dev = self.elwiseknl(
self.ctx,
knl_store_outputs.store_outputs["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_store_outputs.store_outputs["func"].substitute({"CUDA_IDX": ""}),
"store_outputs",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
def _set_src_knls(self):
"""Sources - initialises arrays on compute device, prepares kernel and
gets kernel function.
"""
if self.grid.hertziandipoles:
(
self.srcinfo1_hertzian_dev,
self.srcinfo2_hertzian_dev,
self.srcwaves_hertzian_dev,
) = htod_src_arrays(self.grid.hertziandipoles, self.grid, self.queue)
self.update_hertzian_dipole_dev = self.elwiseknl(
self.ctx,
knl_source_updates.update_hertzian_dipole["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_source_updates.update_hertzian_dipole["func"].substitute(
{"CUDA_IDX": "", "REAL": config.sim_config.dtypes["C_float_or_double"]}
),
"update_hertzian_dipole",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
if self.grid.magneticdipoles:
(
self.srcinfo1_magnetic_dev,
self.srcinfo2_magnetic_dev,
self.srcwaves_magnetic_dev,
) = htod_src_arrays(self.grid.magneticdipoles, self.grid, self.queue)
self.update_magnetic_dipole_dev = self.elwiseknl(
self.ctx,
knl_source_updates.update_magnetic_dipole["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_source_updates.update_magnetic_dipole["func"].substitute(
{"CUDA_IDX": "", "REAL": config.sim_config.dtypes["C_float_or_double"]}
),
"update_magnetic_dipole",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
if self.grid.voltagesources:
(
self.srcinfo1_voltage_dev,
self.srcinfo2_voltage_dev,
self.srcwaves_voltage_dev,
) = htod_src_arrays(self.grid.voltagesources, self.grid, self.queue)
self.update_voltage_source_dev = self.elwiseknl(
self.ctx,
knl_source_updates.update_voltage_source["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_source_updates.update_voltage_source["func"].substitute(
{"CUDA_IDX": "", "REAL": config.sim_config.dtypes["C_float_or_double"]}
),
"update_voltage_source",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
def _set_snapshot_knl(self):
"""Snapshots - initialises arrays on compute device, prepares kernel and
gets kernel function.
"""
(
self.snapEx_dev,
self.snapEy_dev,
self.snapEz_dev,
self.snapHx_dev,
self.snapHy_dev,
self.snapHz_dev,
) = htod_snapshot_array(self.grid, self.queue)
self.store_snapshot_dev = self.elwiseknl(
self.ctx,
knl_snapshots.store_snapshot["args_opencl"].substitute(
{"REAL": config.sim_config.dtypes["C_float_or_double"]}
),
knl_snapshots.store_snapshot["func"].substitute(
{
"CUDA_IDX": "",
"NX_SNAPS": Snapshot.nx_max,
"NY_SNAPS": Snapshot.ny_max,
"NZ_SNAPS": Snapshot.nz_max,
}
),
"store_snapshot",
preamble=self.knl_common,
options=config.sim_config.devices["compiler_opts"],
)
def store_outputs(self, iteration):
"""Stores field component values for every receiver."""
if self.grid.rxs:
self.store_outputs_dev(
np.int32(len(self.grid.rxs)),
np.int32(iteration),
self.rxcoords_dev,
self.rxs_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
self.grid.Hx_dev,
self.grid.Hy_dev,
self.grid.Hz_dev,
)
def store_snapshots(self, iteration):
"""Stores any snapshots.
Args:
iteration: int for iteration number.
"""
for i, snap in enumerate(self.grid.snapshots):
if snap.time == iteration + 1:
snapno = 0 if config.get_model_config().device["snapsgpu2cpu"] else i
self.store_snapshot_dev(
np.int32(snapno),
np.int32(snap.xs),
np.int32(snap.xf),
np.int32(snap.ys),
np.int32(snap.yf),
np.int32(snap.zs),
np.int32(snap.zf),
np.int32(snap.dx),
np.int32(snap.dy),
np.int32(snap.dz),
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
self.grid.Hx_dev,
self.grid.Hy_dev,
self.grid.Hz_dev,
self.snapEx_dev,
self.snapEy_dev,
self.snapEz_dev,
self.snapHx_dev,
self.snapHy_dev,
self.snapHz_dev,
)
if config.get_model_config().device["snapsgpu2cpu"]:
dtoh_snapshot_array(
self.snapEx_dev.get(),
self.snapEy_dev.get(),
self.snapEz_dev.get(),
self.snapHx_dev.get(),
self.snapHy_dev.get(),
self.snapHz_dev.get(),
0,
snap,
)
def update_magnetic(self):
"""Updates magnetic field components."""
self.update_magnetic_dev(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
self.grid.ID_dev,
self.grid.Hx_dev,
self.grid.Hy_dev,
self.grid.Hz_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
)
def update_magnetic_pml(self):
"""Updates magnetic field components with the PML correction."""
for pml in self.grid.pmls["slabs"]:
pml.update_magnetic()
def update_magnetic_sources(self, iteration):
"""Updates magnetic field components from sources."""
if self.grid.magneticdipoles:
self.update_magnetic_dipole_dev(
np.int32(len(self.grid.magneticdipoles)),
np.int32(iteration),
config.sim_config.dtypes["float_or_double"](self.grid.dx),
config.sim_config.dtypes["float_or_double"](self.grid.dy),
config.sim_config.dtypes["float_or_double"](self.grid.dz),
self.srcinfo1_magnetic_dev,
self.srcinfo2_magnetic_dev,
self.srcwaves_magnetic_dev,
self.grid.ID_dev,
self.grid.Hx_dev,
self.grid.Hy_dev,
self.grid.Hz_dev,
)
def update_electric_a(self):
"""Updates electric field components."""
# All materials are non-dispersive so do standard update.
if config.get_model_config().materials["maxpoles"] == 0:
self.update_electric_dev(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
self.grid.ID_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
self.grid.Hx_dev,
self.grid.Hy_dev,
self.grid.Hz_dev,
)
# If there are any dispersive materials do 1st part of dispersive update
# (it is split into two parts as it requires present and updated electric field values).
else:
self.dispersive_update_a(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
np.int32(config.get_model_config().materials["maxpoles"]),
self.grid.ID_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
self.grid.Hx_dev,
self.grid.Hy_dev,
self.grid.Hz_dev,
self.grid.updatecoeffsdispersive_dev,
self.grid.Tx_dev,
self.grid.Ty_dev,
self.grid.Tz_dev,
)
def update_electric_pml(self):
"""Updates electric field components with the PML correction."""
for pml in self.grid.pmls["slabs"]:
pml.update_electric()
def update_electric_sources(self, iteration):
"""Updates electric field components from sources -
update any Hertzian dipole sources last.
"""
if self.grid.voltagesources:
self.update_voltage_source_dev(
np.int32(len(self.grid.voltagesources)),
np.int32(iteration),
config.sim_config.dtypes["float_or_double"](self.grid.dx),
config.sim_config.dtypes["float_or_double"](self.grid.dy),
config.sim_config.dtypes["float_or_double"](self.grid.dz),
self.srcinfo1_voltage_dev,
self.srcinfo2_voltage_dev,
self.srcwaves_voltage_dev,
self.grid.ID_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
)
if self.grid.hertziandipoles:
self.update_hertzian_dipole_dev(
np.int32(len(self.grid.hertziandipoles)),
np.int32(self.grid.iteration),
config.sim_config.dtypes["float_or_double"](self.grid.dx),
config.sim_config.dtypes["float_or_double"](self.grid.dy),
config.sim_config.dtypes["float_or_double"](self.grid.dz),
self.srcinfo1_hertzian_dev,
self.srcinfo2_hertzian_dev,
self.srcwaves_hertzian_dev,
self.grid.ID_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
)
self.grid.iteration += 1
def update_electric_b(self):
"""If there are any dispersive materials do 2nd part of dispersive
update - it is split into two parts as it requires present and
updated electric field values. Therefore it can only be completely
updated after the electric field has been updated by the PML and
source updates.
"""
if config.get_model_config().materials["maxpoles"] > 0:
self.dispersive_update_b(
np.int32(self.grid.nx),
np.int32(self.grid.ny),
np.int32(self.grid.nz),
np.int32(config.get_model_config().materials["maxpoles"]),
self.grid.ID_dev,
self.grid.Ex_dev,
self.grid.Ey_dev,
self.grid.Ez_dev,
self.grid.updatecoeffsdispersive_dev,
self.grid.Tx_dev,
self.grid.Ty_dev,
self.grid.Tz_dev,
)
def time_start(self):
"""Starts event timers used to calculate solving time for model."""
self.event_marker1 = self.cl.enqueue_marker(self.queue)
self.event_marker1.wait()
def calculate_memory_used(self, iteration):
"""Calculates memory used on last iteration.
Args:
iteration: int for iteration number.
Returns:
Memory (RAM) used on compute device.
"""
# No clear way to determine memory used from PyOpenCL unlike PyCUDA.
pass
def calculate_solve_time(self):
"""Calculates solving time for model."""
event_marker2 = self.cl.enqueue_marker(self.queue)
event_marker2.wait()
return (event_marker2.profile.end - self.event_marker1.profile.start) * 1e-9
def finalise(self):
"""Copies data from compute device back to CPU to save to file(s)."""
# Copy output from receivers array back to correct receiver objects
if self.grid.rxs:
dtoh_rx_array(self.rxs_dev.get(), self.rxcoords_dev.get(), self.grid)
# Copy data from any snapshots back to correct snapshot objects
if self.grid.snapshots and not config.get_model_config().device["snapsgpu2cpu"]:
for i, snap in enumerate(self.grid.snapshots):
dtoh_snapshot_array(
self.snapEx_dev.get(),
self.snapEy_dev.get(),
self.snapEz_dev.get(),
self.snapHx_dev.get(),
self.snapHy_dev.get(),
self.snapHz_dev.get(),
i,
snap,
)
def cleanup(self):
pass

109
gprMax/updates/updates.py 普通文件
查看文件

@@ -0,0 +1,109 @@
# 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/>.
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from gprMax.grid.fdtd_grid import FDTDGrid
GridType = TypeVar("GridType", bound=FDTDGrid)
class Updates(Generic[GridType], ABC):
"""Defines update functions for a solver."""
def __init__(self, G: GridType):
"""
Args:
G: FDTDGrid class describing a grid in a model.
"""
self.grid = G
@abstractmethod
def store_outputs(self, iteration: int) -> None:
"""Stores field component values for every receiver and transmission line."""
pass
@abstractmethod
def store_snapshots(self, iteration: int) -> None:
"""Stores any snapshots.
Args:
iteration: int for iteration number.
"""
pass
@abstractmethod
def update_magnetic(self) -> None:
"""Updates magnetic field components."""
pass
@abstractmethod
def update_magnetic_pml(self) -> None:
"""Updates magnetic field components with the PML correction."""
pass
@abstractmethod
def update_magnetic_sources(self, iteration: int) -> None:
"""Updates magnetic field components from sources."""
pass
@abstractmethod
def update_electric_a(self) -> None:
"""Updates electric field components."""
pass
@abstractmethod
def update_electric_pml(self) -> None:
"""Updates electric field components with the PML correction."""
pass
@abstractmethod
def update_electric_sources(self, iteration: int) -> None:
"""Updates electric field components from sources -
update any Hertzian dipole sources last.
"""
pass
@abstractmethod
def update_electric_b(self) -> None:
"""If there are any dispersive materials do 2nd part of dispersive
update - it is split into two parts as it requires present and
updated electric field values. Therefore it can only be completely
updated after the electric field has been updated by the PML and
source updates.
"""
pass
@abstractmethod
def time_start(self) -> None:
"""Starts timer used to calculate solving time for model."""
pass
@abstractmethod
def calculate_solve_time(self) -> float:
"""Calculates solving time for model."""
pass
def finalise(self) -> None:
pass
def cleanup(self) -> None:
pass

查看文件

@@ -15,15 +15,20 @@
#
# You should have received a copy of the GNU General Public License
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import logging
from typing import Generic, Tuple
import numpy as np
import numpy.typing as npt
from typing_extensions import TypeVar
import gprMax.config as config
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.subgrids.grid import SubGridBaseGrid
from .subgrids.grid import SubGridBaseGrid
from .utilities.utilities import round_value
from .utilities.utilities import round_int
logger = logging.getLogger(__name__)
@@ -36,33 +41,18 @@ logger = logging.getLogger(__name__)
encapulsated here.
"""
def create_user_input_points(grid, user_obj):
"""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)
GridType = TypeVar("GridType", bound=FDTDGrid)
class UserInput:
class UserInput(Generic[GridType]):
"""Handles (x, y, z) points supplied by the user."""
def __init__(self, grid):
def __init__(self, grid: GridType):
self.grid = grid
def point_within_bounds(self, p, cmd_str, name):
def point_within_bounds(self, p: npt.NDArray[np.int32], cmd_str: str, name: str = "") -> bool:
try:
self.grid.within_bounds(p)
return self.grid.within_bounds(p)
except ValueError as err:
v = ["x", "y", "z"]
# Discretisation
@@ -76,77 +66,274 @@ class UserInput:
logger.exception(s)
raise
def discretise_point(self, p):
"""Gets the index of a continuous point with the grid."""
rv = np.vectorize(round_value)
return rv(p / self.grid.dl)
def discretise_static_point(self, point: Tuple[float, float, float]) -> npt.NDArray[np.int32]:
"""Get the nearest grid index to a continuous static point.
def round_to_grid(self, p):
"""Gets the nearest continuous point on the grid from a continuous point
in space.
For a static point, the point of the origin of the grid is
ignored. I.e. it is assumed to be at (0, 0, 0). There are no
checks of the validity of the point such as bound checking.
Args:
point: x, y, z coordinates of the point in space.
Returns:
discretised_point: x, y, z indices of the point on the grid.
"""
return self.discretise_point(p) * self.grid.dl
rv = np.vectorize(round_int, otypes=[np.int32])
return rv(point / self.grid.dl)
def descretised_to_continuous(self, p):
"""Returns a point given as indices to a continuous point in the real space."""
return p * self.grid.dl
def round_to_grid_static_point(
self, point: Tuple[float, float, float]
) -> npt.NDArray[np.float64]:
"""Round a continuous static point to the nearest point on the grid.
For a static point, the point of the origin of the grid is
ignored. I.e. it is assumed to be at (0, 0, 0). There are no
checks of the validity of the point such as bound checking.
Args:
point: x, y, z coordinates of the point in space.
Returns:
rounded_point: x, y, z coordinates of the nearest continuous
point on the grid.
"""
return self.discretise_static_point(point) * self.grid.dl
def discretise_point(self, point: Tuple[float, float, float]) -> npt.NDArray[np.int32]:
"""Get the nearest grid index to a continuous static point.
This function translates user points to the correct index for
building objects. Points will be mapped from the user coordinate
space to the local coordinate space of the grid. There are no
checks of the validity of the point such as bound checking.
Args:
point: x, y, z coordinates of the point in space.
Returns:
discretised_point: x, y, z indices of the point on the grid.
"""
return self.discretise_static_point(point)
def round_to_grid(self, point: Tuple[float, float, float]) -> npt.NDArray[np.float64]:
"""Round a continuous static point to the nearest point on the grid.
The point will be mapped from the user coordinate space to the
local coordinate space of the grid. There are no checks of the
validity of the point such as bound checking.
Args:
point: x, y, z coordinates of the point in space.
Returns:
rounded_point: x, y, z coordinates of the nearest continuous
point on the grid.
"""
return self.discretise_point(point) * self.grid.dl
def discrete_to_continuous(self, point: npt.NDArray[np.int32]) -> npt.NDArray[np.float64]:
return point * self.grid.dl
class MainGridUserInput(UserInput):
class MainGridUserInput(UserInput[GridType]):
"""Handles (x, y, z) points supplied by the user in the main grid."""
def __init__(self, grid):
super().__init__(grid)
def check_point(self, p, cmd_str, name=""):
def check_point(
self, point: Tuple[float, float, float], cmd_str: str, name: str = ""
) -> Tuple[bool, npt.NDArray[np.int32]]:
"""Discretises point and check its within the domain"""
p = self.discretise_point(p)
self.point_within_bounds(p, cmd_str, name)
return p
discretised_point = self.discretise_point(point)
within_bounds = self.point_within_bounds(discretised_point, cmd_str, name)
return within_bounds, discretised_point
def check_src_rx_point(self, p, cmd_str, name=""):
p = self.check_point(p, cmd_str, name)
def check_src_rx_point(
self, point: Tuple[float, float, float], cmd_str: str, name: str = ""
) -> Tuple[bool, npt.NDArray[np.int32]]:
within_bounds, discretised_point = self.check_point(point, cmd_str, name)
if self.grid.within_pml(p):
if self.grid.within_pml(discretised_point):
logger.warning(
f"'{cmd_str}' sources and receivers should not normally be positioned within the PML."
)
return p
return within_bounds, discretised_point
def check_box_points(self, p1, p2, cmd_str):
p1 = self.check_point(p1, cmd_str, name="lower")
p2 = self.check_point(p2, cmd_str, name="upper")
def _check_2d_points(
self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], cmd_str: str
) -> Tuple[bool, npt.NDArray[np.int32], npt.NDArray[np.int32]]:
lower_within_grid, lower_point = self.check_point(p1, cmd_str, "lower")
upper_within_grid, upper_point = self.check_point(p2, cmd_str, "upper")
if np.greater(p1, p2).any():
logger.exception(
if np.greater(lower_point, upper_point).any() or np.equal(lower_point, upper_point).all():
raise ValueError(
f"'{cmd_str}' the lower coordinates should be less than the upper coordinates."
)
raise ValueError
return p1, p2
return lower_within_grid and upper_within_grid, lower_point, upper_point
def check_tri_points(self, p1, p2, p3, cmd_str):
p1 = self.check_point(p1, cmd_str, name="vertex_1")
p2 = self.check_point(p2, cmd_str, name="vertex_2")
p3 = self.check_point(p3, cmd_str, name="vertex_3")
def check_output_object_bounds(
self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], cmd_str: str
) -> Tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]]:
# We only care if the bounds are in the global grid (an error
# will be thrown if that is not the case).
_, lower_bound, upper_bound = self._check_2d_points(p1, p2, cmd_str)
return lower_bound, upper_bound
return p1, p2, p3
def check_box_points(
self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], cmd_str: str
) -> Tuple[bool, npt.NDArray[np.int32], npt.NDArray[np.int32]]:
return self._check_2d_points(p1, p2, cmd_str)
def discretise_static_point(self, p):
"""Gets the index of a continuous point regardless of the point of
origin of the grid.
def check_tri_points(
self,
p1: Tuple[float, float, float],
p2: Tuple[float, float, float],
p3: Tuple[float, float, float],
cmd_str: str,
) -> Tuple[npt.NDArray[np.int32], npt.NDArray[np.int32], npt.NDArray[np.int32]]:
# We only care if the point are in the global grid (an error
# will be thrown if that is not the case).
_, p1_checked = self.check_point(p1, cmd_str, name="vertex_1")
_, p2_checked = self.check_point(p2, cmd_str, name="vertex_2")
_, p3_checked = self.check_point(p3, cmd_str, name="vertex_3")
return p1_checked, p2_checked, p3_checked
def check_thickness(
self,
dimension: str,
lower_extent: float,
thickness: float,
cmd_str: str,
) -> Tuple[bool, float, float]:
"""Check the thickness of an object in a specified dimension.
Args:
dimension: Dimension to check the thickness value for.
This must have value x, y, or z.
lower_extent: Lower extent of the object in the specified
dimension.
thickness: Thickness of the object.
Raises:
ValueError: Raised if dimension has an invalid value.
Returns:
within_grid: True if part of the object is within the
current grid. False otherwise.
lower_extent: Lower extent limited to the bounds of the
grid.
thickness: Thickness value such that lower_extent +
thickness is within the bounds of the grid.
"""
return super().discretise_point(p)
if thickness < 0:
raise ValueError(f"'{cmd_str}' requires a non negative thickness")
def round_to_grid_static_point(self, p):
"""Gets the index of a continuous point regardless of the point of
origin of the grid.
if lower_extent < 0:
raise ValueError(
f"'{cmd_str}' lower extent should be non negative in the {dimension} dimension"
)
upper_extent = lower_extent + thickness
if dimension == "x":
lower_point = self.discretise_point((lower_extent, 0, 0))
upper_point = self.discretise_point((upper_extent, 0, 0))
thickness_point = self.discretise_static_point((thickness, 0, 0))
index = 0
elif dimension == "y":
lower_point = self.discretise_point((0, lower_extent, 0))
upper_point = self.discretise_point((0, upper_extent, 0))
thickness_point = self.discretise_static_point((0, thickness, 0))
index = 1
elif dimension == "z":
lower_point = self.discretise_point((0, 0, lower_extent))
upper_point = self.discretise_point((0, 0, upper_extent))
thickness_point = self.discretise_static_point((0, 0, thickness))
index = 2
else:
raise ValueError("Dimension should have value x, y, or z")
try:
self.grid.within_bounds(upper_point)
except ValueError:
raise ValueError(
f"'{cmd_str}' extends beyond the size of the model in the {dimension} dimension"
)
# Work with discretised (int) values as reduces imprecision due
# to floating point calculations
size = self.grid.size[index]
lower_extent = lower_point[index]
upper_extent = upper_point[index]
thickness = thickness_point[index]
# These should only trigger for MPIGrids.
# TODO: Can this be structured so these checks happen in the
# MPIGridUserInput object?
if lower_extent < 0:
thickness += lower_extent
lower_extent = 0
if upper_extent > size:
thickness -= upper_extent - size
dl = self.grid.dl[index]
return (
lower_extent <= size
and upper_extent >= 0
and not (upper_extent > size and thickness <= 0),
lower_extent * dl,
thickness * dl,
)
class MPIUserInput(MainGridUserInput[MPIGrid]):
"""Handles (x, y, z) points supplied by the user for MPI grids.
This class autotranslates points from the global coordinate system
to the grid's local coordinate system.
"""
def discretise_point(self, point: Tuple[float, float, float]) -> npt.NDArray[np.int32]:
"""Get the nearest grid index to a continuous static point.
This function translates user points to the correct index for
building objects. Points will be mapped from the global
coordinate space to the local coordinate space of the grid.
There are no checks of the validity of the point such as bound
checking.
Args:
point: x, y, z coordinates of the point in space.
Returns:
discretised_point: x, y, z indices of the point on the grid.
"""
return super().discretise_point(p) * self.grid.dl
discretised_point = super().discretise_point(point)
return self.grid.global_to_local_coordinate(discretised_point)
def check_box_points(
self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], cmd_str: str
) -> Tuple[bool, npt.NDArray[np.int32], npt.NDArray[np.int32]]:
_, lower_point, upper_point = super().check_box_points(p1, p2, cmd_str)
# Restrict points to the bounds of the local grid
lower_point = np.where(lower_point < 0, 0, lower_point)
upper_point = np.where(upper_point > self.grid.size, self.grid.size, upper_point)
return (
all(lower_point <= upper_point) and all(lower_point < self.grid.size),
lower_point,
upper_point,
)
class SubgridUserInput(MainGridUserInput):
class SubgridUserInput(MainGridUserInput[SubGridBaseGrid]):
"""Handles (x, y, z) points supplied by the user in the subgrid.
This class autotranslates points from main grid to subgrid equivalent
(within IS). Useful if material traverse is not required.
@@ -162,7 +349,7 @@ class SubgridUserInput(MainGridUserInput):
self.outer_bound = np.subtract([grid.nx, grid.ny, grid.nz], self.inner_bound)
def translate_to_gap(self, p):
def translate_to_gap(self, p) -> npt.NDArray[np.int32]:
"""Translates the user input point to the real point in the subgrid."""
p1 = (p[0] - self.grid.i0 * self.grid.ratio) + self.grid.n_boundary_cells_x
@@ -171,41 +358,39 @@ class SubgridUserInput(MainGridUserInput):
return np.array([p1, p2, p3])
def discretise_point(self, p):
"""Discretises a point. Does not provide any checks. The user enters
coordinates relative to self.inner_bound. This function translate
the user point to the correct index for building objects.
def discretise_point(self, point: Tuple[float, float, float]) -> npt.NDArray[np.int32]:
"""Get the nearest grid index to a continuous static point.
This function translates user points to the correct index for
building objects. The user enters coordinates relative to
self.inner_bound which are mapped to the local coordinate space
of the grid. There are no checks of the validity of the point
such as bound checking.
Args:
point: x, y, z coordinates of the point in space relative to
self.inner_bound.
Returns:
discretised_point: x, y, z indices of the point on the grid.
"""
p = super().discretise_point(p)
p_t = self.translate_to_gap(p)
return p_t
discretised_point = super().discretise_point(point)
discretised_point = self.translate_to_gap(discretised_point)
return discretised_point
def round_to_grid(self, p):
p_t = self.discretise_point(p)
p_m = p_t * self.grid.dl
return p_m
def check_point(self, p, cmd_str, name=""):
p_t = super().check_point(p, cmd_str, name)
def check_point(
self, point: Tuple[float, float, float], cmd_str: str, name: str = ""
) -> Tuple[bool, npt.NDArray[np.int32]]:
within_grid, discretised_point = super().check_point(point, cmd_str, name)
# Provide user within a warning if they have placed objects within
# the OS non-working region.
if (
np.less(p_t, self.inner_bound).any()
or np.greater(p_t, self.outer_bound).any()
np.less(discretised_point, self.inner_bound).any()
or np.greater(discretised_point, self.outer_bound).any()
):
logger.warning(
f"'{cmd_str}' this object traverses the Outer Surface. This is an advanced feature."
)
return p_t
def discretise_static_point(self, p):
"""Gets the index of a continuous point regardless of the point of
origin of the grid."""
return super().discretise_point(p)
def round_to_grid_static_point(self, p):
"""Gets the index of a continuous point regardless of the point of
origin of the grid."""
return super().discretise_point(p) * self.grid.dl
return within_grid, discretised_point

查看文件

@@ -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,21 +88,18 @@ 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
]
volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id]
try:
volume = volumes[0]
except NameError:
logger.exception(
f"{self.__str__()} cannot find FractalBox {fractal_box_id}"
)
logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
raise
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
uip = self._create_uip(grid)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
@@ -120,9 +117,7 @@ class AddGrass(UserObjectGeometry):
# Check for valid orientations
if xs == xf:
if ys == yf or zs == zf:
logger.exception(
f"{self.__str__()} dimensions are not specified correctly"
)
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if xs not in [volume.xs, volume.xf]:
logger.exception(
@@ -152,9 +147,7 @@ class AddGrass(UserObjectGeometry):
elif ys == yf:
if zs == zf:
logger.exception(
f"{self.__str__()} dimensions are not specified correctly"
)
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if ys not in [volume.ys, volume.yf]:
logger.exception(

查看文件

@@ -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,28 +88,24 @@ 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
]
volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id]
if volumes:
volume = volumes[0]
else:
logger.exception(
f"{self.__str__()} cannot find FractalBox {fractal_box_id}"
)
logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
raise ValueError
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
uip = self._create_uip(grid)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
if frac_dim < 0:
logger.exception(
f"{self.__str__()} requires a positive value for the "
+ "fractal dimension"
f"{self.__str__()} requires a positive value for the fractal dimension"
)
raise ValueError
if weighting[0] < 0:
@@ -128,9 +124,7 @@ class AddSurfaceRoughness(UserObjectGeometry):
# Check for valid orientations
if xs == xf:
if ys == yf or zs == zf:
logger.exception(
f"{self.__str__()} dimensions are not specified correctly"
)
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if xs not in [volume.xs, volume.xf]:
logger.exception(
@@ -166,9 +160,7 @@ class AddSurfaceRoughness(UserObjectGeometry):
elif ys == yf:
if zs == zf:
logger.exception(
f"{self.__str__()} dimensions are not specified correctly"
)
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if ys not in [volume.ys, volume.yf]:
logger.exception(

查看文件

@@ -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,34 +72,27 @@ 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
]:
if volumes := [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id]:
volume = volumes[0]
else:
logger.exception(
f"{self.__str__()} cannot find FractalBox {fractal_box_id}"
)
logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
raise ValueError
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
uip = self._create_uip(grid)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
if depth <= 0:
logger.exception(
f"{self.__str__()} requires a positive value for the depth of water"
)
logger.exception(f"{self.__str__()} requires a positive value for the depth of water")
raise ValueError
# Check for valid orientations
if xs == xf:
if ys == yf or zs == zf:
logger.exception(
f"{self.__str__()} dimensions are not specified correctly"
)
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if xs not in [volume.xs, volume.xf]:
logger.exception(
@@ -117,9 +110,7 @@ class AddSurfaceWater(UserObjectGeometry):
elif ys == yf:
if zs == zf:
logger.exception(
f"{self.__str__()} dimensions are not specified correctly"
)
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if ys not in [volume.ys, volume.yf]:
logger.exception(
@@ -154,9 +145,7 @@ class AddSurfaceWater(UserObjectGeometry):
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
surface = next(
(x for x in volume.fractalsurfaces if x.surfaceID == requestedsurface), None
)
surface = next((x for x in volume.fractalsurfaces if x.surfaceID == requestedsurface), None)
if not surface:
logger.exception(
f"{self.__str__()} specified surface {requestedsurface} does not have a rough surface applied"

查看文件

@@ -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 check_averaging, 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
@@ -86,13 +85,19 @@ class Box(UserObjectGeometry):
# Check averaging
try:
# Try user-specified averaging
averagebox = self.kwargs["averaging"]
averagebox = check_averaging(averagebox)
averagebox = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averagebox = grid.averagevolumeobjects
p3, p4 = uip.check_box_points(p1, p2, self.__str__())
uip = self._create_uip(grid)
grid_contains_box, p3, p4 = uip.check_box_points(p1, p2, self.__str__())
# Exit early if none of the box is in this grid as there is
# nothing else to do.
if not grid_contains_box:
return
# Find nearest point on grid without translation
p5 = uip.round_to_grid_static_point(p1)
p6 = uip.round_to_grid_static_point(p2)
@@ -118,7 +123,7 @@ class Box(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = materials[0].ID + "+" + materials[1].ID + "+" + materials[2].ID
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -127,18 +132,10 @@ class Box(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)

查看文件

@@ -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.
@@ -75,9 +37,9 @@ def check_averaging(averaging):
averaging: boolean for geometry object material averaging.
"""
if averaging == "y":
if averaging.lower() == "y":
averaging = True
elif averaging == "n":
elif averaging.lower() == "n":
averaging = False
else:
logger.exception("Averaging should be either y or n")

查看文件

@@ -20,14 +20,16 @@ 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.cmds_geometry.cmds_geometry import check_averaging
from gprMax.user_objects.user_objects import GeometryUserObject
logger = logging.getLogger(__name__)
class Cone(UserObjectGeometry):
class Cone(GeometryUserObject):
"""Introduces a circular cone into the model. The difference with the cylinder is that the faces of the cone
can have different radii and one of them can be zero.
@@ -44,27 +46,27 @@ 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"]
r1 = self.kwargs["r1"]
r2 = self.kwargs["r2"]
except KeyError:
logger.exception(
f"{self.__str__()} please specify two points and two radii"
)
logger.exception(f"{self.__str__()} please specify two points and two radii")
raise
# Check averaging
try:
# Try user-specified averaging
averagecone = self.kwargs["averaging"]
averagecone = check_averaging(averagecone)
averagecone = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averagecone = grid.averagevolumeobjects
@@ -81,6 +83,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)
@@ -122,7 +125,7 @@ class Cone(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = materials[0].ID + "+" + materials[1].ID + "+" + materials[2].ID
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -131,18 +134,10 @@ class Cone(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)

查看文件

@@ -20,14 +20,16 @@ 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.cmds_geometry.cmds_geometry import check_averaging
from gprMax.user_objects.user_objects import GeometryUserObject
logger = logging.getLogger(__name__)
class Cylinder(UserObjectGeometry):
class Cylinder(GeometryUserObject):
"""Introduces a circular cylinder into the model.
Attributes:
@@ -42,11 +44,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"]
@@ -58,8 +63,7 @@ class Cylinder(UserObjectGeometry):
# Check averaging
try:
# Try user-specified averaging
averagecylinder = self.kwargs["averaging"]
averagecylinder = check_averaging(averagecylinder)
averagecylinder = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averagecylinder = grid.averagevolumeobjects
@@ -76,6 +80,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)
@@ -83,9 +88,7 @@ class Cylinder(UserObjectGeometry):
x2, y2, z2 = uip.round_to_grid(p2)
if r <= 0:
logger.exception(
f"{self.__str__()} the radius {r:g} should be a positive value."
)
logger.exception(f"{self.__str__()} the radius {r:g} should be a positive value.")
raise ValueError
# Look up requested materials in existing list of material instances
@@ -107,7 +110,7 @@ class Cylinder(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = materials[0].ID + "+" + materials[1].ID + "+" + materials[2].ID
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -116,18 +119,10 @@ class Cylinder(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)

查看文件

@@ -20,14 +20,16 @@ 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.cmds_geometry.cmds_geometry import check_averaging
from gprMax.user_objects.user_objects import GeometryUserObject
logger = logging.getLogger(__name__)
class CylindricalSector(UserObjectGeometry):
class CylindricalSector(GeometryUserObject):
"""Introduces a cylindrical sector (shaped like a slice of pie) into the model.
Attributes:
@@ -51,11 +53,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"]
@@ -70,11 +75,35 @@ class CylindricalSector(UserObjectGeometry):
logger.exception(self.__str__())
raise
# Check thickness of the object first as may be able to exit
# early if fully outside the grid.
uip = self._create_uip(grid)
# yz-plane cylindrical sector
if normal == "x":
level, ctr1, ctr2 = uip.round_to_grid((extent1, ctr1, ctr2))
# xz-plane cylindrical sector
elif normal == "y":
ctr1, level, ctr2 = uip.round_to_grid((ctr1, extent1, ctr2))
# xy-plane cylindrical sector
elif normal == "z":
ctr1, ctr2, level = uip.round_to_grid((ctr1, ctr2, extent1))
sector_within_grid, level, thickness = uip.check_thickness(
normal, extent1, thickness, self.__str__()
)
# Exit early if none of the cylindrical sector is in this grid
# as there is nothing else to do.
if not sector_within_grid:
return
# Check averaging
try:
# Try user-specified averaging
averagecylindricalsector = self.kwargs["averaging"]
averagecylindricalsector = check_averaging(averagecylindricalsector)
averagecylindricalsector = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averagecylindricalsector = grid.averagevolumeobjects
@@ -95,14 +124,10 @@ class CylindricalSector(UserObjectGeometry):
sectorangle = 2 * np.pi * (end / 360)
if normal not in ["x", "y", "z"]:
logger.exception(
f"{self.__str__()} the normal direction must be either x, y or z."
)
logger.exception(f"{self.__str__()} the normal direction must be either x, y or z.")
raise ValueError
if r <= 0:
logger.exception(
f"{self.__str__()} the radius {r:g} should be a positive value."
)
logger.exception(f"{self.__str__()} the radius {r:g} should be a positive value.")
if sectorstartangle < 0 or sectorangle <= 0:
logger.exception(
f"{self.__str__()} the starting angle and sector angle should be a positive values."
@@ -133,7 +158,7 @@ class CylindricalSector(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = f"{materials[0].ID}+{materials[1].ID}+{materials[2].ID}"
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -142,18 +167,10 @@ class CylindricalSector(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)
@@ -171,18 +188,6 @@ class CylindricalSector(UserObjectGeometry):
numIDy = materials[1].numID
numIDz = materials[2].numID
# yz-plane cylindrical sector
if normal == "x":
level, ctr1, ctr2 = uip.round_to_grid((extent1, ctr1, ctr2))
# xz-plane cylindrical sector
elif normal == "y":
ctr1, level, ctr2 = uip.round_to_grid((ctr1, extent1, ctr2))
# xy-plane cylindrical sector
elif normal == "z":
ctr1, ctr2, level = uip.round_to_grid((ctr1, ctr2, extent1))
build_cylindrical_sector(
ctr1,
ctr2,

查看文件

@@ -20,13 +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:
@@ -36,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"]
@@ -65,14 +65,21 @@ class Edge(UserObjectGeometry):
raise
if self.do_rotate:
self._do_rotate()
self._do_rotate(grid)
p3 = uip.round_to_grid_static_point(p1)
p4 = uip.round_to_grid_static_point(p2)
uip = self._create_uip(grid)
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
edge_within_grid, discretised_p1, discretised_p2 = uip.check_box_points(
p1, p2, self.__str__()
)
# Exit early if none of the edge is in this grid as there is
# nothing else to do.
if not edge_within_grid:
return
xs, ys, zs = discretised_p1
xf, yf, zf = discretised_p2
material = next((x for x in grid.materials if x.ID == material_id), None)
@@ -91,21 +98,18 @@ class Edge(UserObjectGeometry):
raise ValueError
elif xs != xf:
for i in range(xs, xf):
build_edge_x(
i, ys, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID
)
build_edge_x(i, ys, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID)
elif ys != yf:
for j in range(ys, yf):
build_edge_y(
xs, j, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID
)
build_edge_y(xs, j, zs, material.numID, grid.rigidE, grid.rigidH, grid.ID)
elif zs != zf:
for k in range(zs, zf):
build_edge_z(
xs, ys, k, material.numID, grid.rigidE, grid.rigidH, grid.ID
)
build_edge_z(xs, ys, k, material.numID, grid.rigidE, grid.rigidH, grid.ID)
p3 = uip.round_to_grid_static_point(p1)
p4 = uip.round_to_grid_static_point(p2)
logger.info(
f"{self.grid_name(grid)}Edge from {p3[0]:g}m, {p3[1]:g}m, "

查看文件

@@ -20,14 +20,16 @@ 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.cmds_geometry.cmds_geometry import check_averaging
from gprMax.user_objects.user_objects import GeometryUserObject
logger = logging.getLogger(__name__)
class Ellipsoid(UserObjectGeometry):
class Ellipsoid(GeometryUserObject):
"""Introduces an ellipsoidal object with specific parameters into the model.
Attributes:
@@ -41,11 +43,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"]
@@ -53,16 +58,13 @@ class Ellipsoid(UserObjectGeometry):
zr = self.kwargs["zr"]
except KeyError:
logger.exception(
f"{self.__str__()} please specify a point and the three semiaxes."
)
logger.exception(f"{self.__str__()} please specify a point and the three semiaxes.")
raise
# Check averaging
try:
# Try user-specified averaging
averageellipsoid = self.kwargs["averaging"]
averageellipsoid = check_averaging(averageellipsoid)
averageellipsoid = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averageellipsoid = grid.averagevolumeobjects
@@ -80,6 +82,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)
@@ -102,7 +105,7 @@ class Ellipsoid(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = materials[0].ID + "+" + materials[1].ID + "+" + materials[2].ID
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -111,18 +114,10 @@ class Ellipsoid(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)

查看文件

@@ -21,19 +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 check_averaging, 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.
@@ -57,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"]
@@ -100,21 +95,28 @@ class FractalBox(UserObjectGeometry):
seed = None
if self.do_rotate:
self._do_rotate()
self._do_rotate(grid)
# Check averaging
try:
# Go with user specified averaging
averagefractalbox = self.kwargs["averaging"]
averagefractalbox = check_averaging(self.kwargs["averaging"])
except KeyError:
# If they havent specified - default is no dielectric smoothing for
# 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)
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
grid_contains_fractal_box, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
# Exit early if none of the fractal box is in this grid as there
# is nothing else to do.
if not grid_contains_fractal_box:
return
xs, ys, zs = p1
xf, yf, zf = p2
@@ -138,16 +140,12 @@ class FractalBox(UserObjectGeometry):
f"{self.__str__()} requires a positive value for the fractal weighting in the z direction"
)
if n_materials < 0:
logger.exception(
f"{self.__str__()} requires a positive value for the number of bins"
)
logger.exception(f"{self.__str__()} requires a positive value for the number of bins")
raise ValueError
# Find materials to use to build fractal volume, either from mixing
# models or normal materials.
mixingmodel = next(
(x for x in grid.mixingmodels if x.ID == mixing_model_id), None
)
mixingmodel = next((x for x in grid.mixingmodels if x.ID == mixing_model_id), None)
material = next((x for x in grid.materials if x.ID == mixing_model_id), None)
nbins = n_materials
@@ -195,9 +193,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:
@@ -244,9 +242,7 @@ class FractalBox(UserObjectGeometry):
dtype=config.sim_config.dtypes["float_or_double"],
)
materialnumID = next(
x.numID
for x in grid.materials
if x.ID == self.volume.operatingonID
x.numID for x in grid.materials if x.ID == self.volume.operatingonID
)
self.volume.fractalvolume *= materialnumID
else:
@@ -255,9 +251,9 @@ class FractalBox(UserObjectGeometry):
for j in range(0, self.volume.ny):
for k in range(0, self.volume.nz):
numberinbin = self.volume.fractalvolume[i, j, k]
self.volume.fractalvolume[i, j, k] = (
self.volume.mixingmodel.matID[int(numberinbin)]
)
self.volume.fractalvolume[i, j, k] = self.volume.mixingmodel.matID[
int(numberinbin)
]
self.volume.generate_volume_mask()
@@ -266,25 +262,16 @@ class FractalBox(UserObjectGeometry):
# TODO: Allow extract of rough surface profile (to print/file?)
for surface in self.volume.fractalsurfaces:
if surface.surfaceID == "xminus":
for i in range(
surface.fractalrange[0], surface.fractalrange[1]
):
for i in range(surface.fractalrange[0], surface.fractalrange[1]):
for j in range(surface.ys, surface.yf):
for k in range(surface.zs, surface.zf):
if (
i
> surface.fractalsurface[
j - surface.ys, k - surface.zs
]
):
if i > surface.fractalsurface[j - surface.ys, k - surface.zs]:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
k - self.volume.zs,
] = 1
elif (
surface.filldepth > 0 and i > surface.filldepth
):
elif surface.filldepth > 0 and i > surface.filldepth:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
@@ -299,26 +286,19 @@ class FractalBox(UserObjectGeometry):
elif surface.surfaceID == "xplus":
if not surface.ID:
for i in range(
surface.fractalrange[0], surface.fractalrange[1]
):
for i in range(surface.fractalrange[0], surface.fractalrange[1]):
for j in range(surface.ys, surface.yf):
for k in range(surface.zs, surface.zf):
if (
i
< surface.fractalsurface[
j - surface.ys, k - surface.zs
]
< surface.fractalsurface[j - surface.ys, k - surface.zs]
):
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
k - self.volume.zs,
] = 1
elif (
surface.filldepth > 0
and i < surface.filldepth
):
elif surface.filldepth > 0 and i < surface.filldepth:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
@@ -336,16 +316,9 @@ class FractalBox(UserObjectGeometry):
blade = 0
for j in range(surface.ys, surface.yf):
for k in range(surface.zs, surface.zf):
if (
surface.fractalsurface[
j - surface.ys, k - surface.zs
]
> 0
):
if surface.fractalsurface[j - surface.ys, k - surface.zs] > 0:
height = 0
for i in range(
self.volume.xs, surface.fractalrange[1]
):
for i in range(self.volume.xs, surface.fractalrange[1]):
if (
i
< surface.fractalsurface[
@@ -358,9 +331,7 @@ class FractalBox(UserObjectGeometry):
]
!= 1
):
y, z = g.calculate_blade_geometry(
blade, height
)
y, z = g.calculate_blade_geometry(blade, height)
# Add y, z coordinates to existing location
yy = int(j - self.volume.ys + y)
zz = int(k - self.volume.zs + z)
@@ -374,9 +345,7 @@ class FractalBox(UserObjectGeometry):
):
break
else:
self.volume.mask[
i - self.volume.xs, yy, zz
] = 3
self.volume.mask[i - self.volume.xs, yy, zz] = 3
height += 1
blade += 1
@@ -384,12 +353,7 @@ class FractalBox(UserObjectGeometry):
root = 0
for j in range(surface.ys, surface.yf):
for k in range(surface.zs, surface.zf):
if (
surface.fractalsurface[
j - surface.ys, k - surface.zs
]
> 0
):
if surface.fractalsurface[j - surface.ys, k - surface.zs] > 0:
depth = 0
i = self.volume.xf - 1
while i > self.volume.xs:
@@ -409,9 +373,7 @@ class FractalBox(UserObjectGeometry):
]
== 1
):
y, z = g.calculate_root_geometry(
root, depth
)
y, z = g.calculate_root_geometry(root, depth)
# Add y, z coordinates to existing location
yy = int(j - self.volume.ys + y)
zz = int(k - self.volume.zs + z)
@@ -425,33 +387,22 @@ class FractalBox(UserObjectGeometry):
):
break
else:
self.volume.mask[
i - self.volume.xs, yy, zz
] = 3
self.volume.mask[i - self.volume.xs, yy, zz] = 3
depth += 1
i -= 1
root += 1
elif surface.surfaceID == "yminus":
for i in range(surface.xs, surface.xf):
for j in range(
surface.fractalrange[0], surface.fractalrange[1]
):
for j in range(surface.fractalrange[0], surface.fractalrange[1]):
for k in range(surface.zs, surface.zf):
if (
j
> surface.fractalsurface[
i - surface.xs, k - surface.zs
]
):
if j > surface.fractalsurface[i - surface.xs, k - surface.zs]:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
k - self.volume.zs,
] = 1
elif (
surface.filldepth > 0 and j > surface.filldepth
):
elif surface.filldepth > 0 and j > surface.filldepth:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
@@ -467,25 +418,18 @@ class FractalBox(UserObjectGeometry):
elif surface.surfaceID == "yplus":
if not surface.ID:
for i in range(surface.xs, surface.xf):
for j in range(
surface.fractalrange[0], surface.fractalrange[1]
):
for j in range(surface.fractalrange[0], surface.fractalrange[1]):
for k in range(surface.zs, surface.zf):
if (
j
< surface.fractalsurface[
i - surface.xs, k - surface.zs
]
< surface.fractalsurface[i - surface.xs, k - surface.zs]
):
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
k - self.volume.zs,
] = 1
elif (
surface.filldepth > 0
and j < surface.filldepth
):
elif surface.filldepth > 0 and j < surface.filldepth:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
@@ -503,16 +447,9 @@ class FractalBox(UserObjectGeometry):
blade = 0
for i in range(surface.xs, surface.xf):
for k in range(surface.zs, surface.zf):
if (
surface.fractalsurface[
i - surface.xs, k - surface.zs
]
> 0
):
if surface.fractalsurface[i - surface.xs, k - surface.zs] > 0:
height = 0
for j in range(
self.volume.ys, surface.fractalrange[1]
):
for j in range(self.volume.ys, surface.fractalrange[1]):
if (
j
< surface.fractalsurface[
@@ -525,9 +462,7 @@ class FractalBox(UserObjectGeometry):
]
!= 1
):
x, z = g.calculate_blade_geometry(
blade, height
)
x, z = g.calculate_blade_geometry(blade, height)
# Add x, z coordinates to existing location
xx = int(i - self.volume.xs + x)
zz = int(k - self.volume.zs + z)
@@ -541,9 +476,7 @@ class FractalBox(UserObjectGeometry):
):
break
else:
self.volume.mask[
xx, j - self.volume.ys, zz
] = 3
self.volume.mask[xx, j - self.volume.ys, zz] = 3
height += 1
blade += 1
@@ -551,12 +484,7 @@ class FractalBox(UserObjectGeometry):
root = 0
for i in range(surface.xs, surface.xf):
for k in range(surface.zs, surface.zf):
if (
surface.fractalsurface[
i - surface.xs, k - surface.zs
]
> 0
):
if surface.fractalsurface[i - surface.xs, k - surface.zs] > 0:
depth = 0
j = self.volume.yf - 1
while j > self.volume.ys:
@@ -576,9 +504,7 @@ class FractalBox(UserObjectGeometry):
]
== 1
):
x, z = g.calculate_root_geometry(
root, depth
)
x, z = g.calculate_root_geometry(root, depth)
# Add x, z coordinates to existing location
xx = int(i - self.volume.xs + x)
zz = int(k - self.volume.zs + z)
@@ -592,9 +518,7 @@ class FractalBox(UserObjectGeometry):
):
break
else:
self.volume.mask[
xx, j - self.volume.ys, zz
] = 3
self.volume.mask[xx, j - self.volume.ys, zz] = 3
depth += 1
j -= 1
root += 1
@@ -602,23 +526,14 @@ class FractalBox(UserObjectGeometry):
elif surface.surfaceID == "zminus":
for i in range(surface.xs, surface.xf):
for j in range(surface.ys, surface.yf):
for k in range(
surface.fractalrange[0], surface.fractalrange[1]
):
if (
k
> surface.fractalsurface[
i - surface.xs, j - surface.ys
]
):
for k in range(surface.fractalrange[0], surface.fractalrange[1]):
if k > surface.fractalsurface[i - surface.xs, j - surface.ys]:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
k - self.volume.zs,
] = 1
elif (
surface.filldepth > 0 and k > surface.filldepth
):
elif surface.filldepth > 0 and k > surface.filldepth:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
@@ -640,19 +555,14 @@ class FractalBox(UserObjectGeometry):
):
if (
k
< surface.fractalsurface[
i - surface.xs, j - surface.ys
]
< surface.fractalsurface[i - surface.xs, j - surface.ys]
):
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
k - self.volume.zs,
] = 1
elif (
surface.filldepth > 0
and k < surface.filldepth
):
elif surface.filldepth > 0 and k < surface.filldepth:
self.volume.mask[
i - self.volume.xs,
j - self.volume.ys,
@@ -670,16 +580,9 @@ class FractalBox(UserObjectGeometry):
blade = 0
for i in range(surface.xs, surface.xf):
for j in range(surface.ys, surface.yf):
if (
surface.fractalsurface[
i - surface.xs, j - surface.ys
]
> 0
):
if surface.fractalsurface[i - surface.xs, j - surface.ys] > 0:
height = 0
for k in range(
self.volume.zs, surface.fractalrange[1]
):
for k in range(self.volume.zs, surface.fractalrange[1]):
if (
k
< surface.fractalsurface[
@@ -692,9 +595,7 @@ class FractalBox(UserObjectGeometry):
]
!= 1
):
x, y = g.calculate_blade_geometry(
blade, height
)
x, y = g.calculate_blade_geometry(blade, height)
# Add x, y coordinates to existing location
xx = int(i - self.volume.xs + x)
yy = int(j - self.volume.ys + y)
@@ -708,9 +609,7 @@ class FractalBox(UserObjectGeometry):
):
break
else:
self.volume.mask[
xx, yy, k - self.volume.zs
] = 3
self.volume.mask[xx, yy, k - self.volume.zs] = 3
height += 1
blade += 1
@@ -718,12 +617,7 @@ class FractalBox(UserObjectGeometry):
root = 0
for i in range(surface.xs, surface.xf):
for j in range(surface.ys, surface.yf):
if (
surface.fractalsurface[
i - surface.xs, j - surface.ys
]
> 0
):
if surface.fractalsurface[i - surface.xs, j - surface.ys] > 0:
depth = 0
k = self.volume.zf - 1
while k > self.volume.zs:
@@ -743,9 +637,7 @@ class FractalBox(UserObjectGeometry):
]
== 1
):
x, y = g.calculate_root_geometry(
root, depth
)
x, y = g.calculate_root_geometry(root, depth)
# Add x, y coordinates to existing location
xx = int(i - self.volume.xs + x)
yy = int(j - self.volume.ys + y)
@@ -759,20 +651,14 @@ class FractalBox(UserObjectGeometry):
):
break
else:
self.volume.mask[
xx, yy, k - self.volume.zs
] = 3
self.volume.mask[xx, yy, k - self.volume.zs] = 3
depth += 1
k -= 1
root += 1
# Build voxels from any true values of the 3D mask array
waternumID = next(
(x.numID for x in grid.materials if x.ID == "water"), 0
)
grassnumID = next(
(x.numID for x in grid.materials if x.ID == "grass"), 0
)
waternumID = next((x.numID for x in grid.materials if x.ID == "water"), 0)
grassnumID = next((x.numID for x in grid.materials if x.ID == "grass"), 0)
data = self.volume.fractalvolume.astype("int16", order="C")
mask = self.volume.mask.copy(order="C")
build_voxels_from_array_mask(
@@ -804,9 +690,9 @@ class FractalBox(UserObjectGeometry):
for j in range(0, self.volume.ny):
for k in range(0, self.volume.nz):
numberinbin = self.volume.fractalvolume[i, j, k]
self.volume.fractalvolume[i, j, k] = (
self.volume.mixingmodel.matID[int(numberinbin)]
)
self.volume.fractalvolume[i, j, k] = self.volume.mixingmodel.matID[
int(numberinbin)
]
data = self.volume.fractalvolume.astype("int16", order="C")
build_voxels_from_array(

查看文件

@@ -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
@@ -71,11 +72,7 @@ class GeometryObjectsRead(UserObjectGeometry):
materials = [
line.rstrip() + "{" + matstr + "}\n"
for line in f
if (
line.startswith("#")
and not line.startswith("##")
and line.rstrip("\n")
)
if (line.startswith("#") and not line.startswith("##") and line.rstrip("\n"))
]
# Build scene
@@ -86,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:
@@ -131,25 +128,17 @@ class GeometryObjectsRead(UserObjectGeometry):
rigidH = f["/rigidH"][:]
ID = f["/ID"][:]
grid.solid[
xs : xs + data.shape[0],
ys : ys + data.shape[1],
zs : zs + data.shape[2],
] = data + numexistmaterials
xs : xs + data.shape[0], ys : ys + data.shape[1], zs : zs + data.shape[2]
] = (data + numexistmaterials)
grid.rigidE[
:,
xs : xs + rigidE.shape[1],
ys : ys + rigidE.shape[2],
zs : zs + rigidE.shape[3],
:, xs : xs + rigidE.shape[1], ys : ys + rigidE.shape[2], zs : zs + rigidE.shape[3]
] = rigidE
grid.rigidH[
:,
xs : xs + rigidH.shape[1],
ys : ys + rigidH.shape[2],
zs : zs + rigidH.shape[3],
:, xs : xs + rigidH.shape[1], ys : ys + rigidH.shape[2], zs : zs + rigidH.shape[3]
] = rigidH
grid.ID[
:, xs : xs + ID.shape[1], ys : ys + ID.shape[2], zs : zs + ID.shape[3]
] = ID + numexistmaterials
grid.ID[:, xs : xs + ID.shape[1], ys : ys + ID.shape[2], zs : zs + ID.shape[3]] = (
ID + numexistmaterials
)
logger.info(
f"{self.grid_name(grid)}Geometry objects from file {geofile} "
f"inserted at {xs * grid.dx:g}m, {ys * grid.dy:g}m, "

查看文件

@@ -20,13 +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:
@@ -37,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"]
@@ -75,12 +75,19 @@ 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)
p1, p2 = uip.check_box_points(p1, p2, self.__str__())
plate_within_grid, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
# Exit early if none of the plate is in this grid as there is
# nothing else to do.
if not plate_within_grid:
return
xs, ys, zs = p1
xf, yf, zf = p2
@@ -89,17 +96,16 @@ class Plate(UserObjectGeometry):
(xs == xf and (ys == yf or zs == zf))
or (ys == yf and (xs == xf or zs == zf))
or (zs == zf and (xs == xf or ys == yf))
or (xs != xf and ys != yf and zs != zf)
):
logger.exception(f"{self.__str__()} the plate is not specified correctly")
raise ValueError
raise ValueError(f"{self.__str__()} the plate is not specified correctly")
# Look up requested materials in existing list of material instances
materials = [y for x in materialsrequested for y in grid.materials if y.ID == x]
if len(materials) != len(materialsrequested):
notfound = [x for x in materialsrequested if x not in materials]
logger.exception(f"{self.__str__()} material(s) {notfound} do not exist")
raise ValueError
raise ValueError(f"{self.__str__()} material(s) {notfound} do not exist")
# yz-plane plate
if xs == xf:
@@ -114,9 +120,7 @@ class Plate(UserObjectGeometry):
for j in range(ys, yf):
for k in range(zs, zf):
build_face_yz(
xs, j, k, numIDy, numIDz, grid.rigidE, grid.rigidH, grid.ID
)
build_face_yz(xs, j, k, numIDy, numIDz, grid.rigidE, grid.rigidH, grid.ID)
# xz-plane plate
elif ys == yf:
@@ -131,9 +135,7 @@ class Plate(UserObjectGeometry):
for i in range(xs, xf):
for k in range(zs, zf):
build_face_xz(
i, ys, k, numIDx, numIDz, grid.rigidE, grid.rigidH, grid.ID
)
build_face_xz(i, ys, k, numIDx, numIDz, grid.rigidE, grid.rigidH, grid.ID)
# xy-plane plate
elif zs == zf:
@@ -148,9 +150,7 @@ class Plate(UserObjectGeometry):
for i in range(xs, xf):
for j in range(ys, yf):
build_face_xy(
i, j, zs, numIDx, numIDy, grid.rigidE, grid.rigidH, grid.ID
)
build_face_xy(i, j, zs, numIDx, numIDy, grid.rigidE, grid.rigidH, grid.ID)
logger.info(
f"{self.grid_name(grid)}Plate from {p3[0]:g}m, {p3[1]:g}m, "

查看文件

@@ -20,14 +20,16 @@ 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.cmds_geometry.cmds_geometry import check_averaging
from gprMax.user_objects.user_objects import GeometryUserObject
logger = logging.getLogger(__name__)
class Sphere(UserObjectGeometry):
class Sphere(GeometryUserObject):
"""Introduces a spherical object with specific parameters into the model.
Attributes:
@@ -39,11 +41,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"]
@@ -54,8 +59,7 @@ class Sphere(UserObjectGeometry):
# Check averaging
try:
# Try user-specified averaging
averagesphere = self.kwargs["averaging"]
averagesphere = check_averaging(averagesphere)
averagesphere = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averagesphere = grid.averagevolumeobjects
@@ -73,6 +77,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)
@@ -95,7 +100,7 @@ class Sphere(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = materials[0].ID + "+" + materials[1].ID + "+" + materials[2].ID
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -104,18 +109,10 @@ class Sphere(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)

查看文件

@@ -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 check_averaging, 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,13 +74,12 @@ class Triangle(UserObjectGeometry):
raise
if self.do_rotate:
self._do_rotate()
self._do_rotate(grid)
# Check averaging
try:
# Try user-specified averaging
averagetriangularprism = self.kwargs["averaging"]
averagetriangularprism = check_averaging(averagetriangularprism)
averagetriangularprism = check_averaging(self.kwargs["averaging"])
except KeyError:
# Otherwise go with the grid default
averagetriangularprism = grid.averagevolumeobjects
@@ -97,39 +96,52 @@ class Triangle(UserObjectGeometry):
logger.exception(f"{self.__str__()} no materials have been specified")
raise
p4 = uip.round_to_grid_static_point(up1)
p5 = uip.round_to_grid_static_point(up2)
p6 = uip.round_to_grid_static_point(up3)
uip = self._create_uip(grid)
# Check whether points are valid against grid
uip.check_tri_points(up1, up2, up3, object)
dp1, dp2, dp3 = uip.check_tri_points(up1, up2, up3, self.__str__())
# Convert points to metres
x1, y1, z1 = uip.round_to_grid(up1)
x2, y2, z2 = uip.round_to_grid(up2)
x3, y3, z3 = uip.round_to_grid(up3)
if thickness < 0:
logger.exception(
f"{self.__str__()} requires a positive value for thickness"
)
raise ValueError
x1, y1, z1 = uip.discrete_to_continuous(dp1)
x2, y2, z2 = uip.discrete_to_continuous(dp2)
x3, y3, z3 = uip.discrete_to_continuous(dp3)
# Check for valid orientations
# yz-plane triangle
if x1 == x2 == x3:
normal = "x"
lower_extent = up1[0]
# xz-plane triangle
elif y1 == y2 == y3:
normal = "y"
lower_extent = up1[1]
# xy-plane triangle
elif z1 == z2 == z3:
normal = "z"
lower_extent = up1[2]
else:
logger.exception(
f"{self.__str__()} the triangle is not specified correctly"
)
logger.exception(f"{self.__str__()} the triangle is not specified correctly")
raise ValueError
triangle_within_grid, lower_extent, thickness = uip.check_thickness(
normal, lower_extent, thickness, self.__str__()
)
# Exit early if none of the triangle is in this grid as there is
# nothing else to do.
if not triangle_within_grid:
return
# Update start bound of the triangle
# yz-plane triangle
if normal == "x":
x1 = x2 = x3 = lower_extent
# xz-plane triangle
elif normal == "y":
y1 = y2 = y3 = lower_extent
# xy-plane triangle
elif normal == "z":
z1 = z2 = z3 = lower_extent
# Look up requested materials in existing list of material instances
materials = [y for x in materialsrequested for y in grid.materials if y.ID == x]
@@ -150,9 +162,7 @@ class Triangle(UserObjectGeometry):
numIDx = materials[0].numID
numIDy = materials[1].numID
numIDz = materials[2].numID
requiredID = (
materials[0].ID + "+" + materials[1].ID + "+" + materials[2].ID
)
requiredID = Material.create_compound_id(materials[0], materials[1], materials[2])
averagedmaterial = [x for x in grid.materials if x.ID == requiredID]
if averagedmaterial:
numID = averagedmaterial.numID
@@ -161,18 +171,10 @@ class Triangle(UserObjectGeometry):
m = Material(numID, requiredID)
m.type = "dielectric-smoothed"
# Create dielectric-smoothed constituents for material
m.er = np.mean(
(materials[0].er, materials[1].er, materials[2].er), axis=0
)
m.se = np.mean(
(materials[0].se, materials[1].se, materials[2].se), axis=0
)
m.mr = np.mean(
(materials[0].mr, materials[1].mr, materials[2].mr), axis=0
)
m.sm = np.mean(
(materials[0].sm, materials[1].sm, materials[2].sm), axis=0
)
m.er = np.mean((materials[0].er, materials[1].er, materials[2].er), axis=0)
m.se = np.mean((materials[0].se, materials[1].se, materials[2].se), axis=0)
m.mr = np.mean((materials[0].mr, materials[1].mr, materials[2].mr), axis=0)
m.sm = np.mean((materials[0].sm, materials[1].sm, materials[2].sm), axis=0)
# Append the new material object to the materials list
grid.materials.append(m)
@@ -216,6 +218,10 @@ class Triangle(UserObjectGeometry):
grid.ID,
)
p4 = uip.round_to_grid_static_point(up1)
p5 = uip.round_to_grid_static_point(up2)
p6 = uip.round_to_grid_static_point(up3)
if thickness > 0:
dielectricsmoothing = "on" if averaging else "off"
logger.info(

文件差异内容过多而无法显示 加载差异

查看文件

@@ -0,0 +1,409 @@
import logging
from typing import List, Optional, Tuple
import numpy as np
import numpy.typing as npt
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.model import Model
from gprMax.snapshots import Snapshot as SnapshotUser
from gprMax.subgrids.grid import SubGridBaseGrid
from gprMax.user_objects.user_objects import OutputUserObject
from gprMax.utilities.utilities import round_int
logger = logging.getLogger(__name__)
class Snapshot(OutputUserObject):
"""Obtains information about the electromagnetic fields within a volume
of the model at a given time instant.
Attributes:
p1: tuple required to specify lower left (x,y,z) coordinates of volume
of snapshot in metres.
p2: tuple required to specify upper right (x,y,z) coordinates of volume
of snapshot in metres.
dl: tuple require to specify spatial discretisation of the snapshot
in metres.
filename: string required for name of the file to store snapshot.
time/iterations: either a float for time or an int for iterations
must be specified for point in time at which the
snapshot will be taken.
fileext: optional string to indicate type for snapshot file, either
'.vti' (default) or '.h5'
outputs: optional list of outputs for receiver. It can be any
selection from Ex, Ey, Ez, Hx, Hy, or Hz.
"""
@property
def order(self):
return 9
@property
def hash(self):
return "#snapshot"
def __init__(
self,
p1: Tuple[float, float, float],
p2: Tuple[float, float, float],
dl: Tuple[float, float, float],
filename: str,
fileext: Optional[str] = None,
iterations: Optional[int] = None,
time: Optional[float] = None,
outputs: Optional[List[str]] = None,
):
super().__init__(
p1=p1,
p2=p2,
dl=dl,
filename=filename,
fileext=fileext,
iterations=iterations,
time=time,
outputs=outputs,
)
self.lower_bound = p1
self.upper_bound = p2
self.dl = dl
self.filename = filename
self.file_extension = fileext
self.iterations = iterations
self.time = time
self.outputs = outputs
def _calculate_upper_bound(
self, start: npt.NDArray[np.int32], step: npt.NDArray[np.int32], size: npt.NDArray[np.int32]
) -> npt.NDArray[np.int32]:
return start + step * np.ceil(size / step)
def build(self, model: Model, grid: FDTDGrid):
if isinstance(grid, SubGridBaseGrid):
raise ValueError(f"{self.params_str()} do not add snapshots to subgrids.")
uip = self._create_uip(grid)
discretised_lower_bound, discretised_upper_bound = uip.check_output_object_bounds(
self.lower_bound, self.upper_bound, self.params_str()
)
discretised_dl = uip.discretise_static_point(self.dl)
snapshot_size = discretised_upper_bound - discretised_lower_bound
# If p2 does not line up with the set discretisation, the actual
# maximum element accessed in the grid will be this upper bound.
upper_bound = self._calculate_upper_bound(
discretised_lower_bound, discretised_dl, snapshot_size
)
# Each coordinate may need a different method to correct p2.
# Therefore, this check needs to be repeated after each
# correction has been applied.
while any(discretised_upper_bound < upper_bound):
try:
uip.point_within_bounds(
upper_bound, f"[{upper_bound[0]}, {upper_bound[1]}, {upper_bound[2]}]"
)
upper_bound_within_grid = True
except ValueError:
upper_bound_within_grid = False
# Ideally extend p2 up to the correct upper bound. This will
# not change the snapshot output.
if upper_bound_within_grid:
discretised_upper_bound = upper_bound
upper_bound_continuous = discretised_upper_bound * grid.dl
logger.warning(
f"{self.params_str()} upper bound not aligned with discretisation. Updating 'p2'"
f" to {upper_bound_continuous}"
)
# If the snapshot size cannot be increased, the
# discretisation may need reducing. E.g. for snapshots of 2D
# models.
elif any(discretised_dl > snapshot_size):
discretised_dl = np.where(
discretised_dl > snapshot_size, snapshot_size, discretised_dl
)
upper_bound = self._calculate_upper_bound(
discretised_lower_bound, discretised_dl, snapshot_size
)
dl_continuous = discretised_dl * grid.dl
logger.warning(
f"{self.params_str()} current bounds and discretisation would go outside"
f" domain. As discretisation is larger than the snapshot size in at least one"
f" dimension, limiting 'dl' to {dl_continuous}"
)
# Otherwise, limit p2 to the discretisation step below the
# current snapshot size. This will reduce the size of the
# snapshot by 1 in the effected dimension(s), but avoid out
# of memory access.
else:
discretised_upper_bound = np.where(
discretised_upper_bound < upper_bound,
upper_bound - discretised_dl,
discretised_upper_bound,
)
snapshot_size = discretised_upper_bound - discretised_lower_bound
upper_bound = self._calculate_upper_bound(
discretised_lower_bound, discretised_dl, snapshot_size
)
upper_bound_continuous = discretised_upper_bound * grid.dl
logger.warning(
f"{self.params_str()} current bounds and discretisation would go outside"
f" domain. Limiting 'p2' to {upper_bound_continuous}"
)
# Raise error to prevent an infinite loop. This is here
# as a precaution, it shouldn't be needed.
if any(discretised_upper_bound < upper_bound):
raise ValueError(f"{self.params_str()} invalid snapshot.")
if any(discretised_dl < 0):
raise ValueError(f"{self.params_str()} the step size should not be less than zero.")
if any(discretised_dl < 1):
raise ValueError(
f"{self.params_str()} the step size should not be less than the spatial discretisation."
)
if self.iterations is not None and self.time is not None:
logger.warning(
f"{self.params_str()} Time and iterations were both specified, using 'iterations'"
)
# If number of iterations given
if self.iterations is not None:
if self.iterations <= 0 or self.iterations > grid.iterations:
raise ValueError(f"{self.params_str()} time value is not valid.")
# If time value given
elif self.time is not None:
if self.time > 0:
self.iterations = round_int((self.time / grid.dt)) + 1
else:
raise ValueError(f"{self.params_str()} time value must be greater than zero.")
# No iteration or time value given
else:
raise ValueError(f"{self} specify a time or number of iterations")
if self.file_extension is None:
self.file_extension = SnapshotUser.fileexts[0]
elif self.file_extension not in SnapshotUser.fileexts:
raise ValueError(
f"'{self.file_extension}' is not a valid format for a snapshot file."
f" Valid options are: {' '.join(SnapshotUser.fileexts)}."
)
# TODO: Allow VTKHDF files when they are implemented
if isinstance(grid, MPIGrid) and self.file_extension != ".h5":
raise ValueError(
f"{self.params_str()} currently only '.h5' snapshots are compatible with MPI."
)
if self.outputs is None:
outputs = dict.fromkeys(SnapshotUser.allowableoutputs, True)
else:
outputs = dict.fromkeys(SnapshotUser.allowableoutputs, False)
# Check and set output names
for output in self.outputs:
if output not in SnapshotUser.allowableoutputs.keys():
raise ValueError(
f"{self.params_str()} contains an output type that is not"
" allowable. Allowable outputs in current context are "
f"{', '.join(SnapshotUser.allowableoutputs.keys())}."
)
else:
outputs[output] = True
snapshot = model.add_snapshot(
grid,
discretised_lower_bound,
discretised_upper_bound,
discretised_dl,
self.iterations,
self.filename,
self.file_extension,
outputs,
)
if snapshot is not None:
p1 = uip.round_to_grid_static_point(self.lower_bound)
p2 = uip.round_to_grid_static_point(self.upper_bound)
dl = uip.round_to_grid_static_point(self.dl)
logger.info(
f"{self.grid_name(grid)}Snapshot from"
f" {p1[0]:g}m, {p1[1]:g}m, {p1[2]:g}m, to"
f" {p2[0]:g}m, {p2[1]:g}m, {p2[2]:g}m, discretisation"
f" {dl[0]:g}m, {dl[1]:g}m, {dl[2]:g}m, at"
f" {snapshot.time * grid.dt:g} secs with field outputs"
f" {', '.join([k for k, v in outputs.items() if v])} "
f" and filename {snapshot.filename}{snapshot.fileext}"
" will be created."
)
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,
p1: Tuple[float, float, float],
p2: Tuple[float, float, float],
dl: Tuple[float, float, float],
output_type: str,
filename: str,
):
super().__init__(p1=p1, p2=p2, dl=dl, filename=filename, output_type=output_type)
self.lower_bound = p1
self.upper_bound = p2
self.dl = dl
self.filename = filename
self.output_type = output_type
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):
uip = self._create_uip(grid)
discretised_lower_bound, discretised_upper_bound = uip.check_output_object_bounds(
self.lower_bound, self.upper_bound, self.params_str()
)
discretised_dl = uip.discretise_static_point(self.dl)
if any(discretised_dl < 0):
raise ValueError(f"{self.params_str()} the step size should not be less than zero.")
if any(discretised_dl > grid.size):
raise ValueError(
f"{self.params_str()} the step size should be less than the domain size."
)
if any(discretised_dl < 1):
raise ValueError(
f"{self.params_str()} the step size should not be less than the spatial"
" discretisation."
)
if self.output_type == "f" and any(discretised_dl != 1):
raise ValueError(
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)."
)
if self.output_type == "n":
g = model.add_geometry_view_voxels(
grid,
discretised_lower_bound,
discretised_upper_bound,
discretised_dl,
self.filename,
)
elif self.output_type == "f":
g = model.add_geometry_view_lines(
grid,
discretised_lower_bound,
discretised_upper_bound,
self.filename,
)
else:
raise ValueError(
f"{self.params_str()} requires type to be either n (normal) or f (fine)."
)
if g is not None:
p1 = uip.round_to_grid_static_point(self.lower_bound)
p2 = uip.round_to_grid_static_point(self.upper_bound)
dl = discretised_dl * grid.dl
logger.info(
f"{self.grid_name(grid)}Geometry view from"
f" {p1[0]:g}m, {p1[1]:g}m, {p1[2]:g}m,"
f" to {p2[0]:g}m, {p2[1]:g}m, {p2[2]:g}m,"
f" discretisation {dl[0]:g}m, {dl[1]:g}m, {dl[2]:g}m,"
f" with filename base {g.filenamebase} created."
)
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, p1: Tuple[float, float, float], p2: Tuple[float, float, float], filename: str
):
super().__init__(p1=p1, p2=p2, filename=filename)
self.lower_bound = p1
self.upper_bound = p2
self.basefilename = filename
def build(self, model: Model, grid: FDTDGrid):
if isinstance(grid, SubGridBaseGrid):
raise ValueError(f"{self.params_str()} do not add geometry objects to subgrids.")
uip = self._create_uip(grid)
discretised_lower_bound, discretised_upper_bound = uip.check_output_object_bounds(
self.lower_bound, self.upper_bound, self.params_str()
)
g = model.add_geometry_object(
grid, discretised_lower_bound, discretised_upper_bound, self.basefilename
)
if g is not None:
p1 = uip.round_to_grid_static_point(self.lower_bound)
p2 = uip.round_to_grid_static_point(self.upper_bound)
logger.info(
f"Geometry objects in the volume from {p1[0]:g}m,"
f" {p1[1]:g}m, {p1[2]:g}m, to {p2[0]:g}m, {p2[1]:g}m,"
f" {p2[2]:g}m, will be written to {g.filename_hdf5},"
f" with materials written to {g.filename_materials}"
)

查看文件

@@ -0,0 +1,588 @@
# 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)
discretised_domain_size = uip.discretise_static_point(self.domain_size)
model.set_size(discretised_domain_size)
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:
raise ValueError(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 not (
isinstance(self.thickness, int) or len(self.thickness) == 1 or len(self.thickness) == 6
):
raise ValueError(f"{self} requires either one or six parameter(s)")
model.G.set_pml_thickness(self.thickness)
# Check each PML does not take up more than half the grid
# TODO: MPI ranks not containing a PML will not throw an error
# here.
if (
2 * grid.pmls["thickness"]["x0"] >= model.nx
or 2 * grid.pmls["thickness"]["y0"] >= model.ny
or 2 * grid.pmls["thickness"]["z0"] >= model.nz
or 2 * grid.pmls["thickness"]["xmax"] >= model.nx
or 2 * grid.pmls["thickness"]["ymax"] >= model.ny
or 2 * grid.pmls["thickness"]["zmax"] >= model.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 = uip.discretise_static_point(self.step_size)
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 = uip.discretise_static_point(self.step_size)
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,157 @@
from abc import ABC, abstractmethod
from typing import List, Union
from gprMax import config
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.grid.mpi_grid import MPIGrid
from gprMax.model import Model
from gprMax.subgrids.grid import SubGridBaseGrid
from gprMax.user_inputs import MainGridUserInput, MPIUserInput, 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) -> 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)
elif isinstance(grid, MPIGrid):
return MPIUserInput(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

查看文件

@@ -49,9 +49,7 @@ def get_host_info():
try:
manufacturer = (
subprocess.check_output(
["wmic", "csproduct", "get", "vendor"],
shell=False,
stderr=subprocess.STDOUT,
["wmic", "csproduct", "get", "vendor"], shell=False, stderr=subprocess.STDOUT
)
.decode("utf-8")
.strip()
@@ -83,9 +81,7 @@ def get_host_info():
try:
allcpuinfo = (
subprocess.check_output(
["wmic", "cpu", "get", "Name"],
shell=False,
stderr=subprocess.STDOUT,
["wmic", "cpu", "get", "Name"], shell=False, stderr=subprocess.STDOUT
)
.decode("utf-8")
.strip()
@@ -133,9 +129,7 @@ def get_host_info():
try:
sockets = (
subprocess.check_output(
["sysctl", "-n", "hw.packages"],
shell=False,
stderr=subprocess.STDOUT,
["sysctl", "-n", "hw.packages"], shell=False, stderr=subprocess.STDOUT
)
.decode("utf-8")
.strip()
@@ -169,9 +163,7 @@ def get_host_info():
try:
manufacturer = (
subprocess.check_output(
["cat", "/sys/class/dmi/id/sys_vendor"],
shell=False,
stderr=subprocess.STDOUT,
["cat", "/sys/class/dmi/id/sys_vendor"], shell=False, stderr=subprocess.STDOUT
)
.decode("utf-8")
.strip()
@@ -195,10 +187,7 @@ def get_host_info():
myenv = {**os.environ, "LANG": "en_US.utf8"}
cpuIDinfo = (
subprocess.check_output(
["cat", "/proc/cpuinfo"],
shell=False,
stderr=subprocess.STDOUT,
env=myenv,
["cat", "/proc/cpuinfo"], shell=False, stderr=subprocess.STDOUT, env=myenv
)
.decode("utf-8")
.strip()
@@ -208,9 +197,7 @@ def get_host_info():
cpuID = re.sub(".*model name.*:", "", line, 1).strip()
cpuID = " ".join(cpuID.split())
allcpuinfo = (
subprocess.check_output(
["lscpu"], shell=False, stderr=subprocess.STDOUT, env=myenv
)
subprocess.check_output(["lscpu"], shell=False, stderr=subprocess.STDOUT, env=myenv)
.decode("utf-8")
.strip()
)
@@ -268,7 +255,7 @@ def print_host_info(hostinfo):
else ""
)
logger.basic(
f"\n{config.sim_config.hostinfo['hostname']} | "
f"{config.sim_config.hostinfo['hostname']} | "
f"{config.sim_config.hostinfo['machineID']} | "
f"{hostinfo['sockets']} x {hostinfo['cpuID']} "
f"({hostinfo['physicalcores']} cores{hyperthreadingstr}) | "

查看文件

@@ -21,6 +21,10 @@ import logging
import sys
from copy import copy
from colorama import Back, Fore, Style, init
init()
logger = logging.getLogger(__name__)
@@ -40,36 +44,39 @@ logging.Logger.basic = basic
# Colour mapping for different log levels
MAPPING = {
"DEBUG": 37, # white
"BASIC": 37, # white
"INFO": 37, # white
"WARNING": 33, # yellow
"ERROR": 31, # red
"CRITICAL": 41, # white on red bg
"DEBUG": Fore.WHITE,
"INFO": Fore.WHITE,
"BASIC": Fore.WHITE,
"WARNING": Fore.YELLOW,
"ERROR": Fore.RED,
"CRITICAL": Fore.WHITE + Back.RED, # white on red bg
}
PREFIX = "\033["
SUFFIX = "\033[0m"
class CustomFormatter(logging.Formatter):
"""Logging Formatter to add colors and count warning / errors
(https://stackoverflow.com/a/46482050)."""
def __init__(self, pattern):
def __init__(self, pattern: str):
logging.Formatter.__init__(self, pattern)
def format(self, record):
def format(self, record: logging.LogRecord) -> str:
colored_record = copy(record)
levelname = colored_record.levelname
seq = MAPPING.get(levelname, 37) # default white
colored_levelname = f"{PREFIX}{seq}m{levelname}{SUFFIX}"
colour = MAPPING.get(levelname, Fore.BLUE) # default white
colored_levelname = f"{colour}{levelname}{Style.RESET_ALL}"
colored_record.levelname = colored_levelname
colored_record.msg = f"{PREFIX}{seq}m{colored_record.getMessage()}{SUFFIX}"
colored_record.msg = f"{colour}{colored_record.getMessage()}{Style.RESET_ALL}"
return logging.Formatter.format(self, colored_record)
def logging_config(
name="gprMax", level=logging.INFO, format_style="std", log_file=False
name="gprMax",
level=logging.INFO,
format_style="std",
log_file=False,
mpi_logger=False,
log_all_ranks=False,
):
"""Setup and configure logging.
@@ -94,19 +101,33 @@ def logging_config(
logger.setLevel(logging.DEBUG)
logger.propagate = False
if logger.hasHandlers():
logger.handlers.clear()
# Don't add handlers for non-zero ranks unless logging is turned on
# for all ranks
if mpi_logger:
from mpi4py import MPI
rank = MPI.COMM_WORLD.rank
if not log_all_ranks and not rank == 0:
return
# Config for logging to console
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(level)
handler.setFormatter(CustomFormatter(format))
if logger.hasHandlers():
logger.handlers.clear()
if mpi_logger and log_all_ranks and format == format_full:
handler.setFormatter(CustomFormatter(f"[Rank {rank}] {format}"))
else:
handler.setFormatter(CustomFormatter(format))
logger.addHandler(handler)
# Config for logging to file if required
if log_file:
filename = (
name + "-log-" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + ".txt"
)
if mpi_logger and log_all_ranks:
filename = f"{name}-log-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-{MPI.COMM_WORLD.rank}.txt"
else:
filename = name + "-log-" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + ".txt"
handler = logging.FileHandler(filename, mode="w")
formatter = logging.Formatter(format_full)
handler.setLevel(logging.DEBUG)

查看文件

@@ -23,6 +23,7 @@ import re
import textwrap
from shutil import get_terminal_size
from time import perf_counter as timer_fn
from typing import Union
import numpy as np
from colorama import Fore, Style, init
@@ -71,10 +72,7 @@ def logo(version):
"Finite-Difference Time-Domain (FDTD) method"
)
current_year = datetime.datetime.now().year
copyright = (
f"Copyright (C) 2015-{current_year}: The University of "
"Edinburgh, United Kingdom"
)
copyright = f"Copyright (C) 2015-{current_year}: The University of " "Edinburgh, United Kingdom"
authors = "Authors: Craig Warren, Antonis Giannopoulos, and John Hartley"
licenseinfo1 = (
"gprMax is free software: you can redistribute it and/or "
@@ -115,39 +113,57 @@ def logo(version):
+ textwrap.fill(copyright, width=get_terminal_width() - 1, initial_indent=" ")
+ "\n"
)
str += (
textwrap.fill(authors, width=get_terminal_width() - 1, initial_indent=" ")
+ "\n\n"
)
str += textwrap.fill(authors, width=get_terminal_width() - 1, initial_indent=" ") + "\n\n"
str += (
textwrap.fill(
licenseinfo1,
width=get_terminal_width() - 1,
initial_indent=" ",
subsequent_indent=" ",
licenseinfo1, width=get_terminal_width() - 1, initial_indent=" ", subsequent_indent=" "
)
+ "\n"
)
str += (
textwrap.fill(
licenseinfo2,
width=get_terminal_width() - 1,
initial_indent=" ",
subsequent_indent=" ",
licenseinfo2, width=get_terminal_width() - 1, initial_indent=" ", subsequent_indent=" "
)
+ "\n"
)
str += textwrap.fill(
licenseinfo3,
width=get_terminal_width() - 1,
initial_indent=" ",
subsequent_indent=" ",
str += (
textwrap.fill(
licenseinfo3, width=get_terminal_width() - 1, initial_indent=" ", subsequent_indent=" "
)
+ "\n"
)
return str
def round_value(value, decimalplaces=0):
def round_int(value: float) -> int:
"""Round number to nearest integer (half values are rounded down).
Args:
value: Number to round.
Returns:
rounded: Rounded value.
"""
return int(d.Decimal(value).quantize(d.Decimal("1"), rounding=d.ROUND_HALF_DOWN))
def round_float(value: float, decimalplaces: int) -> float:
"""Round down to a specific number of decimal places.
Args:
value: Number to round.
decimalplaces: Number of decimal places of float to represent
rounded value.
Returns:
rounded: Rounded value.
"""
precision = f"1.{'0' * decimalplaces}"
return float(d.Decimal(value).quantize(d.Decimal(precision), rounding=d.ROUND_FLOOR))
def round_value(value: float, decimalplaces: int = 0) -> Union[float, int]:
"""Rounding function.
Args:
@@ -161,16 +177,11 @@ def round_value(value, decimalplaces=0):
# Rounds to nearest integer (half values are rounded downwards)
if decimalplaces == 0:
rounded = int(
d.Decimal(value).quantize(d.Decimal("1"), rounding=d.ROUND_HALF_DOWN)
)
rounded = round_int(value)
# Rounds down to nearest float represented by number of decimal places
else:
precision = f"1.{'0' * decimalplaces}"
rounded = float(
d.Decimal(value).quantize(d.Decimal(precision), rounding=d.ROUND_FLOOR)
)
rounded = round_float(value, decimalplaces)
return rounded

查看文件

@@ -0,0 +1,184 @@
from os import PathLike
from typing import Literal, Optional, Union
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from gprMax.vtkhdf_filehandlers.vtkhdf import VtkHdfFile
class VtkImageData(VtkHdfFile):
"""File handler for creating a VTKHDF Image Data file.
File format information is available here:
https://docs.vtk.org/en/latest/design_documents/VTKFileFormats.html#image-data
"""
DIRECTION_ATTR = "Direction"
ORIGIN_ATTR = "Origin"
SPACING_ATTR = "Spacing"
WHOLE_EXTENT_ATTR = "WholeExtent"
DIMENSIONS = 3
@property
def TYPE(self) -> Literal["ImageData"]:
return "ImageData"
def __init__(
self,
filename: Union[str, PathLike],
shape: npt.NDArray[np.int32],
origin: Optional[npt.NDArray[np.float32]] = None,
spacing: Optional[npt.NDArray[np.float32]] = None,
direction: Optional[npt.NDArray[np.float32]] = None,
comm: Optional[MPI.Comm] = None,
):
"""Create a new VtkImageData file.
If the file already exists, it will be overriden. Required
attributes (Type and Version) will be written to the file.
The file will be opened using the 'mpio' h5py driver if an MPI
communicator is provided.
Args:
filename: Name of the file (can be a file path). The file
extension will be set to '.vtkhdf'.
shape: Shape of the image data to be stored in the file.
This specifies the number of cells. Image data can be
1D, 2D, or 3D.
origin (optional): Origin of the image data. Default
[0, 0, 0].
spacing (optional): Discritisation of the image data.
Default [1, 1, 1].
direction (optional): Array of direction vectors for each
dimension of the image data. Can be a flattened array.
I.e. [[1, 0, 0], [0, 1, 0], [0, 0, 1]] and
[1, 0, 0, 0, 1, 0, 0, 0, 1] are equivalent. Default
[[1, 0, 0], [0, 1, 0], [0, 0, 1]].
comm (optional): MPI communicator containing all ranks that
want to write to the file.
"""
super().__init__(filename, comm)
if len(shape) == 0:
raise ValueError(f"Shape must not be empty.")
if len(shape) > self.DIMENSIONS:
raise ValueError(f"Shape must not have more than {self.DIMENSIONS} dimensions.")
elif len(shape) < self.DIMENSIONS:
shape = np.concatenate((shape, np.ones(self.DIMENSIONS - len(shape), dtype=np.int32)))
self.shape = shape
whole_extent = np.zeros(2 * self.DIMENSIONS, dtype=np.int32)
whole_extent[1::2] = self.shape
self._set_root_attribute(self.WHOLE_EXTENT_ATTR, whole_extent)
if origin is None:
origin = np.zeros(self.DIMENSIONS, dtype=np.float32)
self.set_origin(origin)
if spacing is None:
spacing = np.ones(self.DIMENSIONS, dtype=np.float32)
self.set_spacing(spacing)
if direction is None:
direction = np.diag(np.ones(self.DIMENSIONS, dtype=np.float32))
self.set_direction(direction)
@property
def whole_extent(self) -> npt.NDArray[np.int32]:
return self._get_root_attribute(self.WHOLE_EXTENT_ATTR)
@property
def origin(self) -> npt.NDArray[np.float32]:
return self._get_root_attribute(self.ORIGIN_ATTR)
@property
def spacing(self) -> npt.NDArray[np.float32]:
return self._get_root_attribute(self.SPACING_ATTR)
@property
def direction(self) -> npt.NDArray[np.float32]:
return self._get_root_attribute(self.DIRECTION_ATTR)
def set_origin(self, origin: npt.NDArray[np.float32]):
"""Set the origin coordinate of the image data.
Args:
origin: x, y, z coordinates to set as the origin.
"""
if len(origin) != self.DIMENSIONS:
raise ValueError(f"Origin attribute must have {self.DIMENSIONS} dimensions.")
self._set_root_attribute(self.ORIGIN_ATTR, origin)
def set_spacing(self, spacing: npt.NDArray[np.float32]):
"""Set the discritisation of the image data.
Args:
spacing: Discritisation of the x, y, and z dimensions.
"""
if len(spacing) != self.DIMENSIONS:
raise ValueError(f"Spacing attribute must have {self.DIMENSIONS} dimensions.")
self._set_root_attribute(self.SPACING_ATTR, spacing)
def set_direction(self, direction: npt.NDArray[np.float32]):
"""Set the coordinate system of the image data.
Args:
direction: Array of direction vectors for each dimension of
the image data. Can be a flattened array. I.e.
[[1, 0, 0], [0, 1, 0], [0, 0, 1]] and
[1, 0, 0, 0, 1, 0, 0, 0, 1] are equivalent.
"""
direction = direction.flatten()
if len(direction) != self.DIMENSIONS * self.DIMENSIONS:
raise ValueError(
f"Direction array must contain {self.DIMENSIONS * self.DIMENSIONS} elements."
)
self._set_root_attribute(self.DIRECTION_ATTR, direction)
def add_point_data(
self, name: str, data: npt.NDArray, offset: Optional[npt.NDArray[np.int32]] = None
):
"""Add point data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
Raises:
ValueError: Raised if data has invalid dimensions.
"""
points_shape = self.shape + 1
if offset is None and any(data.shape != points_shape): # type: ignore
raise ValueError(
"If no offset is specified, data.shape must be one larger in each dimension than"
f" this vtkImageData object. {data.shape} != {points_shape}"
)
return super().add_point_data(name, data, points_shape, offset)
def add_cell_data(
self, name: str, data: npt.NDArray, offset: Optional[npt.NDArray[np.int32]] = None
):
"""Add cell data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
Raises:
ValueError: Raised if data has invalid dimensions.
"""
if offset is None and any(data.shape != self.shape): # type: ignore
raise ValueError(
"If no offset is specified, data.shape must match the dimensions of this"
f" VtkImageData object. {data.shape} != {self.shape}"
)
return super().add_cell_data(name, data, self.shape, offset)

查看文件

@@ -0,0 +1,172 @@
import logging
from os import PathLike
from typing import Literal, Optional, Union
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from gprMax.vtkhdf_filehandlers.vtkhdf import VtkCellType, VtkHdfFile
logger = logging.getLogger(__name__)
class VtkUnstructuredGrid(VtkHdfFile):
"""File handler for creating a VTKHDF Unstructured Grid file.
File format information is available here:
https://docs.vtk.org/en/latest/design_documents/VTKFileFormats.html#unstructured-grid
"""
class Dataset(VtkHdfFile.Dataset):
CONNECTIVITY = "Connectivity"
NUMBER_OF_CELLS = "NumberOfCells"
NUMBER_OF_CONNECTIVITY_IDS = "NumberOfConnectivityIds"
NUMBER_OF_POINTS = "NumberOfPoints"
OFFSETS = "Offsets"
POINTS = "Points"
TYPES = "Types"
@property
def TYPE(self) -> Literal["UnstructuredGrid"]:
return "UnstructuredGrid"
def __init__(
self,
filename: Union[str, PathLike],
points: npt.NDArray,
cell_types: npt.NDArray[VtkCellType],
connectivity: npt.NDArray,
cell_offsets: npt.NDArray,
comm: Optional[MPI.Comm] = None,
) -> None:
"""Create a new VtkUnstructuredGrid file.
An unstructured grid has N points and C cells. A cell is defined
as a collection of points which is specified by the connectivity
and cell_offsets arguments along with the list of cell_types.
If the file already exists, it will be overriden. Required
attributes (Type and Version) will be written to the file.
The file will be opened using the 'mpio' h5py driver if an MPI
communicator is provided.
Args:
filename: Name of the file (can be a file path). The file
extension will be set to '.vtkhdf'.
points: Array of point coordinates of shape (N, 3).
cell_types: Array of VTK cell types of shape (C,).
connectivity: Array of point IDs that together with
cell_offsets, defines the points that make up each cell.
Each point ID has a value between 0 and (N-1) inclusive
and corresponds to a point in the points array.
cell_offsets: Array listing where each cell starts and ends
in the connectivity array. It has shape (C + 1,).
comm (optional): MPI communicator containing all ranks that
want to write to the file.
Raises:
Value Error: Raised if argument dimensions are invalid.
"""
super().__init__(filename, comm)
if len(cell_offsets) != len(cell_types) + 1:
raise ValueError(
"cell_offsets should be one longer than cell_types."
" I.e. one longer than the number of cells"
)
is_sorted = lambda a: np.all(a[:-1] <= a[1:])
if not is_sorted(cell_offsets):
raise ValueError("cell_offsets should be sorted in ascending order")
if len(connectivity) < cell_offsets[-1]:
raise ValueError("Connectivity array is shorter than final cell_offsets value")
elif len(connectivity) > cell_offsets[-1]:
logger.warning(
"Connectivity array longer than final cell_offsets value."
" Some connectivity data will be ignored"
)
self._write_root_dataset(self.Dataset.CONNECTIVITY, connectivity)
self._write_root_dataset(self.Dataset.NUMBER_OF_CELLS, len(cell_types))
self._write_root_dataset(self.Dataset.NUMBER_OF_CONNECTIVITY_IDS, len(connectivity))
self._write_root_dataset(self.Dataset.NUMBER_OF_POINTS, len(points))
self._write_root_dataset(self.Dataset.OFFSETS, cell_offsets)
self._write_root_dataset(self.Dataset.POINTS, points, xyz_data_ordering=False)
self._write_root_dataset(self.Dataset.TYPES, cell_types)
@property
def number_of_cells(self) -> int:
number_of_cells = self._get_root_dataset(self.Dataset.NUMBER_OF_CELLS)
return np.sum(number_of_cells, dtype=np.int32)
@property
def number_of_connectivity_ids(self) -> int:
number_of_connectivity_ids = self._get_root_dataset(self.Dataset.NUMBER_OF_CONNECTIVITY_IDS)
return np.sum(number_of_connectivity_ids, dtype=np.int32)
@property
def number_of_points(self) -> int:
number_of_points = self._get_root_dataset(self.Dataset.NUMBER_OF_POINTS)
return np.sum(number_of_points, dtype=np.int32)
def add_point_data(
self, name: str, data: npt.NDArray, offset: Optional[npt.NDArray[np.int32]] = None
):
"""Add point data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
Raises:
ValueError: Raised if data has invalid dimensions.
"""
shape = np.array(data.shape)
number_of_dimensions = len(shape)
if number_of_dimensions < 1 or number_of_dimensions > 2:
raise ValueError(f"Data must have 1 or 2 dimensions, not {number_of_dimensions}")
elif len(data) != self.number_of_points:
raise ValueError(
"Length of data must match the number of points in the vtkUnstructuredGrid."
f" {len(data)} != {self.number_of_points}"
)
elif number_of_dimensions == 2 and shape[1] != 1 and shape[1] != 3:
raise ValueError(f"The second dimension should have shape 1 or 3, not {shape[1]}")
return super().add_point_data(name, data, shape, offset)
def add_cell_data(
self, name: str, data: npt.NDArray, offset: Optional[npt.NDArray[np.int32]] = None
):
"""Add cell data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
Raises:
ValueError: Raised if data has invalid dimensions.
"""
shape = np.array(data.shape)
number_of_dimensions = len(shape)
if number_of_dimensions < 1 or number_of_dimensions > 2:
raise ValueError(f"Data must have 1 or 2 dimensions, not {number_of_dimensions}.")
elif len(data) != self.number_of_cells:
raise ValueError(
"Length of data must match the number of cells in the vtkUnstructuredGrid."
f" {len(data)} != {self.number_of_cells}"
)
elif number_of_dimensions == 2 and shape[1] != 1 and shape[1] != 3:
raise ValueError(f"The second dimension should have shape 1 or 3, not {shape[1]}")
return super().add_cell_data(name, data, shape, offset)

查看文件

@@ -0,0 +1,471 @@
import logging
from abc import abstractmethod
from contextlib import AbstractContextManager
from enum import Enum
from os import PathLike
from pathlib import Path
from types import TracebackType
from typing import Optional, Tuple, Union
import h5py
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
logger = logging.getLogger(__name__)
class VtkHdfFile(AbstractContextManager):
VERSION = [2, 2]
FILE_EXTENSION = ".vtkhdf"
ROOT_GROUP = "VTKHDF"
# TODO: Can this be moved to using an Enum like root datasets?
# Main barrier: Can't subclass an enum with members and any base
# Enum class would need VERSION and TYPE as members.
VERSION_ATTR = "Version"
TYPE_ATTR = "Type"
class Dataset(str, Enum):
pass
@property
@abstractmethod
def TYPE(self) -> str:
pass
def __enter__(self):
return self
def __init__(self, filename: Union[str, PathLike], comm: Optional[MPI.Comm] = None) -> None:
"""Create a new VtkHdfFile.
If the file already exists, it will be overriden. Required
attributes (Type and Version) will be written to the file.
The file will be opened using the 'mpio' h5py driver if an MPI
communicator is provided.
Args:
filename: Name of the file (can be a file path). The file
extension will be set to '.vtkhdf'.
comm (optional): MPI communicator containing all ranks that
want to write to the file.
"""
# Ensure the filename uses the correct extension
self.filename = Path(filename)
if self.filename.suffix != "" and self.filename.suffix != self.FILE_EXTENSION:
logger.warning(
f"Invalid file extension '{self.filename.suffix}' for VTKHDF file. Changing to '{self.FILE_EXTENSION}'."
)
self.filename = self.filename.with_suffix(self.FILE_EXTENSION)
self.comm = comm
# Check if the filehandler should use an MPI driver
if self.comm is None:
self.file_handler = h5py.File(self.filename, "w")
else:
self.file_handler = h5py.File(self.filename, "w", driver="mpio", comm=self.comm)
self.root_group = self.file_handler.create_group(self.ROOT_GROUP)
# Set required Version and Type root attributes
self._set_root_attribute(self.VERSION_ATTR, self.VERSION)
type_as_ascii = self.TYPE.encode("ascii")
self._set_root_attribute(
self.TYPE_ATTR, type_as_ascii, h5py.string_dtype("ascii", len(type_as_ascii))
)
def __exit__(
self,
exc_type: Optional[type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
"""Close the file when the context is exited.
The parameters describe the exception that caused the context to
be exited. If the context was exited without an exception, all
three arguments will be None. Any exception will be
processed normally upon exit from this method.
Returns:
suppress_exception (optional): Returns True if the exception
should be suppressed (i.e. not propagated). Otherwise,
the exception will be processed normally upon exit from
this method.
"""
self.close()
def close(self) -> None:
"""Close the file handler"""
self.file_handler.close()
def _get_root_attribute(self, attribute: str) -> npt.NDArray:
"""Get attribute from the root VTKHDF group if it exists.
Args:
attribute: Name of the attribute.
Returns:
value: Current value of the attribute if it exists.
Raises:
KeyError: Raised if the attribute is not present as a key.
"""
value = self.root_group.attrs[attribute]
if isinstance(value, h5py.Empty):
raise KeyError(f"Attribute '{attribute}' not present in /{self.ROOT_GROUP} group")
return value
def _set_root_attribute(
self, attribute: str, value: npt.ArrayLike, dtype: npt.DTypeLike = None
):
"""Set attribute in the root VTKHDF group.
Args:
attribute: Name of the new attribute.
value: An array to initialize the attribute.
dtype (optional): Data type of the attribute. Overrides
value.dtype if both are given.
"""
self.root_group.attrs.create(attribute, value, dtype=dtype)
def _build_dataset_path(self, *path: str) -> str:
"""Build an HDF5 dataset path attached to the root VTKHDF group.
Args:
*path: Components of the required path.
Returns:
path: Path to the dataset.
"""
return "/".join([self.ROOT_GROUP, *path])
def _get_root_dataset(self, name: "VtkHdfFile.Dataset") -> h5py.Dataset:
"""Get specified dataset from the root group of the VTKHDF file.
Args:
path: Name of the dataset.
Returns:
dataset: Returns specified h5py dataset.
"""
path = self._build_dataset_path(name)
return self._get_dataset(path)
def _get_dataset(self, path: str) -> h5py.Dataset:
"""Get specified dataset.
Args:
path: Absolute path to the dataset.
Returns:
dataset: Returns specified h5py dataset.
Raises:
KeyError: Raised if the dataset does not exist, or the path
points to some other object, e.g. a Group not a Dataset.
"""
cls = self.file_handler.get(path, getclass=True)
if cls == "default":
raise KeyError("Path does not exist")
elif cls != h5py.Dataset:
raise KeyError(f"Dataset not found. Found '{cls}' instead")
dataset = self.file_handler.get(path)
assert isinstance(dataset, h5py.Dataset)
return dataset
def _write_root_dataset(
self,
name: "VtkHdfFile.Dataset",
data: npt.ArrayLike,
shape: Optional[npt.NDArray[np.int32]] = None,
offset: Optional[npt.NDArray[np.int32]] = None,
xyz_data_ordering=True,
):
"""Write specified dataset to the root group of the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to initialize the dataset.
shape (optional): Size of the full dataset being created.
Can be omitted if data provides the full dataset.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
xyz_data_ordering (optional): If True, the data will be
transposed as VTKHDF stores datasets using ZYX ordering.
Default True.
"""
path = self._build_dataset_path(name)
self._write_dataset(
path, data, shape=shape, offset=offset, xyz_data_ordering=xyz_data_ordering
)
def _write_dataset(
self,
path: str,
data: npt.ArrayLike,
shape: Optional[Union[npt.NDArray[np.int32], Tuple[int, ...]]] = None,
offset: Optional[npt.NDArray[np.int32]] = None,
dtype: Optional[npt.DTypeLike] = None,
xyz_data_ordering=True,
):
"""Write specified dataset to the VTKHDF file.
If data has shape (d1, d2, ..., dn), i.e. n dimensions, then, if
specified, shape and offset must be of length n.
Args:
path: Absolute path to the dataset.
data: Data to initialize the dataset.
shape (optional): Size of the full dataset being created.
Can be omitted if data provides the full dataset.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
dtype (optional): Type of the data. If omitted, the type
will be deduced from the provided data.
xyz_data_ordering (optional): If True, the data will be
transposed as VTKHDF stores datasets using ZYX ordering.
Default True.
Raises:
ValueError: Raised if the combination of data.shape, shape,
and offset are invalid.
"""
# If dtype is a string and using parallel I/O, ensure using
# fixed length strings
if isinstance(dtype, np.dtype) and self.comm is not None:
string_info = h5py.check_string_dtype(dtype)
if string_info is not None and string_info.length is None:
logger.warning(
"HDF5 does not support variable length strings with parallel I/O."
" Using fixed length strings instead."
)
dtype = h5py.string_dtype(encoding="ascii", length=0)
if not isinstance(data, np.ndarray):
data = np.array(data, dtype=dtype)
if data.ndim < 1:
data = np.expand_dims(data, axis=-1)
if data.dtype.kind == "U":
if dtype is not None: # Only log warning if user specified a data type
logger.warning(
"NumPy UTF-32 ('U' dtype) is not supported by HDF5."
" Converting to bytes array ('S' dtype)."
)
data = data.astype("S")
# Explicitly define string datatype
# VTKHDF only supports ascii strings (not UTF-8)
if data.dtype.kind == "S":
dtype = h5py.string_dtype(encoding="ascii", length=data.dtype.itemsize)
data = data.astype(dtype)
elif dtype is None:
dtype = data.dtype
# VTKHDF stores datasets using ZYX ordering rather than XYZ
if xyz_data_ordering:
data = data.transpose()
if shape is not None:
shape = np.flip(shape)
if offset is not None:
offset = np.flip(offset)
logger.debug(
f"Writing dataset '{path}', shape: {shape}, data.shape: {data.shape}, dtype: {dtype}"
)
if shape is None or all(shape == data.shape):
shape = data.shape if shape is None else shape
dataset = self.file_handler.create_dataset(path, shape=shape, dtype=dtype)
dataset[:] = data
elif offset is None:
raise ValueError(
"Offset must not be None as the full dataset has not been provided."
" I.e. data.shape != shape"
)
else:
dimensions = len(data.shape)
if dimensions != len(shape):
raise ValueError(
"The data and specified shape must have the same number of dimensions."
f" {dimensions} != {len(shape)}"
)
if dimensions != len(offset):
raise ValueError(
"The data and specified offset must have the same number of dimensions."
f" {dimensions} != {len(offset)}"
)
if any(offset + data.shape > shape):
raise ValueError(
"The provided offset and data does not fit within the bounds of the dataset."
f" {offset} + {data.shape} = {offset + data.shape} > {shape}"
)
dataset = self.file_handler.create_dataset(path, shape=shape, dtype=dtype)
start = offset
stop = offset + data.shape
dataset_slice = tuple([slice(start[i], stop[i]) for i in range(dimensions)])
dataset[dataset_slice] = data
def _create_dataset(
self, path: str, shape: Union[npt.NDArray[np.int32], Tuple[int, ...]], dtype: npt.DTypeLike
):
"""Create dataset in the VTKHDF file without writing any data.
Args:
path: Absolute path to the dataset.
shape: Size of the full dataset being created.
dtype: Type of the data.
Raises:
TypeError: Raised if attempt to use variable length strings
with parallel I/O.
"""
dtype = np.dtype(dtype)
# If dtype is a string and using parallel I/O, ensure using
# fixed length strings
if self.comm is not None:
string_info = h5py.check_string_dtype(dtype)
if string_info is not None and string_info.length is None:
raise TypeError(
"HDF5 does not support variable length strings with parallel I/O."
" Use fixed length strings instead."
)
if dtype.kind == "U":
logger.warning(
"NumPy UTF-32 ('U' dtype) is not supported by HDF5."
" Converting to bytes array ('S' dtype)."
)
# Explicitly define string datatype
# VTKHDF only supports ascii strings (not UTF-8)
if dtype.kind == "U" or dtype.kind == "S":
dtype = h5py.string_dtype(encoding="ascii", length=dtype.itemsize)
logger.debug(f"Creating dataset '{path}', shape: {shape}, dtype: {dtype}")
self.file_handler.create_dataset(path, shape=shape, dtype=dtype)
def add_point_data(
self,
name: str,
data: npt.NDArray,
shape: Optional[Union[npt.NDArray[np.int32], Tuple[int, ...]]] = None,
offset: Optional[npt.NDArray[np.int32]] = None,
):
"""Add point data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved.
shape (optional): Size of the full dataset being created.
Can be omitted if data provides the full dataset.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
"""
dataset_path = self._build_dataset_path("PointData", name)
self._write_dataset(dataset_path, data, shape=shape, offset=offset)
def add_cell_data(
self,
name: str,
data: npt.NDArray,
shape: Optional[Union[npt.NDArray[np.int32], Tuple[int, ...]]] = None,
offset: Optional[npt.NDArray[np.int32]] = None,
):
"""Add cell data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved.
shape (optional): Size of the full dataset being created.
Can be omitted if data provides the full dataset.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
"""
dataset_path = self._build_dataset_path("CellData", name)
self._write_dataset(dataset_path, data, shape=shape, offset=offset)
def add_field_data(
self,
name: str,
data: Optional[npt.ArrayLike],
shape: Optional[Union[npt.NDArray[np.int32], Tuple[int, ...]]] = None,
offset: Optional[npt.NDArray[np.int32]] = None,
dtype: Optional[npt.DTypeLike] = None,
):
"""Add field data to the VTKHDF file.
Args:
name: Name of the dataset.
data: Data to be saved. Can be None if both shape and dtype
are specified. If None, the dataset will be created but
no data written. This can be useful if, for example,
not all ranks are writing the data. As long as all ranks
know the shape and dtype, ranks not writing data can
perform the collective operation of creating the
dataset, but only the rank(s) with the data need to
write data.
shape (optional): Size of the full dataset being created.
Can be omitted if data provides the full dataset.
offset (optional): Offset to store the provided data at. Can
be omitted if data provides the full dataset.
dtype (optional): Type of the data. If omitted, the type
will be deduced from the provided data.
"""
dataset_path = self._build_dataset_path("FieldData", name)
if data is not None:
self._write_dataset(
dataset_path, data, shape=shape, offset=offset, dtype=dtype, xyz_data_ordering=False
)
elif shape is not None and dtype is not None:
self._create_dataset(dataset_path, shape, dtype)
else:
raise ValueError(
"If data is None, shape and dtype must be provided. I.e. they must not be None"
)
class VtkCellType(np.uint8, Enum):
"""VTK cell types as defined here:
https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html#l00019
"""
# Linear cells
EMPTY_CELL = 0
VERTEX = 1
POLY_VERTEX = 2
LINE = 3
POLY_LINE = 4
TRIANGLE = 5
TRIANGLE_STRIP = 6
POLYGON = 7
PIXEL = 8
QUAD = 9
TETRA = 10
VOXEL = 11
HEXAHEDRON = 12
WEDGE = 13
PYRAMID = 14
PENTAGONAL_PRISM = 15
HEXAGONAL_PRISM = 16

查看文件

@@ -104,17 +104,12 @@ class Waveform:
elif self.type == "gaussiandotnorm":
delay = time - self.chi
normalise = np.sqrt(np.exp(1) / (2 * self.zeta))
ampvalue = (
-2 * self.zeta * delay * np.exp(-self.zeta * delay**2) * normalise
)
ampvalue = -2 * self.zeta * delay * np.exp(-self.zeta * delay**2) * normalise
elif self.type in ["gaussiandotdot", "gaussiandoubleprime"]:
delay = time - self.chi
ampvalue = (
2
* self.zeta
* (2 * self.zeta * delay**2 - 1)
* np.exp(-self.zeta * delay**2)
2 * self.zeta * (2 * self.zeta * delay**2 - 1) * np.exp(-self.zeta * delay**2)
)
elif self.type == "gaussiandotdotnorm":
@@ -132,12 +127,7 @@ class Waveform:
delay = time - self.chi
normalise = 1 / (2 * self.zeta)
ampvalue = -(
(
2
* self.zeta
* (2 * self.zeta * delay**2 - 1)
* np.exp(-self.zeta * delay**2)
)
(2 * self.zeta * (2 * self.zeta * delay**2 - 1) * np.exp(-self.zeta * delay**2))
* normalise
)

8
reframe_tests/.gitignore vendored 普通文件
查看文件

@@ -0,0 +1,8 @@
output/
perflogs/
stage/
reframe.log
reframe.out
reframe_perf.out
configuration/user_config.py

查看文件

@@ -0,0 +1,11 @@
cpu_freq,model,num_cpus_per_task,num_nodes,num_tasks,num_tasks_per_node,run_time,simulation_time
2000000,benchmark_model_40,16,1,8,8,147.94,61.11
2000000,benchmark_model_40,16,16,128,8,74.45,16.03
2000000,benchmark_model_40,16,2,16,8,108.6,45.41
2000000,benchmark_model_40,16,4,32,8,92.18,35.0
2000000,benchmark_model_40,16,8,64,8,73.71,16.56
2250000,benchmark_model_40,16,1,8,8,171.95,53.94
2250000,benchmark_model_40,16,16,128,8,58.13,12.04
2250000,benchmark_model_40,16,2,16,8,97.73,38.73
2250000,benchmark_model_40,16,4,32,8,87.61,28.54
2250000,benchmark_model_40,16,8,64,8,68.29,14.47
1 cpu_freq model num_cpus_per_task num_nodes num_tasks num_tasks_per_node run_time simulation_time
2 2000000 benchmark_model_40 16 1 8 8 147.94 61.11
3 2000000 benchmark_model_40 16 16 128 8 74.45 16.03
4 2000000 benchmark_model_40 16 2 16 8 108.6 45.41
5 2000000 benchmark_model_40 16 4 32 8 92.18 35.0
6 2000000 benchmark_model_40 16 8 64 8 73.71 16.56
7 2250000 benchmark_model_40 16 1 8 8 171.95 53.94
8 2250000 benchmark_model_40 16 16 128 8 58.13 12.04
9 2250000 benchmark_model_40 16 2 16 8 97.73 38.73
10 2250000 benchmark_model_40 16 4 32 8 87.61 28.54
11 2250000 benchmark_model_40 16 8 64 8 68.29 14.47

查看文件

@@ -0,0 +1,145 @@
cpu_freq,domain,num_cpus_per_task,num_tasks,num_tasks_per_node,omp_threads,run_time,simulation_time
2000000,0.1,1,1,,1,66.38,56.22
2000000,0.1,2,1,,2,37.98,30.32
2000000,0.1,4,1,,4,22.77,17.18
2000000,0.1,8,1,,8,16.18,10.51
2000000,0.1,16,1,,16,14.27,8.09
2000000,0.1,32,1,,32,17.05,10.1
2000000,0.1,64,1,,64,28.35,19.12
2000000,0.1,128,1,,128,85.27,65.33
2000000,0.15,1,1,,1,182.5,174.22
2000000,0.15,2,1,,2,102.11,92.21
2000000,0.15,4,1,,4,55.17,49.58
2000000,0.15,8,1,,8,37.08,31.39
2000000,0.15,16,1,,16,33.61,25.55
2000000,0.15,32,1,,32,26.1,19.07
2000000,0.15,64,1,,64,33.14,23.48
2000000,0.15,128,1,,128,78.14,59.57
2000000,0.2,1,1,,1,386.67,374.75
2000000,0.2,2,1,,2,206.99,197.88
2000000,0.2,4,1,,4,109.97,104.41
2000000,0.2,8,1,,8,73.19,64.43
2000000,0.2,16,1,,16,62.07,54.08
2000000,0.2,32,1,,32,48.24,40.67
2000000,0.2,64,1,,64,55.74,44.6
2000000,0.2,128,1,,128,101.74,83.55
2000000,0.3,1,1,,1,1151.43,1140.37
2000000,0.3,2,1,,2,611.66,602.01
2000000,0.3,4,1,,4,321.48,310.84
2000000,0.3,8,1,,8,204.9,196.05
2000000,0.3,16,1,,16,174.01,167.72
2000000,0.3,32,1,,32,128.94,116.76
2000000,0.3,64,1,,64,121.17,108.21
2000000,0.3,128,1,,128,198.33,174.66
2000000,0.4,1,1,,1,2610.57,2598.76
2000000,0.4,2,1,,2,1371.05,1359.44
2000000,0.4,4,1,,4,706.84,699.5
2000000,0.4,8,1,,8,466.36,459.21
2000000,0.4,16,1,,16,401.64,393.83
2000000,0.4,32,1,,32,279.96,267.74
2000000,0.4,64,1,,64,271.98,247.58
2000000,0.4,128,1,,128,374.76,314.99
2000000,0.5,1,1,,1,4818.72,4806.97
2000000,0.5,2,1,,2,2549.42,2540.13
2000000,0.5,4,1,,4,1315.68,1306.77
2000000,0.5,8,1,,8,864.02,855.79
2000000,0.5,16,1,,16,755.09,748.3
2000000,0.5,32,1,,32,548.72,527.04
2000000,0.5,64,1,,64,473.02,414.43
2000000,0.5,128,1,,128,594.65,443.44
2000000,0.6,1,1,,1,8219.78,8149.55
2000000,0.6,2,1,,2,4277.96,4266.5
2000000,0.6,4,1,,4,2199.55,2190.22
2000000,0.6,8,1,,8,1445.58,1438.02
2000000,0.6,16,1,,16,1319.07,1312.13
2000000,0.6,32,1,,32,877.47,818.48
2000000,0.6,64,1,,64,741.43,649.31
2000000,0.6,128,1,,128,821.9,554.22
2000000,0.7,1,1,,1,12964.86,12949.06
2000000,0.7,2,1,,2,6769.45,6762.25
2000000,0.7,4,1,,4,3471.68,3465.48
2000000,0.7,8,1,,8,2270.26,2263.86
2000000,0.7,16,1,,16,2040.48,2033.68
2000000,0.7,32,1,,32,1364.91,1274.35
2000000,0.7,64,1,,64,1094.98,936.72
2000000,0.7,128,1,,128,1163.05,775.27
2000000,0.8,1,1,,1,19115.24,18963.06
2000000,0.8,2,1,,2,9894.57,9868.57
2000000,0.8,4,1,,4,5021.3,5011.79
2000000,0.8,8,1,,8,3285.76,3272.44
2000000,0.8,16,1,,16,3010.09,3003.21
2000000,0.8,32,1,,32,1961.83,1789.48
2000000,0.8,64,1,,64,1528.58,1304.87
2000000,0.8,128,1,,128,1671.89,1115.37
2250000,0.1,1,1,,1,46.42,38.46
2250000,0.1,2,1,,2,27.41,19.9
2250000,0.1,4,1,,4,15.61,11.83
2250000,0.1,8,1,,8,13.1,9.04
2250000,0.1,16,1,,16,11.33,5.89
2250000,0.1,32,1,,32,13.28,6.98
2250000,0.1,64,1,,64,22.95,14.36
2250000,0.1,128,1,,128,63.82,47.17
2250000,0.15,1,1,,1,122.83,114.3
2250000,0.15,2,1,,2,67.81,58.22
2250000,0.15,4,1,,4,37.45,33.57
2250000,0.15,8,1,,8,32.12,28.33
2250000,0.15,16,1,,16,28.47,23.02
2250000,0.15,32,1,,32,21.8,15.31
2250000,0.15,64,1,,64,26.29,17.85
2250000,0.15,128,1,,128,67.36,50.01
2250000,0.2,1,1,,1,249.09,240.5
2250000,0.2,2,1,,2,131.92,122.94
2250000,0.2,4,1,,4,73.47,69.44
2250000,0.2,8,1,,8,68.64,59.68
2250000,0.2,16,1,,16,58.96,50.94
2250000,0.2,32,1,,32,42.94,35.78
2250000,0.2,64,1,,64,44.52,36.89
2250000,0.2,128,1,,128,84.06,68.65
2250000,0.3,1,1,,1,713.41,703.34
2250000,0.3,2,1,,2,369.84,363.33
2250000,0.3,4,1,,4,211.28,203.39
2250000,0.3,8,1,,8,190.98,186.1
2250000,0.3,16,1,,16,169.92,163.23
2250000,0.3,32,1,,32,117.74,109.5
2250000,0.3,64,1,,64,116.59,101.76
2250000,0.3,128,1,,128,162.47,134.58
2250000,0.4,1,1,,1,1593.84,1584.41
2250000,0.4,2,1,,2,821.79,813.12
2250000,0.4,4,1,,4,476.39,468.35
2250000,0.4,8,1,,8,445.69,437.75
2250000,0.4,16,1,,16,392.55,385.05
2250000,0.4,32,1,,32,280.65,265.65
2250000,0.4,64,1,,64,249.73,221.02
2250000,0.4,128,1,,128,312.19,240.34
2250000,0.5,1,1,,1,2917.2,2908.0
2250000,0.5,2,1,,2,1501.78,1493.7
2250000,0.5,4,1,,4,868.33,859.61
2250000,0.5,8,1,,8,831.58,827.08
2250000,0.5,16,1,,16,734.53,729.57
2250000,0.5,32,1,,32,520.43,486.83
2250000,0.5,64,1,,64,431.9,373.89
2250000,0.5,128,1,,128,523.72,368.59
2250000,0.6,1,1,,1,4930.04,4918.3
2250000,0.6,2,1,,2,2513.92,2508.71
2250000,0.6,4,1,,4,1437.79,1433.86
2250000,0.6,8,1,,8,1385.16,1380.08
2250000,0.6,16,1,,16,1278.64,1274.32
2250000,0.6,32,1,,32,843.05,800.03
2250000,0.6,64,1,,64,683.45,575.28
2250000,0.6,128,1,,128,736.02,466.24
2250000,0.7,1,1,,1,7778.74,7766.64
2250000,0.7,2,1,,2,3979.22,3973.14
2250000,0.7,4,1,,4,2290.33,2285.86
2250000,0.7,8,1,,8,2193.65,2185.43
2250000,0.7,16,1,,16,1984.77,1980.02
2250000,0.7,32,1,,32,1302.34,1186.39
2250000,0.7,64,1,,64,1011.7,830.39
2250000,0.7,128,1,,128,1077.62,685.29
2250000,0.8,1,1,,1,11319.94,11306.89
2250000,0.8,2,1,,2,5715.39,5709.03
2250000,0.8,4,1,,4,3279.14,3260.67
2250000,0.8,8,1,,8,3194.27,3174.3
2250000,0.8,16,1,,16,2923.77,2918.94
2250000,0.8,32,1,,32,1871.78,1736.6
2250000,0.8,64,1,,64,1424.7,1179.98
2250000,0.8,128,1,,128,1530.45,930.84
1 cpu_freq domain num_cpus_per_task num_tasks num_tasks_per_node omp_threads run_time simulation_time
2 2000000 0.1 1 1 1 66.38 56.22
3 2000000 0.1 2 1 2 37.98 30.32
4 2000000 0.1 4 1 4 22.77 17.18
5 2000000 0.1 8 1 8 16.18 10.51
6 2000000 0.1 16 1 16 14.27 8.09
7 2000000 0.1 32 1 32 17.05 10.1
8 2000000 0.1 64 1 64 28.35 19.12
9 2000000 0.1 128 1 128 85.27 65.33
10 2000000 0.15 1 1 1 182.5 174.22
11 2000000 0.15 2 1 2 102.11 92.21
12 2000000 0.15 4 1 4 55.17 49.58
13 2000000 0.15 8 1 8 37.08 31.39
14 2000000 0.15 16 1 16 33.61 25.55
15 2000000 0.15 32 1 32 26.1 19.07
16 2000000 0.15 64 1 64 33.14 23.48
17 2000000 0.15 128 1 128 78.14 59.57
18 2000000 0.2 1 1 1 386.67 374.75
19 2000000 0.2 2 1 2 206.99 197.88
20 2000000 0.2 4 1 4 109.97 104.41
21 2000000 0.2 8 1 8 73.19 64.43
22 2000000 0.2 16 1 16 62.07 54.08
23 2000000 0.2 32 1 32 48.24 40.67
24 2000000 0.2 64 1 64 55.74 44.6
25 2000000 0.2 128 1 128 101.74 83.55
26 2000000 0.3 1 1 1 1151.43 1140.37
27 2000000 0.3 2 1 2 611.66 602.01
28 2000000 0.3 4 1 4 321.48 310.84
29 2000000 0.3 8 1 8 204.9 196.05
30 2000000 0.3 16 1 16 174.01 167.72
31 2000000 0.3 32 1 32 128.94 116.76
32 2000000 0.3 64 1 64 121.17 108.21
33 2000000 0.3 128 1 128 198.33 174.66
34 2000000 0.4 1 1 1 2610.57 2598.76
35 2000000 0.4 2 1 2 1371.05 1359.44
36 2000000 0.4 4 1 4 706.84 699.5
37 2000000 0.4 8 1 8 466.36 459.21
38 2000000 0.4 16 1 16 401.64 393.83
39 2000000 0.4 32 1 32 279.96 267.74
40 2000000 0.4 64 1 64 271.98 247.58
41 2000000 0.4 128 1 128 374.76 314.99
42 2000000 0.5 1 1 1 4818.72 4806.97
43 2000000 0.5 2 1 2 2549.42 2540.13
44 2000000 0.5 4 1 4 1315.68 1306.77
45 2000000 0.5 8 1 8 864.02 855.79
46 2000000 0.5 16 1 16 755.09 748.3
47 2000000 0.5 32 1 32 548.72 527.04
48 2000000 0.5 64 1 64 473.02 414.43
49 2000000 0.5 128 1 128 594.65 443.44
50 2000000 0.6 1 1 1 8219.78 8149.55
51 2000000 0.6 2 1 2 4277.96 4266.5
52 2000000 0.6 4 1 4 2199.55 2190.22
53 2000000 0.6 8 1 8 1445.58 1438.02
54 2000000 0.6 16 1 16 1319.07 1312.13
55 2000000 0.6 32 1 32 877.47 818.48
56 2000000 0.6 64 1 64 741.43 649.31
57 2000000 0.6 128 1 128 821.9 554.22
58 2000000 0.7 1 1 1 12964.86 12949.06
59 2000000 0.7 2 1 2 6769.45 6762.25
60 2000000 0.7 4 1 4 3471.68 3465.48
61 2000000 0.7 8 1 8 2270.26 2263.86
62 2000000 0.7 16 1 16 2040.48 2033.68
63 2000000 0.7 32 1 32 1364.91 1274.35
64 2000000 0.7 64 1 64 1094.98 936.72
65 2000000 0.7 128 1 128 1163.05 775.27
66 2000000 0.8 1 1 1 19115.24 18963.06
67 2000000 0.8 2 1 2 9894.57 9868.57
68 2000000 0.8 4 1 4 5021.3 5011.79
69 2000000 0.8 8 1 8 3285.76 3272.44
70 2000000 0.8 16 1 16 3010.09 3003.21
71 2000000 0.8 32 1 32 1961.83 1789.48
72 2000000 0.8 64 1 64 1528.58 1304.87
73 2000000 0.8 128 1 128 1671.89 1115.37
74 2250000 0.1 1 1 1 46.42 38.46
75 2250000 0.1 2 1 2 27.41 19.9
76 2250000 0.1 4 1 4 15.61 11.83
77 2250000 0.1 8 1 8 13.1 9.04
78 2250000 0.1 16 1 16 11.33 5.89
79 2250000 0.1 32 1 32 13.28 6.98
80 2250000 0.1 64 1 64 22.95 14.36
81 2250000 0.1 128 1 128 63.82 47.17
82 2250000 0.15 1 1 1 122.83 114.3
83 2250000 0.15 2 1 2 67.81 58.22
84 2250000 0.15 4 1 4 37.45 33.57
85 2250000 0.15 8 1 8 32.12 28.33
86 2250000 0.15 16 1 16 28.47 23.02
87 2250000 0.15 32 1 32 21.8 15.31
88 2250000 0.15 64 1 64 26.29 17.85
89 2250000 0.15 128 1 128 67.36 50.01
90 2250000 0.2 1 1 1 249.09 240.5
91 2250000 0.2 2 1 2 131.92 122.94
92 2250000 0.2 4 1 4 73.47 69.44
93 2250000 0.2 8 1 8 68.64 59.68
94 2250000 0.2 16 1 16 58.96 50.94
95 2250000 0.2 32 1 32 42.94 35.78
96 2250000 0.2 64 1 64 44.52 36.89
97 2250000 0.2 128 1 128 84.06 68.65
98 2250000 0.3 1 1 1 713.41 703.34
99 2250000 0.3 2 1 2 369.84 363.33
100 2250000 0.3 4 1 4 211.28 203.39
101 2250000 0.3 8 1 8 190.98 186.1
102 2250000 0.3 16 1 16 169.92 163.23
103 2250000 0.3 32 1 32 117.74 109.5
104 2250000 0.3 64 1 64 116.59 101.76
105 2250000 0.3 128 1 128 162.47 134.58
106 2250000 0.4 1 1 1 1593.84 1584.41
107 2250000 0.4 2 1 2 821.79 813.12
108 2250000 0.4 4 1 4 476.39 468.35
109 2250000 0.4 8 1 8 445.69 437.75
110 2250000 0.4 16 1 16 392.55 385.05
111 2250000 0.4 32 1 32 280.65 265.65
112 2250000 0.4 64 1 64 249.73 221.02
113 2250000 0.4 128 1 128 312.19 240.34
114 2250000 0.5 1 1 1 2917.2 2908.0
115 2250000 0.5 2 1 2 1501.78 1493.7
116 2250000 0.5 4 1 4 868.33 859.61
117 2250000 0.5 8 1 8 831.58 827.08
118 2250000 0.5 16 1 16 734.53 729.57
119 2250000 0.5 32 1 32 520.43 486.83
120 2250000 0.5 64 1 64 431.9 373.89
121 2250000 0.5 128 1 128 523.72 368.59
122 2250000 0.6 1 1 1 4930.04 4918.3
123 2250000 0.6 2 1 2 2513.92 2508.71
124 2250000 0.6 4 1 4 1437.79 1433.86
125 2250000 0.6 8 1 8 1385.16 1380.08
126 2250000 0.6 16 1 16 1278.64 1274.32
127 2250000 0.6 32 1 32 843.05 800.03
128 2250000 0.6 64 1 64 683.45 575.28
129 2250000 0.6 128 1 128 736.02 466.24
130 2250000 0.7 1 1 1 7778.74 7766.64
131 2250000 0.7 2 1 2 3979.22 3973.14
132 2250000 0.7 4 1 4 2290.33 2285.86
133 2250000 0.7 8 1 8 2193.65 2185.43
134 2250000 0.7 16 1 16 1984.77 1980.02
135 2250000 0.7 32 1 32 1302.34 1186.39
136 2250000 0.7 64 1 64 1011.7 830.39
137 2250000 0.7 128 1 128 1077.62 685.29
138 2250000 0.8 1 1 1 11319.94 11306.89
139 2250000 0.8 2 1 2 5715.39 5709.03
140 2250000 0.8 4 1 4 3279.14 3260.67
141 2250000 0.8 8 1 8 3194.27 3174.3
142 2250000 0.8 16 1 16 2923.77 2918.94
143 2250000 0.8 32 1 32 1871.78 1736.6
144 2250000 0.8 64 1 64 1424.7 1179.98
145 2250000 0.8 128 1 128 1530.45 930.84

查看文件

@@ -0,0 +1,17 @@
cpu_freq,model,mpi_tasks,num_cpus_per_task,num_tasks,num_tasks_per_node,run_time,simulation_time
2000000,benchmark_model_40,1,128,1,1,397.64,294.58
2000000,benchmark_model_40,128,1,128,128,129.22,68.77
2000000,benchmark_model_40,16,8,16,16,104.83,64.91
2000000,benchmark_model_40,2,64,2,2,192.89,151.06
2000000,benchmark_model_40,32,4,32,32,101.99,63.86
2000000,benchmark_model_40,4,32,4,4,119.14,80.95
2000000,benchmark_model_40,64,2,64,64,105.57,61.03
2000000,benchmark_model_40,8,16,8,8,102.26,58.24
2250000,benchmark_model_40,1,128,1,1,348.95,241.2
2250000,benchmark_model_40,128,1,128,128,118.04,66.21
2250000,benchmark_model_40,16,8,16,16,106.06,61.8
2250000,benchmark_model_40,2,64,2,2,189.82,140.84
2250000,benchmark_model_40,32,4,32,32,99.12,60.65
2250000,benchmark_model_40,4,32,4,4,117.36,76.9
2250000,benchmark_model_40,64,2,64,64,108.8,58.79
2250000,benchmark_model_40,8,16,8,8,94.92,55.32
1 cpu_freq model mpi_tasks num_cpus_per_task num_tasks num_tasks_per_node run_time simulation_time
2 2000000 benchmark_model_40 1 128 1 1 397.64 294.58
3 2000000 benchmark_model_40 128 1 128 128 129.22 68.77
4 2000000 benchmark_model_40 16 8 16 16 104.83 64.91
5 2000000 benchmark_model_40 2 64 2 2 192.89 151.06
6 2000000 benchmark_model_40 32 4 32 32 101.99 63.86
7 2000000 benchmark_model_40 4 32 4 4 119.14 80.95
8 2000000 benchmark_model_40 64 2 64 64 105.57 61.03
9 2000000 benchmark_model_40 8 16 8 8 102.26 58.24
10 2250000 benchmark_model_40 1 128 1 1 348.95 241.2
11 2250000 benchmark_model_40 128 1 128 128 118.04 66.21
12 2250000 benchmark_model_40 16 8 16 16 106.06 61.8
13 2250000 benchmark_model_40 2 64 2 2 189.82 140.84
14 2250000 benchmark_model_40 32 4 32 32 99.12 60.65
15 2250000 benchmark_model_40 4 32 4 4 117.36 76.9
16 2250000 benchmark_model_40 64 2 64 64 108.8 58.79
17 2250000 benchmark_model_40 8 16 8 8 94.92 55.32

查看文件

@@ -0,0 +1,169 @@
import os
from pathlib import Path
import numpy as np
from primePy import primes
from reframe import simple_test
from reframe.core.builtins import parameter, run_after
from reframe_tests.tests.base_tests import GprMaxMPIRegressionTest, GprMaxRegressionTest
"""ReFrame tests for performance benchmarking
Usage:
cd gprMax/reframe_tests
reframe -C configuraiton/{CONFIG_FILE} -c reframe_benchmarks.py -c base_tests.py -r
"""
def calculate_mpi_decomposition(number: int):
factors: list[int] = primes.factors(number)
if len(factors) < 3:
factors += [1] * (3 - len(factors))
elif len(factors) > 3:
base = factors[-3:]
factors = factors[:-3]
for factor in reversed(factors): # Use the largest factors first
min_index = np.argmin(base)
base[min_index] *= factor
factors = base
return sorted(factors)
@simple_test
class SingleNodeBenchmark(GprMaxRegressionTest):
tags = {"benchmark", "single node", "openmp"}
omp_threads = parameter([1, 2, 4, 8, 16, 32, 64, 128])
# domain = parameter([0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8])
cpu_freq = parameter([2000000, 2250000])
time_limit = "8h"
sourcesdir = "src"
model = parameter(
[
"benchmark_model_10",
"benchmark_model_15",
"benchmark_model_20",
"benchmark_model_30",
"benchmark_model_40",
"benchmark_model_50",
"benchmark_model_60",
"benchmark_model_70",
"benchmark_model_80",
]
)
@run_after("init")
def setup_env_vars(self):
self.num_cpus_per_task = self.omp_threads
self.env_vars["SLURM_CPU_FREQ_REQ"] = self.cpu_freq
super().setup_env_vars()
@simple_test
class SingleNodeMPIBenchmark(GprMaxRegressionTest):
tags = {"benchmark", "mpi", "openmp", "single node"}
mpi_tasks = parameter([1, 2, 4, 8, 16, 32, 64, 128, 256])
# domain = parameter([0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8])
cpu_freq = parameter([2000000, 2250000])
model = parameter(["benchmark_model_40"])
sourcesdir = "src"
time_limit = "1h"
@run_after("setup")
def setup_env_vars(self):
cpus_per_node = self.current_partition.processor.num_cpus
self.skip_if(
cpus_per_node < self.mpi_tasks,
f"Insufficient CPUs per node ({cpus_per_node}) to run test with at least {self.mpi_tasks} processors",
)
self.num_cpus_per_task = cpus_per_node // self.mpi_tasks
self.num_tasks = cpus_per_node // self.num_cpus_per_task
self.num_tasks_per_node = self.num_tasks
self.extra_executable_opts = [
"--mpi",
*map(str, calculate_mpi_decomposition(self.num_tasks)),
]
self.env_vars["SLURM_CPU_FREQ_REQ"] = self.cpu_freq
super().setup_env_vars()
@simple_test
class MPIStrongScalingBenchmark(GprMaxRegressionTest):
tags = {"benchmark", "mpi", "openmp"}
num_nodes = parameter([1, 2, 4, 8, 16])
# domain = parameter([0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8])
cpu_freq = parameter([2000000, 2250000])
time_limit = "8h"
sourcesdir = "src"
model = parameter(["benchmark_model_40"])
# serial_dependency = SingleNodeBenchmark
# mpi_layout = parameter([[1, 1, 1]]) # parameter([[2, 2, 2], [4, 4, 4], [6, 6, 6]])
def build_reference_filepath(self, suffix: str = "") -> str:
filename = (
f"MPIWeakScalingBenchmark_{suffix}" if len(suffix) > 0 else "MPIWeakScalingBenchmark"
)
reference_file = Path("regression_checks", filename).with_suffix(".h5")
return os.path.abspath(reference_file)
@run_after("setup")
def setup_env_vars(self):
cpus_per_node = self.current_partition.processor.num_cpus
self.num_cpus_per_task = 16
self.num_tasks_per_node = cpus_per_node // self.num_cpus_per_task
self.num_tasks = self.num_tasks_per_node * self.num_nodes
self.extra_executable_opts = [
"--mpi",
*map(str, calculate_mpi_decomposition(self.num_tasks)),
]
self.env_vars["SLURM_CPU_FREQ_REQ"] = self.cpu_freq
super().setup_env_vars()
@simple_test
class MPIWeakScalingBenchmark(GprMaxRegressionTest):
tags = {"benchmark", "mpi", "openmp"}
num_nodes = parameter([1, 2, 4, 8, 16])
# domain = parameter([0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8])
cpu_freq = parameter([2000000, 2250000])
time_limit = "8h"
sourcesdir = "src"
model = parameter(["benchmark_model_40"])
def build_reference_filepath(self, suffix: str = "") -> str:
filename = (
f"MPIStrongScalingBenchmark_{suffix}_{self.num_nodes}"
if len(suffix) > 0
else f"MPIStrongScalingBenchmark_{self.num_nodes}"
)
reference_file = Path("regression_checks", filename).with_suffix(".h5")
return os.path.abspath(reference_file)
@run_after("setup")
def setup_env_vars(self):
cpus_per_node = self.current_partition.processor.num_cpus
self.num_cpus_per_task = 16
self.num_tasks_per_node = cpus_per_node // self.num_cpus_per_task
self.num_tasks = self.num_tasks_per_node * self.num_nodes
size = 0.4
scale_factor = calculate_mpi_decomposition(self.num_nodes)
self.prerun_cmds.append(
f'sed -i "s/#domain: 0.4 0.4 0.4/#domain: {size * scale_factor[0]} {size * scale_factor[1]} {size * scale_factor[2]}/g" {self.model}.in'
)
self.extra_executable_opts = [
"--mpi",
*map(str, calculate_mpi_decomposition(self.num_tasks)),
]
self.env_vars["SLURM_CPU_FREQ_REQ"] = self.cpu_freq
super().setup_env_vars()

查看文件

@@ -0,0 +1,7 @@
#title: Benchmark model
#domain: 0.1 0.1 0.1
#dx_dy_dz: 0.001 0.001 0.001
#time_window: 3e-9
#waveform: gaussiandotnorm 1 900e6 myWave
#hertzian_dipole: x 0.05 0.05 0.05 myWave

查看文件

@@ -0,0 +1,7 @@
#title: Benchmark model
#domain: 0.15 0.15 0.15
#dx_dy_dz: 0.001 0.001 0.001
#time_window: 3e-9
#waveform: gaussiandotnorm 1 900e6 myWave
#hertzian_dipole: x 0.075 0.075 0.075 myWave

查看文件

@@ -0,0 +1,7 @@
#title: Benchmark model
#domain: 0.2 0.2 0.2
#dx_dy_dz: 0.001 0.001 0.001
#time_window: 3e-9
#waveform: gaussiandotnorm 1 900e6 myWave
#hertzian_dipole: x 0.1 0.1 0.1 myWave

查看文件

@@ -0,0 +1,7 @@
#title: Benchmark model
#domain: 0.3 0.3 0.3
#dx_dy_dz: 0.001 0.001 0.001
#time_window: 3e-9
#waveform: gaussiandotnorm 1 900e6 myWave
#hertzian_dipole: x 0.15 0.15 0.15 myWave

查看文件

@@ -0,0 +1,7 @@
#title: Benchmark model
#domain: 0.4 0.4 0.4
#dx_dy_dz: 0.001 0.001 0.001
#time_window: 3e-9
#waveform: gaussiandotnorm 1 900e6 myWave
#hertzian_dipole: x 0.2 0.2 0.2 myWave

查看文件

@@ -0,0 +1,7 @@
#title: Benchmark model
#domain: 0.5 0.5 0.5
#dx_dy_dz: 0.001 0.001 0.001
#time_window: 3e-9
#waveform: gaussiandotnorm 1 900e6 myWave
#hertzian_dipole: x 0.25 0.25 0.25 myWave

某些文件未显示,因为此 diff 中更改的文件太多 显示更多