Merge branch '53-create-mpi-fractal-objects' into mpi

这个提交包含在:
Nathan Mannall
2025-05-22 15:58:25 +01:00
当前提交 6a754ca82f
共有 25 个文件被更改,包括 1556 次插入641 次删除

查看文件

@@ -26,6 +26,10 @@ from gprMax.config cimport float_or_double_complex
cpdef void generate_fractal2D(
int nx,
int ny,
int ox,
int oy,
int gx,
int gy,
int nthreads,
float D,
np.float64_t[:] weighting,
@@ -50,12 +54,16 @@ cpdef void generate_fractal2D(
cdef Py_ssize_t i, j
cdef double v2x, v2y, rr, B
cdef int sx, sy
sx = gx // 2
sy = gy // 2
for i in prange(nx, nogil=True, schedule='static', num_threads=nthreads):
for j in range(ny):
# Positional vector for current position
v2x = weighting[0] * i
v2y = weighting[1] * j
v2x = weighting[0] * ((i + ox + sx) % gx)
v2y = weighting[1] * ((j + oy + sy) % gy)
# Calulate norm of v2 - v1
rr = ((v2x - v1[0])**2 + (v2y - v1[1])**2)**(1/2)
@@ -70,6 +78,12 @@ cpdef void generate_fractal3D(
int nx,
int ny,
int nz,
int ox,
int oy,
int oz,
int gx,
int gy,
int gz,
int nthreads,
float D,
np.float64_t[:] weighting,
@@ -94,14 +108,19 @@ cpdef void generate_fractal3D(
cdef Py_ssize_t i, j, k
cdef double v2x, v2y, v2z, rr, B
cdef int sx, sy, sz
sx = gx // 2
sy = gy // 2
sz = gz // 2
for i in prange(nx, nogil=True, schedule='static', num_threads=nthreads):
for j in range(ny):
for k in range(nz):
# Positional vector for current position
v2x = weighting[0] * i
v2y = weighting[1] * j
v2z = weighting[2] * k
v2x = weighting[0] * ((i + ox + sx) % gx)
v2y = weighting[1] * ((j + oy + sy) % gy)
v2z = weighting[2] * ((k + oz + sz) % gz)
# Calulate norm of v2 - v1
rr = ((v2x - v1[0])**2 + (v2y - v1[1])**2 + (v2z - v1[2])**2)**(1/2)

查看文件

@@ -1,340 +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 numpy as np
from scipy import fftpack
import gprMax.config as config
from .cython.fractals_generate import generate_fractal2D, generate_fractal3D
from .utilities.utilities import round_value
np.seterr(divide="raise")
class FractalSurface:
"""Fractal surfaces."""
surfaceIDs = ["xminus", "xplus", "yminus", "yplus", "zminus", "zplus"]
def __init__(self, xs, xf, ys, yf, zs, zf, dimension, seed):
"""
Args:
xs, xf, ys, yf, zs, zf: floats for the extent of the fractal surface
(one pair of coordinates must be equal
to correctly define a surface).
dimension: float for the fractal dimension that controls the fractal
distribution.
seed: int for seed value for random number generator.
"""
self.ID = None
self.surfaceID = None
self.xs = xs
self.xf = xf
self.ys = ys
self.yf = yf
self.zs = zs
self.zf = zf
self.nx = xf - xs
self.ny = yf - ys
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.weighting = np.array([1, 1], dtype=np.float64)
self.fractalrange = (0, 0)
self.filldepth = 0
self.grass = []
def get_surface_dims(self):
"""Gets the dimensions of the fractal surface based on surface plane."""
if self.xs == self.xf:
surfacedims = (self.ny, self.nz)
elif self.ys == self.yf:
surfacedims = (self.nx, self.nz)
elif self.zs == self.zf:
surfacedims = (self.nx, self.ny)
return surfacedims
def generate_fractal_surface(self):
"""Generate a 2D array with a fractal distribution."""
surfacedims = self.get_surface_dims()
self.fractalsurface = np.zeros(surfacedims, dtype=self.dtype)
# Positional vector at centre of array, scaled by weighting
v1 = np.array(
[
self.weighting[0] * (surfacedims[0]) / 2,
self.weighting[1] * (surfacedims[1]) / 2,
]
)
# 2D array of random numbers to be convolved with the fractal function
rng = np.random.default_rng(seed=self.seed)
A = rng.standard_normal(size=(surfacedims[0], surfacedims[1]))
# 2D FFT
A = fftpack.fftn(A)
# Shift the zero frequency component to the centre of the array
A = fftpack.fftshift(A)
# Generate fractal
generate_fractal2D(
surfacedims[0],
surfacedims[1],
config.get_model_config().ompthreads,
self.dimension,
self.weighting,
v1,
A,
self.fractalsurface,
)
# Shift the zero frequency component to start of the array
self.fractalsurface = fftpack.ifftshift(self.fractalsurface)
# Set DC component of FFT to zero
self.fractalsurface[0, 0] = 0
# Take the real part (numerical errors can give rise to an imaginary part)
# of the IFFT, and convert type to floattype. N.B calculation of fractals
# must always be carried out at double precision, i.e. float64, complex128
self.fractalsurface = np.real(fftpack.ifftn(self.fractalsurface)).astype(
config.sim_config.dtypes["float_or_double"], copy=False
)
# Scale the fractal volume according to requested range
fractalmin = np.amin(self.fractalsurface)
fractalmax = np.amax(self.fractalsurface)
fractalrange = fractalmax - fractalmin
self.fractalsurface = (
self.fractalsurface * ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange)
+ self.fractalrange[0]
- ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange) * fractalmin
)
class FractalVolume:
"""Fractal volumes."""
def __init__(self, xs, xf, ys, yf, zs, zf, dimension, seed):
"""
Args:
xs, xf, ys, yf, zs, zf: floats for the extent of the fractal volume.
dimension: float for the fractal dimension that controls the fractal
distribution.
seed: int for seed value for random number generator.
"""
self.ID = None
self.operatingonID = None
self.xs = xs
self.xf = xf
self.ys = ys
self.yf = yf
self.zs = zs
self.zf = zf
self.nx = xf - xs
self.ny = yf - ys
self.nz = zf - zs
self.originalxs = xs
self.originalxf = xf
self.originalys = ys
self.originalyf = yf
self.originalzs = zs
self.originalzf = zf
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.weighting = np.array([1, 1, 1], dtype=np.float64)
self.nbins = 0
self.fractalsurfaces = []
def generate_fractal_volume(self):
"""Generate a 3D volume with a fractal distribution."""
# 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.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.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.insert(filterscaling, 2, 1)
else:
filterscaling = np.amin(np.array([self.nx, self.ny, self.nz])) / np.array(
[self.nx, self.ny, self.nz]
)
# Adjust weighting to account for filter scaling
self.weighting = np.multiply(self.weighting, filterscaling)
self.fractalvolume = np.zeros((self.nx, self.ny, self.nz), dtype=self.dtype)
# Positional vector at centre of array, scaled by weighting
v1 = np.array(
[
self.weighting[0] * self.nx / 2,
self.weighting[1] * self.ny / 2,
self.weighting[2] * self.nz / 2,
]
)
# 3D array of random numbers to be convolved with the fractal function
rng = np.random.default_rng(seed=self.seed)
A = rng.standard_normal(size=(self.nx, self.ny, self.nz))
# 3D FFT
A = fftpack.fftn(A)
# Shift the zero frequency component to the centre of the array
A = fftpack.fftshift(A)
# Generate fractal
generate_fractal3D(
self.nx,
self.ny,
self.nz,
config.get_model_config().ompthreads,
self.dimension,
self.weighting,
v1,
A,
self.fractalvolume,
)
# Shift the zero frequency component to the start of the array
self.fractalvolume = fftpack.ifftshift(self.fractalvolume)
# Set DC component of FFT to zero
self.fractalvolume[0, 0, 0] = 0
# Take the real part (numerical errors can give rise to an imaginary part)
# of the IFFT, and convert type to floattype. N.B calculation of fractals
# must always be carried out at double precision, i.e. float64, complex128
self.fractalvolume = np.real(fftpack.ifftn(self.fractalvolume)).astype(
config.sim_config.dtypes["float_or_double"], copy=False
)
# Bin fractal values
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(
self.fractalvolume[:, j, k], bins, right=True
)
def generate_volume_mask(self):
"""Generate a 3D volume to use as a mask for adding rough surfaces,
water and grass/roots. Zero signifies the mask is not set, one
signifies the mask is set.
"""
self.mask = np.zeros((self.nx, self.ny, self.nz), dtype=np.int8)
maskxs = self.originalxs - self.xs
maskxf = (self.originalxf - self.originalxs) + maskxs
maskys = self.originalys - self.ys
maskyf = (self.originalyf - self.originalys) + maskys
maskzs = self.originalzs - self.zs
maskzf = (self.originalzf - self.originalzs) + maskzs
self.mask[maskxs:maskxf, maskys:maskyf, maskzs:maskzf] = 1
class Grass:
"""Geometry information for blades of grass."""
def __init__(self, numblades, seed):
"""
Args:
numblades: int for the number of blades of grass.
seed: int for seed value for random number generator.
"""
self.numblades = numblades
self.geometryparams = np.zeros(
(self.numblades, 6), dtype=config.sim_config.dtypes["float_or_double"]
)
self.seed = seed
self.set_geometry_parameters()
def set_geometry_parameters(self):
"""Sets randomly defined parameters that will be used to calculate
blade and root geometries.
"""
self.R1 = np.random.default_rng(seed=self.seed)
self.R2 = np.random.default_rng(seed=self.seed)
self.R3 = np.random.default_rng(seed=self.seed)
self.R4 = np.random.default_rng(seed=self.seed)
self.R5 = np.random.default_rng(seed=self.seed)
self.R6 = np.random.default_rng(seed=self.seed)
for i in range(self.numblades):
self.geometryparams[i, 0] = 10 + 20 * self.R1.random()
self.geometryparams[i, 1] = 10 + 20 * self.R2.random()
self.geometryparams[i, 2] = self.R3.choice([-1, 1])
self.geometryparams[i, 3] = self.R4.choice([-1, 1])
def calculate_blade_geometry(self, blade, height):
"""Calculates the x and y coordinates for a given height of grass blade.
Args:
blade: int for the numeric ID of grass blade.
height: float for the height of grass blade.
Returns:
x, y: floats for the x and y coordinates of grass blade.
"""
x = (
self.geometryparams[blade, 2]
* (height / self.geometryparams[blade, 0])
* (height / self.geometryparams[blade, 0])
)
y = (
self.geometryparams[blade, 3]
* (height / self.geometryparams[blade, 1])
* (height / self.geometryparams[blade, 1])
)
x = round_value(x)
y = round_value(y)
return x, y
def calculate_root_geometry(self, root, depth):
"""Calculates the x and y coordinates for a given depth of grass root.
Args:
root: int for the umeric ID of grass root.
depth: float for the depth of grass root.
Returns:
x, y: floats for the x and y coordinates of grass root.
"""
self.geometryparams[root, 4] += -1 + 2 * self.R5.random()
self.geometryparams[root, 5] += -1 + 2 * self.R6.random()
x = round(self.geometryparams[root, 4])
y = round(self.geometryparams[root, 5])
return x, y

查看文件

@@ -0,0 +1,428 @@
import logging
from typing import List, Optional, Tuple
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from mpi4py_fft import PFFT, DistArray, newDistArray
from mpi4py_fft.pencil import Subcomm
from scipy import fftpack
from gprMax import config
from gprMax.cython.fractals_generate import generate_fractal2D
from gprMax.fractals.grass import Grass
from gprMax.fractals.mpi_utilities import calculate_starts_and_subshape, create_mpi_type
from gprMax.utilities.mpi import Dim, Dir, get_relative_neighbour
logger = logging.getLogger(__name__)
np.seterr(divide="raise")
class FractalSurface:
"""Fractal surfaces."""
surfaceIDs = ["xminus", "xplus", "yminus", "yplus", "zminus", "zplus"]
def __init__(self, xs, xf, ys, yf, zs, zf, dimension, seed):
"""
Args:
xs, xf, ys, yf, zs, zf: floats for the extent of the fractal surface
(one pair of coordinates must be equal
to correctly define a surface).
dimension: float for the fractal dimension that controls the fractal
distribution.
seed: int for seed value for random number generator.
"""
self.ID = None
self.surfaceID = None
self.start = np.array([xs, ys, zs], dtype=np.int32)
self.stop = np.array([xf, yf, zf], dtype=np.int32)
self.dtype = np.dtype(np.complex128)
self.seed = seed
self.dimension = (
dimension # Fractal dimension from: http://dx.doi.org/10.1017/CBO9781139174695
)
self.weighting = np.array([1, 1], dtype=np.float64)
self.fractalrange: Tuple[int, int] = (0, 0)
self.filldepth = 0
self.grass: List[Grass] = []
@property
def xs(self) -> int:
return self.start[0]
@xs.setter
def xs(self, value: int):
self.start[0] = value
@property
def ys(self) -> int:
return self.start[1]
@ys.setter
def ys(self, value: int):
self.start[1] = value
@property
def zs(self) -> int:
return self.start[2]
@zs.setter
def zs(self, value: int):
self.start[2] = value
@property
def xf(self) -> int:
return self.stop[0]
@xf.setter
def xf(self, value: int):
self.stop[0] = value
@property
def yf(self) -> int:
return self.stop[1]
@yf.setter
def yf(self, value: int):
self.stop[1] = value
@property
def zf(self) -> int:
return self.stop[2]
@zf.setter
def zf(self, value: int):
self.stop[2] = value
@property
def size(self) -> npt.NDArray[np.int32]:
return self.stop - self.start
@property
def nx(self) -> int:
return self.xf - self.xs
@property
def ny(self) -> int:
return self.yf - self.ys
@property
def nz(self) -> int:
return self.zf - self.zs
def get_surface_dims(self):
"""Gets the dimensions of the fractal surface based on surface plane."""
if self.xs == self.xf:
surfacedims = (self.ny, self.nz)
elif self.ys == self.yf:
surfacedims = (self.nx, self.nz)
elif self.zs == self.zf:
surfacedims = (self.nx, self.ny)
return surfacedims
def generate_fractal_surface(self):
"""Generate a 2D array with a fractal distribution."""
surfacedims = self.get_surface_dims()
self.fractalsurface = np.zeros(surfacedims, dtype=self.dtype)
# Positional vector at centre of array, scaled by weighting
v1 = np.array(
[
self.weighting[0] * (surfacedims[0]) / 2,
self.weighting[1] * (surfacedims[1]) / 2,
]
)
# 2D array of random numbers to be convolved with the fractal function
rng = np.random.default_rng(seed=self.seed)
A = rng.standard_normal(size=(surfacedims[0], surfacedims[1]))
# 2D FFT
A = fftpack.fftn(A)
# Generate fractal
generate_fractal2D(
surfacedims[0],
surfacedims[1],
0,
0,
surfacedims[0],
surfacedims[1],
config.get_model_config().ompthreads,
self.dimension,
self.weighting,
v1,
A,
self.fractalsurface,
)
# Set DC component of FFT to zero
self.fractalsurface[0, 0] = 0
# Take the real part (numerical errors can give rise to an imaginary part)
# of the IFFT, and convert type to floattype. N.B calculation of fractals
# must always be carried out at double precision, i.e. float64, complex128
self.fractalsurface = np.real(fftpack.ifftn(self.fractalsurface)).astype(
config.sim_config.dtypes["float_or_double"], copy=False
)
# Scale the fractal volume according to requested range
fractalmin = np.amin(self.fractalsurface)
fractalmax = np.amax(self.fractalsurface)
fractalrange = fractalmax - fractalmin
self.fractalsurface = (
self.fractalsurface * ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange)
+ self.fractalrange[0]
- ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange) * fractalmin
)
class MPIFractalSurface(FractalSurface):
def __init__(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
dimension: float,
seed: Optional[int],
comm: MPI.Cartcomm,
upper_bound: npt.NDArray[np.int32],
):
super().__init__(xs, xf, ys, yf, zs, zf, dimension, seed)
self.comm = comm
self.upper_bound = upper_bound
def generate_fractal_surface(self):
"""Generate a 2D array with a fractal distribution."""
if self.xs == self.xf:
color = self.xs
static_dimension = Dim.X
dims = [Dim.Y, Dim.Z]
elif self.ys == self.yf:
color = self.ys
static_dimension = Dim.Y
dims = [Dim.X, Dim.Z]
elif self.zs == self.zf:
color = self.zs
static_dimension = Dim.Z
dims = [Dim.X, Dim.Y]
# Exit early if this rank does not contain the Fractal Surface
if (
any(self.stop[dims] <= 0)
or any(self.start[dims] >= self.upper_bound[dims])
or self.fractalrange[1] <= 0
or self.fractalrange[0] >= self.upper_bound[static_dimension]
):
self.comm.Split(MPI.UNDEFINED)
# Update start and stop to local bounds
self.start = np.maximum(self.start, 0)
self.start = np.minimum(self.start, self.upper_bound)
self.stop = np.maximum(self.stop, 0)
self.stop = np.minimum(self.stop, self.upper_bound)
return
else:
# Create new cartsesian communicator for the Fractal Surface
comm = self.comm.Split(color=color)
assert isinstance(comm, MPI.Intracomm)
min_coord = np.array(self.comm.coords, dtype=np.int32)[dims]
max_coord = min_coord + 1
comm.Allreduce(MPI.IN_PLACE, min_coord, MPI.MIN)
comm.Allreduce(MPI.IN_PLACE, max_coord, MPI.MAX)
cart_dims = (max_coord - min_coord).tolist()
self.comm = comm.Create_cart(cart_dims)
# Check domain decomosition is valid for the Fractal Volume
if all([dim > 1 for dim in self.comm.dims]):
raise ValueError(
"Fractal surface must be positioned such that its MPI decomposition is 1 in at least"
f" 1 dimension. Current decompostion is: {self.comm.dims}"
)
surfacedims = self.get_surface_dims()
# Positional vector at centre of array, scaled by weighting
v1 = self.weighting * surfacedims / 2
subcomm = Subcomm(self.comm)
A = DistArray(self.size[dims], subcomm, dtype=self.dtype)
fft = PFFT(
None,
axes=tuple(np.argsort(self.comm.dims)[::-1]),
darray=A,
collapse=False,
backend="fftw",
)
# Decomposition of A may be different to the MPIGrid
A_shape = np.array(A.shape)
A_substart = np.array(A.substart)
# 3D array of random numbers to be convolved with the fractal function
rng = np.random.default_rng(seed=self.seed)
for index in np.ndindex(*A.global_shape):
index = np.array(index)
if any(index < A_substart) or any(index >= A_substart + A_shape):
rng.standard_normal()
else:
index -= A_substart
A[index[0], index[1]] = rng.standard_normal()
A_hat = newDistArray(fft)
assert isinstance(A_hat, DistArray)
# 2D FFT
fft.forward(A, A_hat, normalize=False)
# Generate fractal
generate_fractal2D(
A_hat.shape[0],
A_hat.shape[1],
A_hat.substart[0],
A_hat.substart[1],
A_hat.global_shape[0],
A_hat.global_shape[1],
config.get_model_config().ompthreads,
self.dimension,
self.weighting,
v1,
A_hat,
A_hat,
)
# Set DC component of FFT to zero
if all(A_substart == 0):
A_hat[0, 0] = 0
# Inverse 2D FFT transform
fft.backward(A_hat, A, normalize=True)
# Take the real part (numerical errors can give rise to an imaginary part)
# of the IFFT, and convert type to floattype. N.B calculation of fractals
# must always be carried out at double precision, i.e. float64, complex128
A = np.real(A).astype(config.sim_config.dtypes["float_or_double"], copy=False)
# Allreduce to get min and max values in the fractal surface
min_value = np.array(np.amin(A), dtype=config.sim_config.dtypes["float_or_double"])
max_value = np.array(np.amax(A), dtype=config.sim_config.dtypes["float_or_double"])
self.comm.Allreduce(MPI.IN_PLACE, min_value, MPI.MIN)
self.comm.Allreduce(MPI.IN_PLACE, max_value, MPI.MAX)
# Scale the fractal volume according to requested range
fractalrange = max_value - min_value
A = (
A * ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange)
+ self.fractalrange[0]
- ((self.fractalrange[1] - self.fractalrange[0]) / fractalrange) * min_value
)
# Distribute A (DistArray) to match the MPIGrid decomposition
local_shape = (np.minimum(self.stop, self.upper_bound) - np.maximum(self.start, 0))[dims]
self.fractalsurface = np.zeros(
local_shape,
dtype=config.sim_config.dtypes["float_or_double"],
)
# Negative means send to negative neighbour
# Positive means receive from negative neighbour
negative_offset = np.where(self.start[dims] >= 0, A_substart, self.start[dims] + A_substart)
# Negative means send to positive neighbour
# Positive means receive from positive neighbour
positive_offset = np.minimum(self.stop, self.upper_bound)[dims] - (
self.start[dims] + A_substart + A_shape
)
dirs = np.full(2, Dir.NONE)
starts, subshape = calculate_starts_and_subshape(
A_shape, -negative_offset, -positive_offset, dirs, sending=True
)
ends = starts + subshape
A_local = A[starts[0] : ends[0], starts[1] : ends[1]]
starts, subshape = calculate_starts_and_subshape(
local_shape, negative_offset, positive_offset, dirs
)
ends = starts + subshape
self.fractalsurface[starts[0] : ends[0], starts[1] : ends[1]] = A_local
requests: List[MPI.Request] = []
# Need to check neighbours in each direction (2D plane)
sections = [
(Dir.NEG, Dir.NONE),
(Dir.POS, Dir.NONE),
(Dir.NONE, Dir.NEG),
(Dir.NONE, Dir.POS),
(Dir.NEG, Dir.NEG),
(Dir.NEG, Dir.POS),
(Dir.POS, Dir.NEG),
(Dir.POS, Dir.POS),
]
for section in sections:
dirs[0] = section[0]
dirs[1] = section[1]
rank = get_relative_neighbour(self.comm, dirs)
# Skip if no neighbour
if rank == -1:
continue
# Check if any data to send
if all(
np.select(
[dirs == Dir.NEG, dirs == Dir.POS],
[negative_offset <= 0, positive_offset <= 0],
dirs == Dir.NONE,
)
):
mpi_type = create_mpi_type(
A_shape, -negative_offset, -positive_offset, dirs, sending=True
)
logger.debug(
f"Sending fractal surface to rank {rank}, MPI type={mpi_type.decode()}"
)
self.comm.Isend([A, mpi_type], rank)
# Check if any data to receive
if all(
np.select(
[dirs == Dir.NEG, dirs == Dir.POS],
[negative_offset > 0, positive_offset > 0],
dirs == Dir.NONE,
)
):
mpi_type = create_mpi_type(local_shape, negative_offset, positive_offset, dirs)
logger.debug(
f"Receiving fractal surface from rank {rank}, MPI type={mpi_type.decode()}"
)
request = self.comm.Irecv([self.fractalsurface, mpi_type], rank)
requests.append(request)
if len(requests) > 0:
requests[0].Waitall(requests)
# Update start and stop to local bounds
self.start = np.maximum(self.start, 0)
self.start = np.minimum(self.start, self.upper_bound)
self.stop = np.maximum(self.stop, 0)
self.stop = np.minimum(self.stop, self.upper_bound)
logger.debug(
f"Generated fractal surface: start={self.start}, stop={self.stop}, size={self.size}, fractalrange={self.fractalrange}"
)

查看文件

@@ -0,0 +1,524 @@
# 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
from typing import List, Optional, Tuple, Union
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from mpi4py_fft import PFFT, DistArray, newDistArray
from mpi4py_fft.pencil import Subcomm
from scipy import fftpack
from gprMax import config
from gprMax.cython.fractals_generate import generate_fractal3D
from gprMax.fractals.fractal_surface import FractalSurface
from gprMax.fractals.mpi_utilities import calculate_starts_and_subshape, create_mpi_type
from gprMax.materials import ListMaterial, PeplinskiSoil, RangeMaterial
from gprMax.utilities.mpi import Dim, Dir, get_relative_neighbour
from gprMax.utilities.utilities import round_value
logger = logging.getLogger(__name__)
np.seterr(divide="raise")
class FractalVolume:
"""Fractal volumes."""
def __init__(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
dimension: float,
seed: Optional[int],
):
"""
Args:
xs, xf, ys, yf, zs, zf: floats for the extent of the fractal volume.
dimension: float for the fractal dimension that controls the fractal
distribution.
seed: int for seed value for random number generator.
"""
self.ID = None
self.operatingonID = None
self.start = np.array([xs, ys, zs], dtype=np.int32)
self.stop = np.array([xf, yf, zf], dtype=np.int32)
self.original_start = self.start.copy()
self.original_stop = self.stop.copy()
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.weighting = np.array([1, 1, 1], dtype=np.float64)
self.nbins = 0
self.mixingmodel: Optional[Union[PeplinskiSoil, RangeMaterial, ListMaterial]] = None
self.fractalsurfaces: List[FractalSurface] = []
@property
def xs(self) -> int:
return self.start[0]
@xs.setter
def xs(self, value: int):
self.start[0] = value
@property
def ys(self) -> int:
return self.start[1]
@ys.setter
def ys(self, value: int):
self.start[1] = value
@property
def zs(self) -> int:
return self.start[2]
@zs.setter
def zs(self, value: int):
self.start[2] = value
@property
def xf(self) -> int:
return self.stop[0]
@xf.setter
def xf(self, value: int):
self.stop[0] = value
@property
def yf(self) -> int:
return self.stop[1]
@yf.setter
def yf(self, value: int):
self.stop[1] = value
@property
def zf(self) -> int:
return self.stop[2]
@zf.setter
def zf(self, value: int):
self.stop[2] = value
@property
def size(self) -> npt.NDArray[np.int32]:
return self.stop - self.start
@property
def nx(self) -> int:
return self.xf - self.xs
@property
def ny(self) -> int:
return self.yf - self.ys
@property
def nz(self) -> int:
return self.zf - self.zs
@property
def originalxs(self) -> int:
return self.original_start[0]
@originalxs.setter
def originalxs(self, value: int):
self.original_start[0] = value
@property
def originalys(self) -> int:
return self.original_start[1]
@originalys.setter
def originalys(self, value: int):
self.original_start[1] = value
@property
def originalzs(self) -> int:
return self.original_start[2]
@originalzs.setter
def originalzs(self, value: int):
self.original_start[2] = value
@property
def originalxf(self) -> int:
return self.original_stop[0]
@originalxf.setter
def originalxf(self, value: int):
self.original_stop[0] = value
@property
def originalyf(self) -> int:
return self.original_stop[1]
@originalyf.setter
def originalyf(self, value: int):
self.original_stop[1] = value
@property
def originalzf(self) -> int:
return self.original_stop[2]
@originalzf.setter
def originalzf(self, value: int):
self.original_stop[2] = value
def generate_fractal_volume(self) -> bool:
"""Generate a 3D volume with a fractal distribution."""
# 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.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.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.insert(filterscaling, 2, 1)
else:
filterscaling = np.amin(np.array([self.nx, self.ny, self.nz])) / np.array(
[self.nx, self.ny, self.nz]
)
# Adjust weighting to account for filter scaling
self.weighting = np.multiply(self.weighting, filterscaling)
self.fractalvolume = np.zeros((self.nx, self.ny, self.nz), dtype=self.dtype)
# Positional vector at centre of array, scaled by weighting
v1 = np.array(
[
self.weighting[0] * self.nx / 2,
self.weighting[1] * self.ny / 2,
self.weighting[2] * self.nz / 2,
]
)
# 3D array of random numbers to be convolved with the fractal function
rng = np.random.default_rng(seed=self.seed)
A = rng.standard_normal(size=(self.nx, self.ny, self.nz))
# 3D FFT
A = fftpack.fftn(A)
# Generate fractal
generate_fractal3D(
self.nx,
self.ny,
self.nz,
0,
0,
0,
self.nx,
self.ny,
self.nz,
config.get_model_config().ompthreads,
self.dimension,
self.weighting,
v1,
A,
self.fractalvolume,
)
# Set DC component of FFT to zero
self.fractalvolume[0, 0, 0] = 0
# Take the real part (numerical errors can give rise to an imaginary part)
# of the IFFT, and convert type to floattype. N.B calculation of fractals
# must always be carried out at double precision, i.e. float64, complex128
self.fractalvolume = np.real(fftpack.ifftn(self.fractalvolume)).astype(
config.sim_config.dtypes["float_or_double"], copy=False
)
# Bin fractal values
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(
self.fractalvolume[:, j, k], bins, right=True
)
return True
def generate_volume_mask(self):
"""Generate a 3D volume to use as a mask for adding rough surfaces,
water and grass/roots. Zero signifies the mask is not set, one
signifies the mask is set.
"""
self.mask = np.zeros((self.nx, self.ny, self.nz), dtype=np.int8)
maskxs = self.originalxs - self.xs
maskxf = (self.originalxf - self.originalxs) + maskxs
maskys = self.originalys - self.ys
maskyf = (self.originalyf - self.originalys) + maskys
maskzs = self.originalzs - self.zs
maskzf = (self.originalzf - self.originalzs) + maskzs
self.mask[maskxs:maskxf, maskys:maskyf, maskzs:maskzf] = 1
class MPIFractalVolume(FractalVolume):
def __init__(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
dimension: float,
seed: Optional[int],
comm: MPI.Cartcomm,
upper_bound: npt.NDArray[np.int32],
):
super().__init__(xs, xf, ys, yf, zs, zf, dimension, seed)
self.comm = comm
self.upper_bound = upper_bound
# Limit the original start and stop to within the local bounds
self.original_start = np.maximum(self.original_start, 0)
self.original_stop = np.minimum(self.original_stop, self.upper_bound)
# Ensure original_stop is not less than original_start
self.original_stop = np.where(
self.original_stop < self.original_start, self.original_start, self.original_stop
)
def generate_fractal_volume(self) -> bool:
"""Generate a 3D volume with a fractal distribution."""
# Exit early if this rank does not contain the Fractal Volume
# The size of a fractal volume can increase if a Fractal Surface
# is attached. Hence the check needs to happen here once that
# has happened.
if any(self.stop <= 0) or any(self.start >= self.upper_bound):
self.comm.Split(MPI.UNDEFINED)
return False
else:
# Create new cartsesian communicator for the Fractal Volume
comm = self.comm.Split()
assert isinstance(comm, MPI.Intracomm)
min_coord = np.array(self.comm.coords, dtype=np.int32)
max_coord = min_coord + 1
comm.Allreduce(MPI.IN_PLACE, min_coord, MPI.MIN)
comm.Allreduce(MPI.IN_PLACE, max_coord, MPI.MAX)
self.comm = comm.Create_cart((max_coord - min_coord).tolist())
# Check domain decomosition is valid for the Fractal Volume
if all([dim > 1 for dim in self.comm.dims]):
raise ValueError(
"Fractal volume must be positioned such that its MPI decomposition is 1 in at least"
f" 1 dimension. Current decompostion is: {self.comm.dims}"
)
# Scale filter according to size of fractal volume
sorted_size = np.sort(self.size)
min_size = sorted_size[1] if sorted_size[0] == 1 else sorted_size[0]
filterscaling = np.where(self.size == 1, 1, min_size / self.size)
# Adjust weighting to account for filter scaling
self.weighting = np.multiply(self.weighting, filterscaling)
# Positional vector at centre of array, scaled by weighting
v1 = self.weighting * self.size / 2
subcomm = Subcomm(self.comm)
A = DistArray(self.size, subcomm, dtype=self.dtype)
fft = PFFT(
None,
axes=tuple(np.argsort(self.comm.dims)[::-1]),
darray=A,
collapse=False,
backend="fftw",
)
# Decomposition of A may be different to the MPIGrid
A_shape = np.array(A.shape)
A_substart = np.array(A.substart)
static_dimension = Dim(A.alignment)
# 3D array of random numbers to be convolved with the fractal function
rng = np.random.default_rng(seed=self.seed)
for index in np.ndindex(*A.global_shape):
index = np.array(index)
if any(index < A_substart) or any(index >= A_substart + A_shape):
rng.standard_normal()
else:
index -= A_substart
A[index[0], index[1], index[2]] = rng.standard_normal()
A_hat = newDistArray(fft)
assert isinstance(A_hat, DistArray)
# 3D FFT
fft.forward(A, A_hat, normalize=False)
# Generate fractal
generate_fractal3D(
A_hat.shape[0],
A_hat.shape[1],
A_hat.shape[2],
A_hat.substart[0],
A_hat.substart[1],
A_hat.substart[2],
A_hat.global_shape[0],
A_hat.global_shape[1],
A_hat.global_shape[2],
config.get_model_config().ompthreads,
self.dimension,
self.weighting,
v1,
A_hat,
A_hat,
)
# Set DC component of FFT to zero
if all(A_substart == 0):
A_hat[0, 0, 0] = 0
# Inverse 3D FFT transform
fft.backward(A_hat, A, normalize=True)
# Take the real part (numerical errors can give rise to an imaginary part)
# of the IFFT, and convert type to floattype. N.B calculation of fractals
# must always be carried out at double precision, i.e. float64, complex128
A = np.real(A).astype(config.sim_config.dtypes["float_or_double"], copy=False)
# Allreduce to get min and max values in the fractal volume
min_value = np.array(np.amin(A), dtype=config.sim_config.dtypes["float_or_double"])
max_value = np.array(np.amax(A), dtype=config.sim_config.dtypes["float_or_double"])
self.comm.Allreduce(MPI.IN_PLACE, min_value, MPI.MIN)
self.comm.Allreduce(MPI.IN_PLACE, max_value, MPI.MAX)
# Bin fractal values
bins = np.linspace(min_value, max_value, self.nbins)
for j in range(A_shape[1]):
for k in range(A_shape[2]):
A[:, j, k] = np.digitize(A[:, j, k], bins, right=True)
# Distribute A (DistArray) to match the MPIGrid decomposition
local_shape = np.minimum(self.stop, self.upper_bound) - np.maximum(self.start, 0)
self.fractalvolume = np.zeros(
local_shape,
dtype=config.sim_config.dtypes["float_or_double"],
)
# Negative means send to negative neighbour
# Positive means receive from negative neighbour
negative_offset = np.where(self.start >= 0, A_substart, self.start + A_substart)
# Negative means send to positive neighbour
# Positive means receive from positive neighbour
positive_offset = np.minimum(self.stop, self.upper_bound) - (
self.start + A_substart + A_shape
)
dirs = np.full(3, Dir.NONE)
starts, subshape = calculate_starts_and_subshape(
A_shape, -negative_offset, -positive_offset, dirs, sending=True
)
ends = starts + subshape
A_local = A[starts[0] : ends[0], starts[1] : ends[1], starts[2] : ends[2]]
starts, subshape = calculate_starts_and_subshape(
local_shape, negative_offset, positive_offset, dirs
)
ends = starts + subshape
self.fractalvolume[starts[0] : ends[0], starts[1] : ends[1], starts[2] : ends[2]] = A_local
requests: List[MPI.Request] = []
# Need to check neighbours in each direction (2D plane)
sections = [
(Dir.NEG, Dir.NONE),
(Dir.POS, Dir.NONE),
(Dir.NONE, Dir.NEG),
(Dir.NONE, Dir.POS),
(Dir.NEG, Dir.NEG),
(Dir.NEG, Dir.POS),
(Dir.POS, Dir.NEG),
(Dir.POS, Dir.POS),
]
# Dimensions of the 2D plane
dims = [dim for dim in Dim if dim != static_dimension]
for section in sections:
dirs[dims[0]] = section[0]
dirs[dims[1]] = section[1]
rank = get_relative_neighbour(self.comm, dirs)
# Skip if no neighbour
if rank == -1:
continue
# Check if any data to send
if all(
np.select(
[dirs == Dir.NEG, dirs == Dir.POS],
[negative_offset <= 0, positive_offset <= 0],
dirs == Dir.NONE,
)
):
mpi_type = create_mpi_type(
A_shape, -negative_offset, -positive_offset, dirs, sending=True
)
logger.debug(f"Sending fractal volume to rank {rank}, MPI type={mpi_type.decode()}")
self.comm.Isend([A, mpi_type], rank)
# Check if any data to receive
if all(
np.select(
[dirs == Dir.NEG, dirs == Dir.POS],
[negative_offset > 0, positive_offset > 0],
dirs == Dir.NONE,
)
):
mpi_type = create_mpi_type(local_shape, negative_offset, positive_offset, dirs)
logger.debug(
f"Receiving fractal volume from rank {rank}, MPI type={mpi_type.decode()}"
)
request = self.comm.Irecv([self.fractalvolume, mpi_type], rank)
requests.append(request)
if len(requests) > 0:
requests[0].Waitall(requests)
# Update start and stop to local bounds
self.start = np.maximum(self.start, 0)
self.stop = np.minimum(self.stop, self.upper_bound)
logger.debug(
f"Generated fractal volume: start={self.start}, stop={self.stop}, size={self.size}"
)
return True

86
gprMax/fractals/grass.py 普通文件
查看文件

@@ -0,0 +1,86 @@
import numpy as np
from gprMax import config
from gprMax.utilities.utilities import round_value
np.seterr(divide="raise")
class Grass:
"""Geometry information for blades of grass."""
def __init__(self, numblades, seed):
"""
Args:
numblades: int for the number of blades of grass.
seed: int for seed value for random number generator.
"""
self.numblades = numblades
self.geometryparams = np.zeros(
(self.numblades, 6), dtype=config.sim_config.dtypes["float_or_double"]
)
self.seed = seed
self.set_geometry_parameters()
def set_geometry_parameters(self):
"""Sets randomly defined parameters that will be used to calculate
blade and root geometries.
"""
self.R1 = np.random.default_rng(seed=self.seed)
self.R2 = np.random.default_rng(seed=self.seed)
self.R3 = np.random.default_rng(seed=self.seed)
self.R4 = np.random.default_rng(seed=self.seed)
self.R5 = np.random.default_rng(seed=self.seed)
self.R6 = np.random.default_rng(seed=self.seed)
for i in range(self.numblades):
self.geometryparams[i, 0] = 10 + 20 * self.R1.random()
self.geometryparams[i, 1] = 10 + 20 * self.R2.random()
self.geometryparams[i, 2] = self.R3.choice([-1, 1])
self.geometryparams[i, 3] = self.R4.choice([-1, 1])
def calculate_blade_geometry(self, blade, height):
"""Calculates the x and y coordinates for a given height of grass blade.
Args:
blade: int for the numeric ID of grass blade.
height: float for the height of grass blade.
Returns:
x, y: floats for the x and y coordinates of grass blade.
"""
x = (
self.geometryparams[blade, 2]
* (height / self.geometryparams[blade, 0])
* (height / self.geometryparams[blade, 0])
)
y = (
self.geometryparams[blade, 3]
* (height / self.geometryparams[blade, 1])
* (height / self.geometryparams[blade, 1])
)
x = round_value(x)
y = round_value(y)
return x, y
def calculate_root_geometry(self, root, depth):
"""Calculates the x and y coordinates for a given depth of grass root.
Args:
root: int for the umeric ID of grass root.
depth: float for the depth of grass root.
Returns:
x, y: floats for the x and y coordinates of grass root.
"""
self.geometryparams[root, 4] += -1 + 2 * self.R5.random()
self.geometryparams[root, 5] += -1 + 2 * self.R6.random()
x = round(self.geometryparams[root, 4])
y = round(self.geometryparams[root, 5])
return x, y

查看文件

@@ -0,0 +1,62 @@
from typing import Tuple
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
from gprMax.utilities.mpi import Dir
def calculate_starts_and_subshape(
shape: npt.NDArray[np.int32],
negative_offset: npt.NDArray[np.int32],
positive_offset: npt.NDArray[np.int32],
dirs: npt.NDArray[np.int32],
sending: bool = False,
) -> Tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]]:
negative_offset = np.where(
dirs == Dir.NONE,
np.maximum(negative_offset, 0),
np.abs(negative_offset),
)
positive_offset = np.where(
dirs == Dir.NONE,
np.maximum(positive_offset, 0),
np.abs(positive_offset),
)
starts = np.select(
[dirs == Dir.NEG, dirs == Dir.POS],
[0, shape - positive_offset - sending],
default=negative_offset,
)
subshape = np.select(
[dirs == Dir.NEG, dirs == Dir.POS],
[negative_offset + sending, positive_offset + sending],
default=shape - negative_offset - positive_offset,
)
return starts, subshape
def create_mpi_type(
shape: npt.NDArray[np.int32],
negative_offset: npt.NDArray[np.int32],
positive_offset: npt.NDArray[np.int32],
dirs: npt.NDArray[np.int32],
sending: bool = False,
) -> MPI.Datatype:
starts, subshape = calculate_starts_and_subshape(
shape, negative_offset, positive_offset, dirs, sending
)
print(
f"create_mpi_type: shape={shape}, negative_offset={negative_offset}, positive_offset={positive_offset}, dirs={dirs}, sending={sending}"
)
print(f"create_mpi_type: starts={starts}, subshape={subshape}")
mpi_type = MPI.FLOAT.Create_subarray(shape.tolist(), subshape.tolist(), starts.tolist())
mpi_type.Commit()
return mpi_type

查看文件

@@ -21,7 +21,7 @@ import itertools
import logging
import sys
from collections import OrderedDict
from typing import Any, Iterable, List, Tuple, Union
from typing import Any, Iterable, List, Optional, Tuple, Union
import numpy as np
import numpy.typing as npt
@@ -32,7 +32,8 @@ from typing_extensions import TypeVar
from gprMax import config
from gprMax.cython.pml_build import pml_average_er_mr
from gprMax.cython.yee_cell_build import build_electric_components, build_magnetic_components
from gprMax.fractals import FractalVolume
from gprMax.fractals.fractal_surface import FractalSurface
from gprMax.fractals.fractal_volume import FractalVolume
from gprMax.materials import ListMaterial, Material, PeplinskiSoil, RangeMaterial, process_materials
from gprMax.pml import CFS, PML, print_pml_info
from gprMax.receivers import Rx
@@ -185,6 +186,34 @@ class FDTDGrid:
self.pmls["thickness"]["ymax"] = int(thickness[4])
self.pmls["thickness"]["zmax"] = int(thickness[5])
def add_fractal_volume(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
frac_dim: float,
seed: Optional[int],
) -> FractalVolume:
volume = FractalVolume(xs, xf, ys, yf, zs, zf, frac_dim, seed)
self.fractalvolumes.append(volume)
return volume
def create_fractal_surface(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
frac_dim: float,
seed: Optional[int],
) -> FractalSurface:
return FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim, seed)
def add_source(self, source: Source):
if isinstance(source, VoltageSource):
self.voltagesources.append(source)

查看文件

@@ -28,6 +28,8 @@ from numpy import ndarray
from gprMax import config
from gprMax.cython.pml_build import pml_sum_er_mr
from gprMax.fractals.fractal_surface import MPIFractalSurface
from gprMax.fractals.fractal_volume import MPIFractalVolume
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.pml import MPIPML, PML
from gprMax.receivers import Rx
@@ -130,6 +132,34 @@ class MPIGrid(FDTDGrid):
if self.has_neighbour(Dim.Z, Dir.POS):
self.pmls["thickness"]["zmax"] = 0
def add_fractal_volume(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
frac_dim: float,
seed: Optional[int],
) -> MPIFractalVolume:
volume = MPIFractalVolume(xs, xf, ys, yf, zs, zf, frac_dim, seed, self.comm, self.size)
self.fractalvolumes.append(volume)
return volume
def create_fractal_surface(
self,
xs: int,
xf: int,
ys: int,
yf: int,
zs: int,
zf: int,
frac_dim: float,
seed: Optional[int],
) -> MPIFractalSurface:
return MPIFractalSurface(xs, xf, ys, yf, zs, zf, frac_dim, seed, self.comm, self.size)
def is_coordinator(self) -> bool:
"""Test if the current rank is the coordinator.

查看文件

@@ -421,6 +421,10 @@ def process_geometrycmds(geometry):
)
raise ValueError
# Only build objects attached to the current fractal box
if tmp[12] != ID:
continue
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6]))
frac_dim = float(tmp[7])
@@ -462,6 +466,10 @@ def process_geometrycmds(geometry):
)
raise ValueError
# Only build objects attached to the current fractal box
if tmp[8] != ID:
continue
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6]))
depth = float(tmp[7])
@@ -477,6 +485,10 @@ def process_geometrycmds(geometry):
)
raise ValueError
# Only build objects attached to the current fractal box
if tmp[11] != ID:
continue
p1 = (float(tmp[1]), float(tmp[2]), float(tmp[3]))
p2 = (float(tmp[4]), float(tmp[5]), float(tmp[6]))
frac_dim = float(tmp[7])

查看文件

@@ -20,7 +20,8 @@ import logging
import numpy as np
from gprMax.fractals import FractalSurface, Grass
from gprMax.fractals.fractal_surface import FractalSurface
from gprMax.fractals.grass import Grass
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.materials import create_grass
from gprMax.user_objects.rotatable import RotatableMixin
@@ -92,119 +93,84 @@ class AddGrass(RotatableMixin, GeometryUserObject):
# Get the correct fractal volume
volumes = [volume for volume in grid.fractalvolumes if volume.ID == fractal_box_id]
try:
if volumes:
volume = volumes[0]
except NameError:
logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
raise
else:
raise ValueError(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
uip = self._create_uip(grid)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
discretised_p1, discretised_p2 = uip.check_output_object_bounds(p1, p2, self.__str__())
xs, ys, zs = discretised_p1
xf, yf, zf = discretised_p2
if frac_dim < 0:
logger.exception(
raise ValueError(
f"{self.__str__()} requires a positive value for the fractal dimension"
)
raise ValueError
if limits[0] < 0 or limits[1] < 0:
logger.exception(
raise ValueError(
f"{self.__str__()} requires a positive value for the minimum and maximum heights for grass blades"
)
raise ValueError
# Check for valid orientations
if np.count_nonzero(discretised_p1 == discretised_p2) != 1:
raise ValueError(f"{self.__str__()} dimensions are not specified correctly")
if xs == xf:
if ys == yf or zs == zf:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if xs not in [volume.xs, volume.xf]:
logger.exception(
f"{self.__str__()} must specify external surfaces on a fractal box"
)
raise ValueError
fractalrange = (
round_value(limits[0] / grid.dx),
round_value(limits[1] / grid.dx),
)
# xminus surface
if xs == volume.xs:
logger.exception(
raise ValueError(
f"{self.__str__()} grass can only be specified on surfaces in the positive axis direction"
)
raise ValueError
# xplus surface
elif xf == volume.xf:
if fractalrange[1] > grid.nx:
logger.exception(
f"{self.__str__()} cannot apply grass to "
"fractal box as it would exceed the domain "
"size in the x direction"
)
raise ValueError
lower_bound = uip.discretise_point((limits[0], 0, 0))
upper_bound = uip.discretise_point((limits[1], p2[1], p2[2]))
uip.point_within_bounds(upper_bound, self.__str__())
fractalrange = (lower_bound[0], upper_bound[0])
requestedsurface = "xplus"
else:
raise ValueError(
f"{self.__str__()} must specify external surfaces on a fractal box"
)
elif ys == yf:
if zs == zf:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if ys not in [volume.ys, volume.yf]:
logger.exception(
f"{self.__str__()} must specify external surfaces on a fractal box"
)
raise ValueError
fractalrange = (
round_value(limits[0] / grid.dy),
round_value(limits[1] / grid.dy),
)
# yminus surface
if ys == volume.ys:
logger.exception(
raise ValueError(
f"{self.__str__()} grass can only be specified on surfaces in the positive axis direction"
)
raise ValueError
# yplus surface
elif yf == volume.yf:
if fractalrange[1] > grid.ny:
logger.exception(
f"{self.__str__()} cannot apply grass to "
"fractal box as it would exceed the domain "
"size in the y direction"
)
raise ValueError
lower_bound = uip.discretise_point((0, limits[0], 0))
upper_bound = uip.discretise_point((p2[0], limits[1], p2[2]))
uip.point_within_bounds(upper_bound, self.__str__())
fractalrange = (lower_bound[1], upper_bound[1])
requestedsurface = "yplus"
elif zs == zf:
if zs not in [volume.zs, volume.zf]:
logger.exception(
else:
raise ValueError(
f"{self.__str__()} must specify external surfaces on a fractal box"
)
raise ValueError
fractalrange = (
round_value(limits[0] / grid.dz),
round_value(limits[1] / grid.dz),
)
elif zs == zf:
# zminus surface
if zs == volume.zs:
logger.exception(
raise ValueError(
f"{self.__str__()} grass can only be specified on surfaces in the positive axis direction"
)
raise ValueError
# zplus surface
elif zf == volume.zf:
if fractalrange[1] > grid.nz:
logger.exception(
f"{self.__str__()} cannot apply grass to "
"fractal box as it would exceed the domain "
"size in the z direction"
)
raise ValueError
lower_bound = uip.discretise_point((0, 0, limits[0]))
upper_bound = uip.discretise_point((p2[0], p2[1], limits[1]))
uip.point_within_bounds(upper_bound, self.__str__())
fractalrange = (lower_bound[2], upper_bound[2])
requestedsurface = "zplus"
else:
raise ValueError(
f"{self.__str__()} must specify external surfaces on a fractal box"
)
else:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
raise ValueError(f"{self.__str__()} dimensions are not specified correctly")
surface = FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim, seed)
surface.ID = "grass"
@@ -215,11 +181,10 @@ class AddGrass(RotatableMixin, GeometryUserObject):
surface.operatingonID = volume.ID
surface.generate_fractal_surface()
if n_blades > surface.fractalsurface.shape[0] * surface.fractalsurface.shape[1]:
logger.exception(
raise ValueError(
f"{self.__str__()} the specified surface is not large "
"enough for the number of grass blades/roots specified"
)
raise ValueError
# Scale the distribution so that the summation is equal to one,
# i.e. a probability distribution
@@ -266,18 +231,20 @@ class AddGrass(RotatableMixin, GeometryUserObject):
grass = next((x for x in grid.materials if x.ID == "grass"))
testgrass = next((x for x in grass.tau if x < grid.dt), None)
if testgrass:
logger.exception(
raise ValueError(
f"{self.__str__()} requires the time step for the "
"model to be less than the relaxation time required to model grass."
)
raise ValueError
volume.fractalsurfaces.append(surface)
p3 = uip.round_to_grid_static_point(p1)
p4 = uip.round_to_grid_static_point(p2)
logger.info(
f"{self.grid_name(grid)}{n_blades} blades of grass on surface from "
f"{xs * grid.dx:g}m, {ys * grid.dy:g}m, {zs * grid.dz:g}m, "
f"to {xf * grid.dx:g}m, {yf * grid.dy:g}m, {zf * grid.dz:g}m "
f"{p3[0]:g}m, {p3[1]:g}m, {p3[2]:g}m, "
f"to {p4[0]:g}m, {p4[1]:g}m, {p4[2]:g}m "
f"with fractal dimension {surface.dimension:g}, fractal seeding "
f"{surface.seed}, and range {limits[0]:g}m to {limits[1]:g}m, "
f"added to {surface.operatingonID}."

查看文件

@@ -20,11 +20,9 @@ import logging
import numpy as np
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
@@ -37,18 +35,19 @@ class AddSurfaceRoughness(RotatableMixin, GeometryUserObject):
Attributes:
p1: list of the lower left (x,y,z) coordinates of a surface on a
FractalBox class.
p2: list of the upper right (x,y,z) coordinates of a surface on a
FractalBox class.
frac_dim: float for the fractal dimension which, for an orthogonal
parallelepiped, should take values between zero and three.
weighting: list with weightings in the first and second direction of
the surface.
limits: ist to define lower and upper limits for a range over which
the surface roughness can vary.
fractal_box_id: string identifier for the FractalBox class
that the surface roughness should be applied to.
seed: (optional) float parameter which controls the seeding of the random
number generator used to create the fractals.
p2: list of the upper right (x,y,z) coordinates of a surface on
a FractalBox class.
frac_dim: float for the fractal dimension which, for an
orthogonal parallelepiped, should take values between zero
and three.
weighting: list with weightings in the first and second
direction of the surface.
limits: list to define lower and upper limits for a range over
which the surface roughness can vary.
fractal_box_id: string identifier for the FractalBox class that
the surface roughness should be applied to.
seed: (optional) float parameter which controls the seeding of
the random number generator used to create the fractals.
"""
@property
@@ -95,145 +94,111 @@ class AddSurfaceRoughness(RotatableMixin, GeometryUserObject):
if volumes:
volume = volumes[0]
else:
logger.exception(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
raise ValueError
raise ValueError(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
uip = self._create_uip(grid)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
discretised_p1, discretised_p2 = uip.check_output_object_bounds(p1, p2, self.__str__())
xs, ys, zs = discretised_p1
xf, yf, zf = discretised_p2
if frac_dim < 0:
logger.exception(
raise ValueError(
f"{self.__str__()} requires a positive value for the fractal dimension"
)
raise ValueError
if weighting[0] < 0:
logger.exception(
f"{self.__str__()} requires a positive value for the "
"fractal weighting in the first direction of the surface"
raise ValueError(
f"{self.__str__()} requires a positive value for the fractal weighting in the first"
" direction of the surface"
)
raise ValueError
if weighting[1] < 0:
logger.exception(
f"{self.__str__()} requires a positive value for the "
"fractal weighting in the second direction of the surface"
raise ValueError(
f"{self.__str__()} requires a positive value for the fractal weighting in the"
" second direction of the surface"
)
raise ValueError
# Check for valid orientations
if np.count_nonzero(discretised_p1 == discretised_p2) != 1:
raise ValueError(f"{self.__str__()} dimensions are not specified correctly")
if xs == xf:
if ys == yf or zs == zf:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if xs not in [volume.xs, volume.xf]:
logger.exception(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
raise ValueError
fractalrange = (
round_value(limits[0] / grid.dx),
round_value(limits[1] / grid.dx),
)
# xminus surface
if xs == volume.xs:
if fractalrange[0] < 0 or fractalrange[1] > volume.xf:
logger.exception(
f"{self.__str__()} cannot apply fractal surface "
"to fractal box as it would exceed either the "
"upper coordinates of the fractal box or the "
"domain in the x direction"
)
raise ValueError
lower_bound = discretised_p1
upper_bound = uip.discretise_point((limits[1], p2[1], p2[2]))
grid_bound = uip.discretise_point((limits[0], p1[1], p1[2]))
fractalrange = (grid_bound[0], upper_bound[0])
requestedsurface = "xminus"
# xplus surface
elif xf == volume.xf:
if fractalrange[0] < volume.xs or fractalrange[1] > grid.nx:
logger.exception(
f"{self.__str__()} cannot apply fractal surface "
"to fractal box as it would exceed either the "
"lower coordinates of the fractal box or the "
"domain in the x direction"
)
raise ValueError
lower_bound = uip.discretise_point((limits[0], p1[1], p1[2]))
upper_bound = discretised_p2
grid_bound = uip.discretise_point((limits[1], p2[1], p2[2]))
fractalrange = (lower_bound[0], grid_bound[0])
requestedsurface = "xplus"
elif ys == yf:
if zs == zf:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if ys not in [volume.ys, volume.yf]:
logger.exception(
f"{self.__str__()} can only be used on the external "
+ "surfaces of a fractal box"
else:
raise ValueError(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
raise ValueError
fractalrange = (
round_value(limits[0] / grid.dy),
round_value(limits[1] / grid.dy),
)
elif ys == yf:
# yminus surface
if ys == volume.ys:
if fractalrange[0] < 0 or fractalrange[1] > volume.yf:
logger.exception(
f"{self.__str__()} cannot apply fractal surface "
"to fractal box as it would exceed either the "
"upper coordinates of the fractal box or the "
"domain in the y direction"
)
raise ValueError
lower_bound = discretised_p1
upper_bound = uip.discretise_point((p2[0], limits[1], p2[2]))
grid_bound = uip.discretise_point((p1[0], limits[0], p1[2]))
fractalrange = (grid_bound[1], upper_bound[1])
requestedsurface = "yminus"
# yplus surface
elif yf == volume.yf:
if fractalrange[0] < volume.ys or fractalrange[1] > grid.ny:
logger.exception(
f"{self.__str__()} cannot apply fractal surface "
"to fractal box as it would exceed either the "
"lower coordinates of the fractal box or the "
"domain in the y direction"
)
raise ValueError
lower_bound = uip.discretise_point((p1[0], limits[0], p1[2]))
upper_bound = discretised_p2
grid_bound = uip.discretise_point((p2[0], limits[1], p2[2]))
fractalrange = (lower_bound[1], grid_bound[1])
requestedsurface = "yplus"
elif zs == zf:
if zs not in [volume.zs, volume.zf]:
logger.exception(
f"{self.__str__()} can only be used on the external "
+ "surfaces of a fractal box"
else:
raise ValueError(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
raise ValueError
fractalrange = (
round_value(limits[0] / grid.dz),
round_value(limits[1] / grid.dz),
)
elif zs == zf:
# zminus surface
if zs == volume.zs:
if fractalrange[0] < 0 or fractalrange[1] > volume.zf:
logger.exception(
f"{self.__str__()} cannot apply fractal surface "
"to fractal box as it would exceed either the "
"upper coordinates of the fractal box or the "
"domain in the x direction"
)
raise ValueError
lower_bound = discretised_p1
upper_bound = uip.discretise_point((p2[0], p2[1], limits[1]))
grid_bound = uip.discretise_point((p1[0], p1[1], limits[0]))
fractalrange = (grid_bound[2], upper_bound[2])
requestedsurface = "zminus"
# zplus surface
elif zf == volume.zf:
if fractalrange[0] < volume.zs or fractalrange[1] > grid.nz:
logger.exception(
f"{self.__str__()} cannot apply fractal surface "
"to fractal box as it would exceed either the "
"lower coordinates of the fractal box or the "
"domain in the z direction"
)
raise ValueError
lower_bound = uip.discretise_point((p1[0], p1[1], limits[0]))
upper_bound = discretised_p2
grid_bound = uip.discretise_point((p2[0], p2[1], limits[1]))
fractalrange = (lower_bound[2], grid_bound[2])
requestedsurface = "zplus"
else:
raise ValueError(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
else:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
raise ValueError(f"{self.__str__()} dimensions are not specified correctly")
surface = FractalSurface(xs, xf, ys, yf, zs, zf, frac_dim, seed)
if any(lower_bound < volume.start):
raise ValueError(
f"{self.__str__()} cannot apply fractal surface to"
" fractal box as it would exceed the lower coordinates"
" of the fractal box."
)
elif any(upper_bound > volume.stop):
raise ValueError(
f"{self.__str__()} cannot apply fractal surface to"
" fractal box as it would exceed the upper coordinates"
" of the fractal box."
)
# Check lower or upper extent of the fractal surface (depending
# if the fractal surface has been applied in the minus or plus
# direction).
uip.point_within_bounds(grid_bound, f"{self.__str__()}")
surface = grid.create_fractal_surface(xs, xf, ys, yf, zs, zf, frac_dim, seed)
surface.surfaceID = requestedsurface
surface.fractalrange = fractalrange
surface.operatingonID = volume.ID
@@ -242,18 +207,21 @@ class AddSurfaceRoughness(RotatableMixin, GeometryUserObject):
# List of existing surfaces IDs
existingsurfaceIDs = [x.surfaceID for x in volume.fractalsurfaces]
if surface.surfaceID in existingsurfaceIDs:
logger.exception(
raise ValueError(
f"{self.__str__()} has already been used on the {surface.surfaceID} surface"
)
raise ValueError
surface.generate_fractal_surface()
volume.fractalsurfaces.append(surface)
p3 = uip.round_to_grid_static_point(p1)
p4 = uip.round_to_grid_static_point(p2)
logger.info(
f"{self.grid_name(grid)}Fractal surface from {xs * grid.dx:g}m, "
f"{ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, "
f"{yf * grid.dy:g}m, {zf * grid.dz:g}m with fractal dimension "
f"{self.grid_name(grid)}Fractal surface from {p3[0]:g}m, "
f"{p3[1]:g}m, {p3[2]:g}m, to {p4[0]:g}m, "
f"{p4[1]:g}m, {p4[2]:g}m with fractal dimension "
f"{surface.dimension:g}, fractal weightings {surface.weighting[0]:g}, "
f"{surface.weighting[1]:g}, fractal seeding {surface.seed}, "
f"and range {limits[0]:g}m to {limits[1]:g}m, added to "

查看文件

@@ -24,7 +24,6 @@ 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
@@ -77,80 +76,70 @@ class AddSurfaceWater(RotatableMixin, GeometryUserObject):
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}")
raise ValueError
raise ValueError(f"{self.__str__()} cannot find FractalBox {fractal_box_id}")
uip = self._create_uip(grid)
_, p1, p2 = uip.check_box_points(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
discretised_p1, discretised_p2 = uip.check_output_object_bounds(p1, p2, self.__str__())
xs, ys, zs = discretised_p1
xf, yf, zf = discretised_p2
if depth <= 0:
logger.exception(f"{self.__str__()} requires a positive value for the depth of water")
raise ValueError
raise ValueError(f"{self.__str__()} requires a positive value for the depth of water")
# Check for valid orientations
if np.count_nonzero(discretised_p1 == discretised_p2) != 1:
raise ValueError(f"{self.__str__()} dimensions are not specified correctly")
if xs == xf:
if ys == yf or zs == zf:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if xs not in [volume.xs, volume.xf]:
logger.exception(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
raise ValueError
# xminus surface
if xs == volume.xs:
requestedsurface = "xminus"
# xplus surface
elif xf == volume.xf:
requestedsurface = "xplus"
filldepthcells = round_value(depth / grid.dx)
filldepth = filldepthcells * grid.dx
elif ys == yf:
if zs == zf:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
if ys not in [volume.ys, volume.yf]:
logger.exception(
else:
raise ValueError(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
raise ValueError
filldepthcells = uip.discretise_point((depth, 0, 0))[0]
filldepth = uip.round_to_grid_static_point((depth, 0, 0))[0]
elif ys == yf:
# yminus surface
if ys == volume.ys:
requestedsurface = "yminus"
# yplus surface
elif yf == volume.yf:
requestedsurface = "yplus"
filldepthcells = round_value(depth / grid.dy)
filldepth = filldepthcells * grid.dy
elif zs == zf:
if zs not in [volume.zs, volume.zf]:
logger.exception(
else:
raise ValueError(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
raise ValueError
filldepthcells = uip.discretise_point((0, depth, 0))[1]
filldepth = uip.round_to_grid_static_point((0, depth, 0))[1]
elif zs == zf:
# zminus surface
if zs == volume.zs:
requestedsurface = "zminus"
# zplus surface
elif zf == volume.zf:
requestedsurface = "zplus"
filldepthcells = round_value(depth / grid.dz)
filldepth = filldepthcells * grid.dz
else:
raise ValueError(
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
)
filldepthcells = uip.discretise_point((0, 0, depth))[2]
filldepth = uip.round_to_grid_static_point((0, 0, depth))[2]
else:
logger.exception(f"{self.__str__()} dimensions are not specified correctly")
raise ValueError
raise ValueError(f"{self.__str__()} dimensions are not specified correctly")
surface = next((x for x in volume.fractalsurfaces if x.surfaceID == requestedsurface), None)
if not surface:
logger.exception(
raise ValueError(
f"{self.__str__()} specified surface {requestedsurface} does not have a rough surface applied"
)
raise ValueError
surface.filldepth = filldepthcells
@@ -159,11 +148,10 @@ class AddSurfaceWater(RotatableMixin, GeometryUserObject):
surface.filldepth < surface.fractalrange[0]
or surface.filldepth > surface.fractalrange[1]
):
logger.exception(
raise ValueError(
f"{self.__str__()} requires a value for the depth of water that lies with the "
f"range of the requested surface roughness"
)
raise ValueError
# Check to see if water has been already defined as a material
if not any(x.ID == "water" for x in grid.materials):
@@ -172,15 +160,17 @@ class AddSurfaceWater(RotatableMixin, GeometryUserObject):
# Check if time step for model is suitable for using water
water = next((x for x in grid.materials if x.ID == "water"))
if testwater := next((x for x in water.tau if x < grid.dt), None):
logger.exception(
raise ValueError(
f"{self.__str__()} requires the time step for the model "
f"to be less than the relaxation time required to model water."
)
raise ValueError
p3 = uip.round_to_grid_static_point(p1)
p4 = uip.round_to_grid_static_point(p2)
logger.info(
f"{self.grid_name(grid)}Water on surface from {xs * grid.dx:g}m, "
f"{ys * grid.dy:g}m, {zs * grid.dz:g}m, to {xf * grid.dx:g}m, "
f"{yf * grid.dy:g}m, {zf * grid.dz:g}m with depth {filldepth:g}m, "
f"added to {surface.operatingonID}."
f"{self.grid_name(grid)}Water on surface from {p3[0]:g}m,"
f" {p3[1]:g}m, {p3[2]:g}m, to {p4[0]:g}m, {p4[1]:g}m,"
f" {p4[2]:g}m with depth {filldepth:g}m, added to"
f" {surface.operatingonID}."
)

查看文件

@@ -22,7 +22,6 @@ import numpy as np
import gprMax.config as config
from gprMax.cython.geometry_primitives import build_voxels_from_array, build_voxels_from_array_mask
from gprMax.fractals import FractalVolume
from gprMax.grid.fdtd_grid import FDTDGrid
from gprMax.materials import ListMaterial
from gprMax.user_objects.cmds_geometry.cmds_geometry import check_averaging, rotate_2point_object
@@ -110,12 +109,7 @@ class FractalBox(RotatableMixin, GeometryUserObject):
p3 = uip.round_to_grid_static_point(p1)
p4 = uip.round_to_grid_static_point(p2)
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
p1, p2 = uip.check_output_object_bounds(p1, p2, self.__str__())
xs, ys, zs = p1
xf, yf, zf = p2
@@ -172,7 +166,7 @@ class FractalBox(RotatableMixin, GeometryUserObject):
)
raise ValueError
self.volume = FractalVolume(xs, xf, ys, yf, zs, zf, frac_dim, seed)
self.volume = grid.add_fractal_volume(xs, xf, ys, yf, zs, zf, frac_dim, seed)
self.volume.ID = ID
self.volume.operatingonID = mixing_model_id
self.volume.nbins = nbins
@@ -199,39 +193,26 @@ class FractalBox(RotatableMixin, GeometryUserObject):
self.do_pre_build = False
else:
if self.volume.fractalsurfaces:
self.volume.originalxs = self.volume.xs
self.volume.originalxf = self.volume.xf
self.volume.originalys = self.volume.ys
self.volume.originalyf = self.volume.yf
self.volume.originalzs = self.volume.zs
self.volume.originalzf = self.volume.zf
# Extend the volume to accomodate any rough surfaces, grass,
# or roots
for surface in self.volume.fractalsurfaces:
if surface.surfaceID == "xminus":
if surface.fractalrange[0] < self.volume.xs:
self.volume.nx += self.volume.xs - surface.fractalrange[0]
self.volume.xs = surface.fractalrange[0]
elif surface.surfaceID == "xplus":
if surface.fractalrange[1] > self.volume.xf:
self.volume.nx += surface.fractalrange[1] - self.volume.xf
self.volume.xf = surface.fractalrange[1]
elif surface.surfaceID == "yminus":
if surface.fractalrange[0] < self.volume.ys:
self.volume.ny += self.volume.ys - surface.fractalrange[0]
self.volume.ys = surface.fractalrange[0]
elif surface.surfaceID == "yplus":
if surface.fractalrange[1] > self.volume.yf:
self.volume.ny += surface.fractalrange[1] - self.volume.yf
self.volume.yf = surface.fractalrange[1]
elif surface.surfaceID == "zminus":
if surface.fractalrange[0] < self.volume.zs:
self.volume.nz += self.volume.zs - surface.fractalrange[0]
self.volume.zs = surface.fractalrange[0]
elif surface.surfaceID == "zplus":
if surface.fractalrange[1] > self.volume.zf:
self.volume.nz += surface.fractalrange[1] - self.volume.zf
self.volume.zf = surface.fractalrange[1]
# If there is only 1 bin then a normal material is being used,
@@ -246,7 +227,8 @@ class FractalBox(RotatableMixin, GeometryUserObject):
)
self.volume.fractalvolume *= materialnumID
else:
self.volume.generate_fractal_volume()
if not self.volume.generate_fractal_volume():
return
for i in range(0, self.volume.nx):
for j in range(0, self.volume.ny):
for k in range(0, self.volume.nz):
@@ -262,6 +244,10 @@ class FractalBox(RotatableMixin, GeometryUserObject):
# TODO: Allow extract of rough surface profile (to print/file?)
for surface in self.volume.fractalsurfaces:
if surface.surfaceID == "xminus":
surface.fractalrange = (
max(surface.fractalrange[0], 0),
min(surface.fractalrange[1], grid.nx),
)
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):
@@ -285,6 +271,10 @@ class FractalBox(RotatableMixin, GeometryUserObject):
] = 0
elif surface.surfaceID == "xplus":
surface.fractalrange = (
max(surface.fractalrange[0], 0),
min(surface.fractalrange[1], grid.nx),
)
if not surface.ID:
for i in range(surface.fractalrange[0], surface.fractalrange[1]):
for j in range(surface.ys, surface.yf):
@@ -393,6 +383,10 @@ class FractalBox(RotatableMixin, GeometryUserObject):
root += 1
elif surface.surfaceID == "yminus":
surface.fractalrange = (
max(surface.fractalrange[0], 0),
min(surface.fractalrange[1], grid.ny),
)
for i in range(surface.xs, surface.xf):
for j in range(surface.fractalrange[0], surface.fractalrange[1]):
for k in range(surface.zs, surface.zf):
@@ -416,6 +410,10 @@ class FractalBox(RotatableMixin, GeometryUserObject):
] = 0
elif surface.surfaceID == "yplus":
surface.fractalrange = (
max(surface.fractalrange[0], 0),
min(surface.fractalrange[1], grid.ny),
)
if not surface.ID:
for i in range(surface.xs, surface.xf):
for j in range(surface.fractalrange[0], surface.fractalrange[1]):
@@ -524,6 +522,10 @@ class FractalBox(RotatableMixin, GeometryUserObject):
root += 1
elif surface.surfaceID == "zminus":
surface.fractalrange = (
max(surface.fractalrange[0], 0),
min(surface.fractalrange[1], grid.nz),
)
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]):
@@ -547,6 +549,10 @@ class FractalBox(RotatableMixin, GeometryUserObject):
] = 0
elif surface.surfaceID == "zplus":
surface.fractalrange = (
max(surface.fractalrange[0], 0),
min(surface.fractalrange[1], grid.nz),
)
if not surface.ID:
for i in range(surface.xs, surface.xf):
for j in range(surface.ys, surface.yf):
@@ -685,7 +691,8 @@ class FractalBox(RotatableMixin, GeometryUserObject):
)
raise ValueError
else:
self.volume.generate_fractal_volume()
if not self.volume.generate_fractal_volume():
return
for i in range(0, self.volume.nx):
for j in range(0, self.volume.ny):
for k in range(0, self.volume.nz):

47
gprMax/utilities/mpi.py 普通文件
查看文件

@@ -0,0 +1,47 @@
from enum import IntEnum, unique
from typing import Union
import numpy as np
import numpy.typing as npt
from mpi4py import MPI
@unique
class Dim(IntEnum):
X = 0
Y = 1
Z = 2
@unique
class Dir(IntEnum):
NONE = -1
NEG = 0
POS = 1
def get_neighbours(comm: MPI.Cartcomm) -> npt.NDArray[np.int32]:
neighbours = np.full((3, 2), -1, dtype=np.int32)
neighbours[Dim.X] = comm.Shift(direction=Dim.X, disp=1)
neighbours[Dim.Y] = comm.Shift(direction=Dim.Y, disp=1)
neighbours[Dim.Z] = comm.Shift(direction=Dim.Z, disp=1)
return neighbours
def get_neighbour(comm: MPI.Cartcomm, dim: Dim, dir: Dir) -> int:
neighbours = comm.Shift(direction=dim, disp=1)
return neighbours[dir]
def get_relative_neighbour(
comm: MPI.Cartcomm, dirs: npt.NDArray[np.int32], disp: Union[int, npt.NDArray[np.int32]] = 1
) -> int:
offset = np.select([dirs == Dir.NEG, dirs == Dir.POS], [-disp, disp], default=0)
coord = comm.coords + offset
if any(coord < 0) or any(coord >= comm.dims):
return -1
return comm.Get_cart_rank(coord.tolist())

查看文件

@@ -11,12 +11,12 @@
#add_dispersion_debye: 1 8.6327 8.0994e-12 |10.5897+0.0769+1.0000+0.0000|
#material: 10.7105 0.0777078 1 0 |10.7105+0.0777+1.0000+0.0000|
#add_dispersion_debye: 1 8.9052 8.0994e-12 |10.7105+0.0777+1.0000+0.0000|
#material: 10.8308 0.0784642 1 0 |10.8308+0.0785+1.0000+0.0000|
#add_dispersion_debye: 1 9.18039 8.0994e-12 |10.8308+0.0785+1.0000+0.0000|
#material: 10.9505 0.0792123 1 0 |10.9505+0.0792+1.0000+0.0000|
#add_dispersion_debye: 1 9.45822 8.0994e-12 |10.9505+0.0792+1.0000+0.0000|
#material: 3.67252 0.0108321 1 0 |3.6725+0.0108+1.0000+0.0000|
#add_dispersion_debye: 1 0.0182374 8.0994e-12 |3.6725+0.0108+1.0000+0.0000|
#material: 3.91944 0.0163865 1 0 |3.9194+0.0164+1.0000+0.0000|
#add_dispersion_debye: 1 0.0669568 8.0994e-12 |3.9194+0.0164+1.0000+0.0000|
#material: 4.1448 0.0203355 1 0 |4.1448+0.0203+1.0000+0.0000|
#add_dispersion_debye: 1 0.131947 8.0994e-12 |4.1448+0.0203+1.0000+0.0000|
#material: 4.35657 0.0235572 1 0 |4.3566+0.0236+1.0000+0.0000|

查看文件

@@ -11,14 +11,12 @@
#add_dispersion_debye: 1 8.6327 8.0994e-12 |10.5897+0.0769+1.0000+0.0000|
#material: 10.7105 0.0777078 1 0 |10.7105+0.0777+1.0000+0.0000|
#add_dispersion_debye: 1 8.9052 8.0994e-12 |10.7105+0.0777+1.0000+0.0000|
#material: 10.8308 0.0784642 1 0 |10.8308+0.0785+1.0000+0.0000|
#add_dispersion_debye: 1 9.18039 8.0994e-12 |10.8308+0.0785+1.0000+0.0000|
#material: 10.9505 0.0792123 1 0 |10.9505+0.0792+1.0000+0.0000|
#add_dispersion_debye: 1 9.45822 8.0994e-12 |10.9505+0.0792+1.0000+0.0000|
#material: 3.67252 0.0108321 1 0 |3.6725+0.0108+1.0000+0.0000|
#add_dispersion_debye: 1 0.0182374 8.0994e-12 |3.6725+0.0108+1.0000+0.0000|
#material: 3.91944 0.0163865 1 0 |3.9194+0.0164+1.0000+0.0000|
#add_dispersion_debye: 1 0.0669568 8.0994e-12 |3.9194+0.0164+1.0000+0.0000|
#material: 4.1448 0.0203355 1 0 |4.1448+0.0203+1.0000+0.0000|
#add_dispersion_debye: 1 0.131947 8.0994e-12 |4.1448+0.0203+1.0000+0.0000|
#material: 4.35657 0.0235572 1 0 |4.3566+0.0236+1.0000+0.0000|
#add_dispersion_debye: 1 0.209447 8.0994e-12 |4.3566+0.0236+1.0000+0.0000|
#material: 4.55835 0.0263403 1 0 |4.5583+0.0263+1.0000+0.0000|

查看文件

@@ -29,7 +29,7 @@ else:
class ReceiverMixin(GprMaxMixin):
number_of_receivers = variable(int, value=-1)
@run_after("setup")
@run_after("setup", always_last=True)
def add_receiver_regression_checks(self):
reference_file = self.build_reference_filepath(self.output_file)

查看文件

@@ -8,6 +8,12 @@
#rx: 0.080 0.080 0.080
#soil_peplinski: 0.5 0.5 2.0 2.66 0.001 0.25 my_soil
#fractal_box: 0 0 0 0.05 0.05 0.1 1.5 1 1 1 50 my_soil my_soil_box 42
#fractal_box: 0 0 0 0.049 0.05 0.051 1.5 1 1 1 50 my_soil box_1 42
#fractal_box: 0.049 0.05 0.051 0.1 0.1 0.1 1.5 1 1 1 50 my_soil box_2 22
#add_surface_roughness: 0.049 0 0 0.049 0.05 0.05 1 1 1 0.025 0.05 box_1 43
#add_surface_roughness: 0.049 0.05 0.051 0.049 0.1 0.1 1 1 1 0.049 0.074 box_2 23
#geometry_objects_write: 0 0 0 0.1 0.1 0.1 full_volume

查看文件

@@ -8,7 +8,7 @@
#rx: 0.080 0.080 0.080
#soil_peplinski: 0.5 0.5 2.0 2.66 0.001 0.25 my_soil
#fractal_box: 0 0 0 0.05 0.05 0.1 1.5 1 1 1 50 my_soil my_soil_box 42
#add_surface_roughness: 0 0 0.1 0.05 0.05 0.1 1 1 1 0.05 0.1 my_soil_box 22
#fractal_box: 0 0 0 0.049 0.05 0.051 1.5 1 1 1 50 my_soil my_soil_box 42
#fractal_box: 0.049 0.05 0.051 0.1 0.1 0.1 1.5 1 1 1 50 my_soil my_soil_box 22
#geometry_objects_write: 0 0 0 0.1 0.1 0.1 full_volume

查看文件

@@ -8,6 +8,13 @@ from reframe_tests.tests.standard_tests import GprMaxGeometryTest, GprMaxRegress
"""
@rfm.simple_test
class TestAddGrass(GprMaxGeometryTest):
tags = {"test", "serial", "geometery", "fractal", "surface", "grass"}
sourcesdir = "src/geometry_tests/add_grass_geometry"
model = parameter(["add_grass_full", "add_grass_small"])
@rfm.simple_test
class TestAddSurfaceRoughness(GprMaxGeometryTest):
tags = {"test", "serial", "geometery", "fractal", "surface", "roughness"}
@@ -15,9 +22,9 @@ class TestAddSurfaceRoughness(GprMaxGeometryTest):
model = parameter(
[
"add_surface_roughness_full",
"add_surface_roughness_half",
"add_surface_roughness_small",
"add_surface_roughness_weighted",
"add_surface_roughness_boundaries",
]
)
@@ -29,13 +36,6 @@ class TestAddSurfaceWater(GprMaxGeometryTest):
model = parameter(["add_surface_water_full", "add_surface_water_small"])
@rfm.simple_test
class TestAddGrass(GprMaxGeometryTest):
tags = {"test", "serial", "geometery", "fractal", "surface", "grass"}
sourcesdir = "src/geometry_tests/add_grass_geometry"
model = parameter(["add_grass_full", "add_grass_small"])
@rfm.simple_test
class TestBoxGeometryDefaultPml(GprMaxRegressionTest):
tags = {"test", "serial", "geometery", "box"}
@@ -130,7 +130,12 @@ class TestFractalBoxGeometry(GprMaxGeometryTest):
tags = {"test", "serial", "geometery", "fractal", "box", "fractal_box"}
sourcesdir = "src/geometry_tests/fractal_box_geometry"
model = parameter(
["fractal_box_full", "fractal_box_half", "fractal_box_small", "fractal_box_weighted"]
[
"fractal_box_full",
"fractal_box_small",
"fractal_box_weighted",
"fractal_box_boundaries",
]
)
@@ -160,6 +165,64 @@ class TestTriangleGeometry(GprMaxRegressionTest):
"""
@rfm.simple_test
class TestAddGrassMpi(MpiMixin, TestAddGrass):
tags = {"test", "mpi", "geometery", "fractal", "surface", "grass"}
mpi_layout = parameter([[1, 2, 2], [3, 1, 3], [4, 1, 4]])
test_dependency = TestAddGrass
# Depending on the orientation/alignment of the fractal surface, not all
# MPI decompositions are valid. Therefore create tests that divide the
# domain in each pair of dimensions.
@rfm.simple_test
class TestAddSurfaceRoughnessMpiYZ(MpiMixin, TestAddSurfaceRoughness):
tags = {"test", "mpi", "geometery", "fractal", "surface", "roughness"}
mpi_layout = parameter([[1, 2, 2], [1, 3, 3], [1, 4, 4]])
test_dependency = TestAddSurfaceRoughness
model = parameter(
[
"add_surface_roughness_full",
"add_surface_roughness_small",
"add_surface_roughness_weighted",
]
)
@rfm.simple_test
class TestAddSurfaceRoughnessMpiXZ(MpiMixin, TestAddSurfaceRoughness):
tags = {"test", "mpi", "geometery", "fractal", "surface", "roughness"}
mpi_layout = parameter([[2, 1, 2], [3, 1, 3], [4, 1, 4]])
test_dependency = TestAddSurfaceRoughness
model = parameter(
[
"add_surface_roughness_full",
"add_surface_roughness_weighted",
"add_surface_roughness_boundaries",
]
)
@rfm.simple_test
class TestAddSurfaceRoughnessMpiXY(MpiMixin, TestAddSurfaceRoughness):
tags = {"test", "mpi", "geometery", "fractal", "surface", "roughness"}
mpi_layout = parameter([[2, 2, 1], [3, 3, 1], [4, 4, 1]])
test_dependency = TestAddSurfaceRoughness
model = parameter(
[
"add_surface_roughness_small",
"add_surface_roughness_boundaries",
]
)
@rfm.simple_test
class TestAddSurfaceWaterMpi(MpiMixin, TestAddSurfaceWater):
tags = {"test", "mpi", "geometery", "fractal", "surface", "roughness", "water"}
mpi_layout = parameter([[1, 2, 2], [3, 1, 3], [4, 1, 4]])
test_dependency = TestAddSurfaceWater
@rfm.simple_test
class TestBoxGeometryDefaultPmlMpi(MpiMixin, TestBoxGeometryDefaultPml):
tags = {"test", "mpi", "geometery", "box"}
@@ -219,10 +282,28 @@ class TestEllipsoidGeometryMpi(MpiMixin, TestEllipsoidGeometry):
@rfm.simple_test
class TestFractalBoxGeometryMpi(MpiMixin, TestFractalBoxGeometry):
tags = {"test", "mpi", "geometery", "fractal", "box", "fractal_box"}
mpi_layout = parameter([[2, 2, 2], [3, 3, 3], [4, 4, 4]])
mpi_layout = parameter([[2, 2, 1], [1, 3, 3], [4, 1, 4]])
test_dependency = TestFractalBoxGeometry
@rfm.simple_test
class TestFractalBoxGeometryMpiDecomposition(MpiMixin, TestFractalBoxGeometry):
"""Extra Fractal Box test.
This tests that so long as the area covered by the fractal box has
an MPI decomposition of 1 in a dimension, the model will run
successfully.
N.B: These tests would fail if run using the 'fractal_box_full'
model.
"""
tags = {"test", "mpi", "geometery", "fractal", "box", "fractal_box"}
mpi_layout = parameter([[2, 4, 4], [4, 2, 4], [4, 4, 2]])
test_dependency = TestFractalBoxGeometry
model = parameter(["fractal_box_small"])
@rfm.simple_test
class TestPlateGeometryMpi(MpiMixin, TestPlateGeometry):
tags = {"test", "mpi", "geometery", "plate"}

查看文件

@@ -9,6 +9,7 @@ jinja2
jupyter
matplotlib
# mpi4py
mpi4py-fft
numpy<2
numpy-stl
pandas