你已经派生过 gprMax
镜像自地址
https://gitee.com/sunhf/gprMax.git
已同步 2025-08-08 07:24:19 +08:00
Merge branch 'refactor-create-internal-objects' into mpi
这个提交包含在:
@@ -103,7 +103,9 @@ class AddGrass(UserObjectGeometry):
|
||||
xf, yf, zf = p2
|
||||
|
||||
if frac_dim < 0:
|
||||
logger.exception(f"{self.__str__()} requires a positive value for the fractal dimension")
|
||||
logger.exception(
|
||||
f"{self.__str__()} requires a positive value for the fractal dimension"
|
||||
)
|
||||
raise ValueError
|
||||
if limits[0] < 0 or limits[1] < 0:
|
||||
logger.exception(
|
||||
@@ -117,7 +119,9 @@ class AddGrass(UserObjectGeometry):
|
||||
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")
|
||||
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
|
||||
@@ -142,7 +146,9 @@ class AddGrass(UserObjectGeometry):
|
||||
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")
|
||||
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
|
||||
@@ -164,7 +170,9 @@ class AddGrass(UserObjectGeometry):
|
||||
|
||||
elif zs == zf:
|
||||
if zs not in [volume.zs, volume.zf]:
|
||||
logger.exception(f"{self.__str__()} must specify external surfaces on a fractal box")
|
||||
logger.exception(
|
||||
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))
|
||||
# zminus surface
|
||||
@@ -219,7 +227,8 @@ class AddGrass(UserObjectGeometry):
|
||||
# probability values, and convert the 1D index back into a x, y index
|
||||
# for the original surface.
|
||||
bladesindex = np.unravel_index(
|
||||
np.digitize(A, probability1D), (surface.fractalsurface.shape[0], surface.fractalsurface.shape[1])
|
||||
np.digitize(A, probability1D),
|
||||
(surface.fractalsurface.shape[0], surface.fractalsurface.shape[1]),
|
||||
)
|
||||
|
||||
# Set the fractal range to minimum and maximum heights of the grass blades
|
||||
@@ -227,7 +236,9 @@ class AddGrass(UserObjectGeometry):
|
||||
|
||||
# Set the fractal surface using the pre-calculated spatial distribution
|
||||
# and a random height
|
||||
surface.fractalsurface = np.zeros((surface.fractalsurface.shape[0], surface.fractalsurface.shape[1]))
|
||||
surface.fractalsurface = np.zeros(
|
||||
(surface.fractalsurface.shape[0], surface.fractalsurface.shape[1])
|
||||
)
|
||||
for i in range(len(bladesindex[0])):
|
||||
surface.fractalsurface[bladesindex[0][i], bladesindex[1][i]] = R.randint(
|
||||
surface.fractalrange[0], surface.fractalrange[1], size=1
|
||||
|
@@ -103,7 +103,9 @@ class AddSurfaceRoughness(UserObjectGeometry):
|
||||
xf, yf, zf = p2
|
||||
|
||||
if frac_dim < 0:
|
||||
logger.exception(f"{self.__str__()} requires a positive value for the " + "fractal dimension")
|
||||
logger.exception(
|
||||
f"{self.__str__()} requires a positive value for the " + "fractal dimension"
|
||||
)
|
||||
raise ValueError
|
||||
if weighting[0] < 0:
|
||||
logger.exception(
|
||||
@@ -124,7 +126,9 @@ class AddSurfaceRoughness(UserObjectGeometry):
|
||||
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")
|
||||
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
|
||||
@@ -155,7 +159,10 @@ class AddSurfaceRoughness(UserObjectGeometry):
|
||||
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")
|
||||
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.dy), round_value(limits[1] / grid.dy))
|
||||
# yminus surface
|
||||
@@ -183,7 +190,10 @@ class AddSurfaceRoughness(UserObjectGeometry):
|
||||
|
||||
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")
|
||||
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.dz), round_value(limits[1] / grid.dz))
|
||||
# zminus surface
|
||||
@@ -222,7 +232,9 @@ class AddSurfaceRoughness(UserObjectGeometry):
|
||||
# List of existing surfaces IDs
|
||||
existingsurfaceIDs = [x.surfaceID for x in volume.fractalsurfaces]
|
||||
if surface.surfaceID in existingsurfaceIDs:
|
||||
logger.exception(f"{self.__str__()} has already been used on the {surface.surfaceID} surface")
|
||||
logger.exception(
|
||||
f"{self.__str__()} has already been used on the {surface.surfaceID} surface"
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
surface.generate_fractal_surface()
|
||||
|
@@ -94,7 +94,9 @@ class AddSurfaceWater(UserObjectGeometry):
|
||||
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")
|
||||
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:
|
||||
@@ -110,7 +112,9 @@ class AddSurfaceWater(UserObjectGeometry):
|
||||
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")
|
||||
logger.exception(
|
||||
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
|
||||
)
|
||||
raise ValueError
|
||||
# yminus surface
|
||||
if ys == volume.ys:
|
||||
@@ -123,7 +127,9 @@ class AddSurfaceWater(UserObjectGeometry):
|
||||
|
||||
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")
|
||||
logger.exception(
|
||||
f"{self.__str__()} can only be used on the external surfaces of a fractal box"
|
||||
)
|
||||
raise ValueError
|
||||
# zminus surface
|
||||
if zs == volume.zs:
|
||||
@@ -148,7 +154,10 @@ class AddSurfaceWater(UserObjectGeometry):
|
||||
surface.filldepth = filldepthcells
|
||||
|
||||
# Check that requested fill depth falls within range of surface roughness
|
||||
if surface.filldepth < surface.fractalrange[0] or surface.filldepth > surface.fractalrange[1]:
|
||||
if (
|
||||
surface.filldepth < surface.fractalrange[0]
|
||||
or surface.filldepth > surface.fractalrange[1]
|
||||
):
|
||||
logger.exception(
|
||||
f"{self.__str__()} requires a value for the depth of water that lies with the "
|
||||
f"range of the requested surface roughness"
|
||||
|
@@ -23,7 +23,6 @@ import numpy as np
|
||||
import gprMax.config as config
|
||||
|
||||
from ..cython.geometry_primitives import build_box
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry, rotate_2point_object
|
||||
|
||||
@@ -88,7 +87,6 @@ class Box(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averagebox = self.kwargs["averaging"]
|
||||
averagebox = check_averaging(averagebox)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averagebox = grid.averagevolumeobjects
|
||||
|
@@ -21,7 +21,6 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from ..cython.geometry_primitives import build_cone
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry
|
||||
|
||||
@@ -63,7 +62,6 @@ class Cone(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averagecone = self.kwargs["averaging"]
|
||||
averagecone = check_averaging(averagecone)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averagecone = grid.averagevolumeobjects
|
||||
@@ -87,11 +85,15 @@ class Cone(UserObjectGeometry):
|
||||
x2, y2, z2 = uip.round_to_grid(p2)
|
||||
|
||||
if r1 < 0:
|
||||
logger.exception(f"{self.__str__()} the radius of the first face {r1:g} should be a positive value.")
|
||||
logger.exception(
|
||||
f"{self.__str__()} the radius of the first face {r1:g} should be a positive value."
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
if r2 < 0:
|
||||
logger.exception(f"{self.__str__()} the radius of the second face {r2:g} should be a positive value.")
|
||||
logger.exception(
|
||||
f"{self.__str__()} the radius of the second face {r2:g} should be a positive value."
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
if r1 == 0 and r2 == 0:
|
||||
|
@@ -21,7 +21,6 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from ..cython.geometry_primitives import build_cylinder
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry
|
||||
|
||||
@@ -60,7 +59,6 @@ class Cylinder(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averagecylinder = self.kwargs["averaging"]
|
||||
averagecylinder = check_averaging(averagecylinder)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averagecylinder = grid.averagevolumeobjects
|
||||
|
@@ -21,7 +21,6 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from ..cython.geometry_primitives import build_cylindrical_sector
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry
|
||||
|
||||
@@ -75,7 +74,6 @@ class CylindricalSector(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averagecylindricalsector = self.kwargs["averaging"]
|
||||
averagecylindricalsector = check_averaging(averagecylindricalsector)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averagecylindricalsector = grid.averagevolumeobjects
|
||||
@@ -101,10 +99,14 @@ class CylindricalSector(UserObjectGeometry):
|
||||
if r <= 0:
|
||||
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.")
|
||||
logger.exception(
|
||||
f"{self.__str__()} the starting angle and sector angle should be a positive values."
|
||||
)
|
||||
raise ValueError
|
||||
if sectorstartangle >= 2 * np.pi or sectorangle >= 2 * np.pi:
|
||||
logger.exception(f"{self.__str__()} the starting angle and sector angle must be less than 360 degrees.")
|
||||
logger.exception(
|
||||
f"{self.__str__()} the starting angle and sector angle must be less than 360 degrees."
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
# Look up requested materials in existing list of material instances
|
||||
|
@@ -21,7 +21,6 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from ..cython.geometry_primitives import build_ellipsoid
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry
|
||||
|
||||
@@ -61,7 +60,6 @@ class Ellipsoid(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averageellipsoid = self.kwargs["averaging"]
|
||||
averageellipsoid = check_averaging(averageellipsoid)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averageellipsoid = grid.averagevolumeobjects
|
||||
|
@@ -116,16 +116,24 @@ class FractalBox(UserObjectGeometry):
|
||||
xf, yf, zf = p2
|
||||
|
||||
if frac_dim < 0:
|
||||
logger.exception(f"{self.__str__()} requires a positive value for the fractal dimension")
|
||||
logger.exception(
|
||||
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 x direction")
|
||||
logger.exception(
|
||||
f"{self.__str__()} requires a positive value for the fractal weighting in the x direction"
|
||||
)
|
||||
raise ValueError
|
||||
if weighting[1] < 0:
|
||||
logger.exception(f"{self.__str__()} requires a positive value for the fractal weighting in the y direction")
|
||||
logger.exception(
|
||||
f"{self.__str__()} requires a positive value for the fractal weighting in the y direction"
|
||||
)
|
||||
raise ValueError
|
||||
if weighting[2] < 0:
|
||||
logger.exception(f"{self.__str__()} requires a positive value for the fractal weighting in the z direction")
|
||||
logger.exception(
|
||||
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")
|
||||
raise ValueError
|
||||
@@ -138,18 +146,25 @@ class FractalBox(UserObjectGeometry):
|
||||
|
||||
if mixingmodel:
|
||||
if nbins == 1:
|
||||
logger.exception(f"{self.__str__()} must be used with more than one material from the mixing model.")
|
||||
logger.exception(
|
||||
f"{self.__str__()} must be used with more than one material from the mixing model."
|
||||
)
|
||||
raise ValueError
|
||||
if isinstance(mixingmodel, ListMaterial) and nbins > len(mixingmodel.mat):
|
||||
logger.exception(
|
||||
f"{self.__str__()} too many materials/bins " "requested compared to materials in " "mixing model."
|
||||
f"{self.__str__()} too many materials/bins "
|
||||
"requested compared to materials in "
|
||||
"mixing model."
|
||||
)
|
||||
raise ValueError
|
||||
# Create materials from mixing model as number of bins now known
|
||||
# from fractal_box command.
|
||||
mixingmodel.calculate_properties(nbins, grid)
|
||||
elif not material:
|
||||
logger.exception(f"{self.__str__()} mixing model or material with " + "ID {mixing_model_id} does not exist")
|
||||
logger.exception(
|
||||
f"{self.__str__()} mixing model or material with "
|
||||
+ "ID {mixing_model_id} does not exist"
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
self.volume = FractalVolume(xs, xf, ys, yf, zs, zf, frac_dim, seed)
|
||||
@@ -221,7 +236,9 @@ class FractalBox(UserObjectGeometry):
|
||||
(self.volume.nx, self.volume.ny, self.volume.nz),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
materialnumID = next(x.numID for x in grid.materials if x.ID == self.volume.operatingonID)
|
||||
materialnumID = next(
|
||||
x.numID for x in grid.materials if x.ID == self.volume.operatingonID
|
||||
)
|
||||
self.volume.fractalvolume *= materialnumID
|
||||
else:
|
||||
self.volume.generate_fractal_volume()
|
||||
@@ -229,7 +246,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()
|
||||
|
||||
@@ -242,28 +261,49 @@ class FractalBox(UserObjectGeometry):
|
||||
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]:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 1
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 1
|
||||
elif surface.filldepth > 0 and i > surface.filldepth:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 2
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 2
|
||||
else:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 0
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 0
|
||||
|
||||
elif surface.surfaceID == "xplus":
|
||||
if not surface.ID:
|
||||
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
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 1
|
||||
elif surface.filldepth > 0 and i < surface.filldepth:
|
||||
self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 2
|
||||
else:
|
||||
self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 0
|
||||
elif surface.ID == "grass":
|
||||
g = surface.grass[0]
|
||||
@@ -275,9 +315,14 @@ class FractalBox(UserObjectGeometry):
|
||||
height = 0
|
||||
for i in range(self.volume.xs, surface.fractalrange[1]):
|
||||
if (
|
||||
i < surface.fractalsurface[j - surface.ys, k - surface.zs]
|
||||
i
|
||||
< surface.fractalsurface[
|
||||
j - surface.ys, k - surface.zs
|
||||
]
|
||||
and self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
]
|
||||
!= 1
|
||||
):
|
||||
@@ -311,11 +356,15 @@ class FractalBox(UserObjectGeometry):
|
||||
i
|
||||
> self.volume.originalxf
|
||||
- (
|
||||
surface.fractalsurface[j - surface.ys, k - surface.zs]
|
||||
surface.fractalsurface[
|
||||
j - surface.ys, k - surface.zs
|
||||
]
|
||||
- self.volume.originalxf
|
||||
)
|
||||
and self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
]
|
||||
== 1
|
||||
):
|
||||
@@ -343,28 +392,49 @@ class FractalBox(UserObjectGeometry):
|
||||
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]:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 1
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 1
|
||||
elif surface.filldepth > 0 and j > surface.filldepth:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 2
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 2
|
||||
else:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 0
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 0
|
||||
|
||||
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 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
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 1
|
||||
elif surface.filldepth > 0 and j < surface.filldepth:
|
||||
self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 2
|
||||
else:
|
||||
self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 0
|
||||
elif surface.ID == "grass":
|
||||
g = surface.grass[0]
|
||||
@@ -376,9 +446,14 @@ class FractalBox(UserObjectGeometry):
|
||||
height = 0
|
||||
for j in range(self.volume.ys, surface.fractalrange[1]):
|
||||
if (
|
||||
j < surface.fractalsurface[i - surface.xs, k - surface.zs]
|
||||
j
|
||||
< surface.fractalsurface[
|
||||
i - surface.xs, k - surface.zs
|
||||
]
|
||||
and self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
]
|
||||
!= 1
|
||||
):
|
||||
@@ -412,11 +487,15 @@ class FractalBox(UserObjectGeometry):
|
||||
j
|
||||
> self.volume.originalyf
|
||||
- (
|
||||
surface.fractalsurface[i - surface.xs, k - surface.zs]
|
||||
surface.fractalsurface[
|
||||
i - surface.xs, k - surface.zs
|
||||
]
|
||||
- self.volume.originalyf
|
||||
)
|
||||
and self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
]
|
||||
== 1
|
||||
):
|
||||
@@ -444,28 +523,51 @@ class FractalBox(UserObjectGeometry):
|
||||
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]:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 1
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 1
|
||||
elif surface.filldepth > 0 and k > surface.filldepth:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 2
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 2
|
||||
else:
|
||||
self.volume.mask[i - self.volume.xs, j - self.volume.ys, k - self.volume.zs] = 0
|
||||
self.volume.mask[
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 0
|
||||
|
||||
elif surface.surfaceID == "zplus":
|
||||
if not surface.ID:
|
||||
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
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 1
|
||||
elif surface.filldepth > 0 and k < surface.filldepth:
|
||||
self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 2
|
||||
else:
|
||||
self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
] = 0
|
||||
elif surface.ID == "grass":
|
||||
g = surface.grass[0]
|
||||
@@ -477,9 +579,14 @@ class FractalBox(UserObjectGeometry):
|
||||
height = 0
|
||||
for k in range(self.volume.zs, surface.fractalrange[1]):
|
||||
if (
|
||||
k < surface.fractalsurface[i - surface.xs, j - surface.ys]
|
||||
k
|
||||
< surface.fractalsurface[
|
||||
i - surface.xs, j - surface.ys
|
||||
]
|
||||
and self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
]
|
||||
!= 1
|
||||
):
|
||||
@@ -513,11 +620,15 @@ class FractalBox(UserObjectGeometry):
|
||||
k
|
||||
> self.volume.originalzf
|
||||
- (
|
||||
surface.fractalsurface[i - surface.xs, j - surface.ys]
|
||||
surface.fractalsurface[
|
||||
i - surface.xs, j - surface.ys
|
||||
]
|
||||
- self.volume.originalzf
|
||||
)
|
||||
and self.volume.mask[
|
||||
i - self.volume.xs, j - self.volume.ys, k - self.volume.zs
|
||||
i - self.volume.xs,
|
||||
j - self.volume.ys,
|
||||
k - self.volume.zs,
|
||||
]
|
||||
== 1
|
||||
):
|
||||
@@ -575,7 +686,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(
|
||||
|
@@ -126,12 +126,18 @@ class GeometryObjectsRead(UserObjectGeometry):
|
||||
rigidE = f["/rigidE"][:]
|
||||
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
|
||||
grid.solid[
|
||||
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]
|
||||
] = rigidE
|
||||
grid.rigidH[
|
||||
:, 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.rigidE[:, 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]] = rigidH
|
||||
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, "
|
||||
|
@@ -21,7 +21,6 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from ..cython.geometry_primitives import build_sphere
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry
|
||||
|
||||
@@ -56,7 +55,6 @@ class Sphere(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averagesphere = self.kwargs["averaging"]
|
||||
averagesphere = check_averaging(averagesphere)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averagesphere = grid.averagevolumeobjects
|
||||
|
@@ -21,7 +21,6 @@ import logging
|
||||
import numpy as np
|
||||
|
||||
from ..cython.geometry_primitives import build_triangle
|
||||
from ..hash_cmds_geometry import check_averaging
|
||||
from ..materials import Material
|
||||
from .cmds_geometry import UserObjectGeometry, rotate_point
|
||||
|
||||
@@ -81,7 +80,6 @@ class Triangle(UserObjectGeometry):
|
||||
try:
|
||||
# Try user-specified averaging
|
||||
averagetriangularprism = self.kwargs["averaging"]
|
||||
averagetriangularprism = check_averaging(averagetriangularprism)
|
||||
except KeyError:
|
||||
# Otherwise go with the grid default
|
||||
averagetriangularprism = grid.averagevolumeobjects
|
||||
|
@@ -26,6 +26,7 @@ from scipy import interpolate
|
||||
|
||||
import gprMax.config as config
|
||||
from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
from gprMax.model import Model
|
||||
from gprMax.user_inputs import MainGridUserInput
|
||||
|
||||
from .cmds_geometry.cmds_geometry import (
|
||||
@@ -74,11 +75,11 @@ class UserObjectMulti(ABC):
|
||||
return f"{self.hash}: {s[:-1]}"
|
||||
|
||||
@abstractmethod
|
||||
def build(self, grid: FDTDGrid, uip: MainGridUserInput):
|
||||
"""Creates object and adds it to grid."""
|
||||
def build(self, model: Model, uip: MainGridUserInput):
|
||||
"""Creates object and adds it to model."""
|
||||
pass
|
||||
|
||||
# TODO: Check if this is actually needed
|
||||
# TODO: Make _do_rotate not use a grid object
|
||||
def rotate(self, axis, angle, origin=None):
|
||||
"""Rotates object (specialised for each object)."""
|
||||
pass
|
||||
@@ -87,16 +88,19 @@ class UserObjectMulti(ABC):
|
||||
"""Readable string of parameters given to object."""
|
||||
return f"{self.hash}: {str(self.kwargs)}"
|
||||
|
||||
def grid_name(self, grid):
|
||||
def grid_name(self, grid: FDTDGrid) -> str:
|
||||
"""Returns subgrid name for use with logging info. Returns an empty
|
||||
string if the grid is the main grid.
|
||||
"""
|
||||
|
||||
if isinstance(grid, SubGridBaseGrid):
|
||||
return f"[{grid.name}] "
|
||||
else:
|
||||
return ""
|
||||
|
||||
def model_name(self, model: Model) -> str:
|
||||
"""Returns model name for use with logging info."""
|
||||
return f"[{model.title}] "
|
||||
|
||||
|
||||
class ExcitationFile(UserObjectMulti):
|
||||
"""An ASCII file that contains columns of amplitude values that specify
|
||||
@@ -114,7 +118,7 @@ class ExcitationFile(UserObjectMulti):
|
||||
self.order = 1
|
||||
self.hash = "#excitation_file"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
kwargs = {}
|
||||
excitationfile = self.kwargs["filepath"]
|
||||
@@ -136,6 +140,7 @@ class ExcitationFile(UserObjectMulti):
|
||||
if not excitationfile.exists():
|
||||
excitationfile = Path(config.sim_config.input_file_path.parent, excitationfile)
|
||||
|
||||
grid = uip.grid
|
||||
logger.info(self.grid_name(grid) + f"Excitation file: {excitationfile}")
|
||||
|
||||
# Get waveform names
|
||||
@@ -153,7 +158,7 @@ class ExcitationFile(UserObjectMulti):
|
||||
waveformvalues = waveformvalues[:, 1:]
|
||||
timestr = "user-defined time array"
|
||||
else:
|
||||
waveformtime = np.arange(0, grid.timewindow + grid.dt, grid.dt)
|
||||
waveformtime = np.arange(0, model.timewindow + grid.dt, grid.dt)
|
||||
timestr = "simulation time array"
|
||||
|
||||
for i, waveformID in enumerate(waveformIDs):
|
||||
@@ -213,7 +218,7 @@ class Waveform(UserObjectMulti):
|
||||
self.order = 2
|
||||
self.hash = "#waveform"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
wavetype = self.kwargs["wave_type"].lower()
|
||||
except KeyError:
|
||||
@@ -227,6 +232,7 @@ class Waveform(UserObjectMulti):
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
grid = uip.grid
|
||||
if wavetype != "user":
|
||||
try:
|
||||
amp = self.kwargs["amp"]
|
||||
@@ -234,7 +240,7 @@ class Waveform(UserObjectMulti):
|
||||
ID = self.kwargs["id"]
|
||||
except KeyError:
|
||||
logger.exception(
|
||||
self.params_str() + (" builtin waveforms " "require exactly four parameters.")
|
||||
self.params_str() + (" builtin waveforms require exactly four parameters.")
|
||||
)
|
||||
raise
|
||||
if freq <= 0:
|
||||
@@ -244,7 +250,7 @@ class Waveform(UserObjectMulti):
|
||||
)
|
||||
raise ValueError
|
||||
if any(x.ID == ID for x in grid.waveforms):
|
||||
logger.exception(self.params_str() + (f" with ID {ID} already " "exists."))
|
||||
logger.exception(self.params_str() + (f" with ID {ID} already exists."))
|
||||
raise ValueError
|
||||
|
||||
w = WaveformUser()
|
||||
@@ -271,14 +277,14 @@ class Waveform(UserObjectMulti):
|
||||
except KeyError:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" a user-defined " "waveform requires at least two parameters.")
|
||||
+ (" a user-defined waveform requires at least two parameters.")
|
||||
)
|
||||
raise
|
||||
|
||||
if "user_time" in self.kwargs:
|
||||
waveformtime = self.kwargs["user_time"]
|
||||
else:
|
||||
waveformtime = np.arange(0, grid.timewindow + grid.dt, grid.dt)
|
||||
waveformtime = np.arange(0, model.timewindow + grid.dt, grid.dt)
|
||||
|
||||
# Set args for interpolation if given by user
|
||||
if "kind" in self.kwargs:
|
||||
@@ -287,7 +293,7 @@ class Waveform(UserObjectMulti):
|
||||
kwargs["fill_value"] = self.kwargs["fill_value"]
|
||||
|
||||
if any(x.ID == ID for x in grid.waveforms):
|
||||
logger.exception(self.params_str() + (f" with ID {ID} already " "exists."))
|
||||
logger.exception(self.params_str() + (f" with ID {ID} already exists."))
|
||||
raise ValueError
|
||||
|
||||
w = WaveformUser()
|
||||
@@ -295,9 +301,7 @@ class Waveform(UserObjectMulti):
|
||||
w.type = wavetype
|
||||
w.userfunc = interpolate.interp1d(waveformtime, uservalues, **kwargs)
|
||||
|
||||
logger.info(
|
||||
self.grid_name(grid) + (f"Waveform {w.ID} that is " "user-defined created.")
|
||||
)
|
||||
logger.info(self.grid_name(grid) + (f"Waveform {w.ID} that is user-defined created."))
|
||||
|
||||
grid.waveforms.append(w)
|
||||
|
||||
@@ -335,40 +339,41 @@ class VoltageSource(UserObjectMulti):
|
||||
rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin)
|
||||
self.kwargs["p1"] = tuple(rot_pts[0, :])
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
p1 = self.kwargs["p1"]
|
||||
polarisation = self.kwargs["polarisation"].lower()
|
||||
resistance = self.kwargs["resistance"]
|
||||
waveform_id = self.kwargs["waveform_id"]
|
||||
except KeyError:
|
||||
logger.exception(self.params_str() + (" requires at least six " "parameters."))
|
||||
logger.exception(self.params_str() + (" requires at least six parameters."))
|
||||
raise
|
||||
|
||||
grid = uip.grid
|
||||
if self.do_rotate:
|
||||
self._do_rotate(grid)
|
||||
|
||||
# Check polarity & position parameters
|
||||
if polarisation not in ("x", "y", "z"):
|
||||
logger.exception(self.params_str() + (" polarisation must be " "x, y, or z."))
|
||||
logger.exception(self.params_str() + (" polarisation must be x, y, or z."))
|
||||
raise ValueError
|
||||
if "2D TMx" in config.get_model_config().mode and polarisation in [
|
||||
"y",
|
||||
"z",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be x in " "2D TMx mode."))
|
||||
logger.exception(self.params_str() + (" polarisation must be x in 2D TMx mode."))
|
||||
raise ValueError
|
||||
elif "2D TMy" in config.get_model_config().mode and polarisation in [
|
||||
"x",
|
||||
"z",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be y in " "2D TMy mode."))
|
||||
logger.exception(self.params_str() + (" polarisation must be y in 2D TMy mode."))
|
||||
raise ValueError
|
||||
elif "2D TMz" in config.get_model_config().mode and polarisation in [
|
||||
"x",
|
||||
"y",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be z in " "2D TMz mode."))
|
||||
logger.exception(self.params_str() + (" polarisation must be z in 2D TMz mode."))
|
||||
raise ValueError
|
||||
|
||||
xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str())
|
||||
@@ -376,14 +381,14 @@ class VoltageSource(UserObjectMulti):
|
||||
|
||||
if resistance < 0:
|
||||
logger.exception(
|
||||
self.params_str() + (" requires a source " "resistance of zero " "or greater.")
|
||||
self.params_str() + (" requires a source resistance of zero or greater.")
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
# Check if there is a waveformID in the waveforms list
|
||||
if not any(x.ID == waveform_id for x in grid.waveforms):
|
||||
logger.exception(
|
||||
self.params_str() + (" there is no waveform with " "the identifier {waveform_id}.")
|
||||
self.params_str() + (" there is no waveform with the identifier {waveform_id}.")
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
@@ -403,7 +408,7 @@ class VoltageSource(UserObjectMulti):
|
||||
+ ")"
|
||||
)
|
||||
v.resistance = resistance
|
||||
v.waveformID = waveform_id
|
||||
v.waveform = grid.get_waveform_by_id(waveform_id)
|
||||
|
||||
try:
|
||||
start = self.kwargs["start"]
|
||||
@@ -412,37 +417,36 @@ class VoltageSource(UserObjectMulti):
|
||||
if start < 0:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" delay of the initiation " "of the source should not " "be less than zero.")
|
||||
+ (" delay of the initiation of the source should not be less than zero.")
|
||||
)
|
||||
raise ValueError
|
||||
if stop < 0:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" time to remove the " "source should not be " "less than zero.")
|
||||
self.params_str() + (" time to remove the source should not be less than zero.")
|
||||
)
|
||||
raise ValueError
|
||||
if stop - start <= 0:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" duration of the source " "should not be zero or " "less.")
|
||||
self.params_str() + (" duration of the source should not be zero or less.")
|
||||
)
|
||||
raise ValueError
|
||||
v.start = start
|
||||
v.stop = min(stop, grid.timewindow)
|
||||
v.stop = min(stop, model.timewindow)
|
||||
startstop = f" start time {v.start:g} secs, finish time {v.stop:g} secs "
|
||||
except KeyError:
|
||||
v.start = 0
|
||||
v.stop = grid.timewindow
|
||||
v.stop = model.timewindow
|
||||
startstop = " "
|
||||
|
||||
v.calculate_waveform_values(grid)
|
||||
iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations
|
||||
v.calculate_waveform_values(iterations, grid.dt)
|
||||
|
||||
logger.info(
|
||||
f"{self.grid_name(grid)}Voltage source with polarity "
|
||||
f"{v.polarisation} at {p2[0]:g}m, {p2[1]:g}m, {p2[2]:g}m, "
|
||||
f"resistance {v.resistance:.1f} Ohms,"
|
||||
+ startstop
|
||||
+ f"using waveform {v.waveformID} created."
|
||||
+ f"using waveform {v.waveform.ID} created."
|
||||
)
|
||||
|
||||
grid.voltagesources.append(v)
|
||||
@@ -481,7 +485,7 @@ class HertzianDipole(UserObjectMulti):
|
||||
rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin)
|
||||
self.kwargs["p1"] = tuple(rot_pts[0, :])
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
polarisation = self.kwargs["polarisation"].lower()
|
||||
p1 = self.kwargs["p1"]
|
||||
@@ -490,30 +494,31 @@ class HertzianDipole(UserObjectMulti):
|
||||
logger.exception(f"{self.params_str()} requires at least 3 parameters.")
|
||||
raise
|
||||
|
||||
grid = uip.grid
|
||||
if self.do_rotate:
|
||||
self._do_rotate(grid)
|
||||
|
||||
# Check polarity & position parameters
|
||||
if polarisation not in ("x", "y", "z"):
|
||||
logger.exception(self.params_str() + (" polarisation must be " "x, y, or z."))
|
||||
logger.exception(self.params_str() + " polarisation must be x, y, or z.")
|
||||
raise ValueError
|
||||
if "2D TMx" in config.get_model_config().mode and polarisation in [
|
||||
"y",
|
||||
"z",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be x in " "2D TMx mode."))
|
||||
logger.exception(self.params_str() + " polarisation must be x in 2D TMx mode.")
|
||||
raise ValueError
|
||||
elif "2D TMy" in config.get_model_config().mode and polarisation in [
|
||||
"x",
|
||||
"z",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be y in " "2D TMy mode."))
|
||||
logger.exception(self.params_str() + " polarisation must be y in 2D TMy mode.")
|
||||
raise ValueError
|
||||
elif "2D TMz" in config.get_model_config().mode and polarisation in [
|
||||
"x",
|
||||
"y",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be z in " "2D TMz mode."))
|
||||
logger.exception(self.params_str() + " polarisation must be z in 2D TMz mode.")
|
||||
raise ValueError
|
||||
|
||||
xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str())
|
||||
@@ -544,7 +549,7 @@ class HertzianDipole(UserObjectMulti):
|
||||
h.ycoordorigin = ycoord
|
||||
h.zcoordorigin = zcoord
|
||||
h.ID = f"{h.__class__.__name__}({str(h.xcoord)},{str(h.ycoord)},{str(h.zcoord)})"
|
||||
h.waveformID = waveform_id
|
||||
h.waveform = grid.get_waveform_by_id(waveform_id)
|
||||
|
||||
try:
|
||||
# Check source start & source remove time parameters
|
||||
@@ -566,14 +571,15 @@ class HertzianDipole(UserObjectMulti):
|
||||
)
|
||||
raise ValueError
|
||||
h.start = start
|
||||
h.stop = min(stop, grid.timewindow)
|
||||
h.stop = min(stop, model.timewindow)
|
||||
startstop = f" start time {h.start:g} secs, finish time {h.stop:g} secs "
|
||||
except KeyError:
|
||||
h.start = 0
|
||||
h.stop = grid.timewindow
|
||||
h.stop = model.timewindow
|
||||
startstop = " "
|
||||
|
||||
h.calculate_waveform_values(grid)
|
||||
iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations
|
||||
h.calculate_waveform_values(iterations, grid.dt)
|
||||
|
||||
if config.get_model_config().mode == "2D":
|
||||
logger.info(
|
||||
@@ -581,7 +587,7 @@ class HertzianDipole(UserObjectMulti):
|
||||
+ f"source in 2D with polarity {h.polarisation} at "
|
||||
+ f"{p2[0]:g}m, {p2[1]:g}m, {p2[2]:g}m,"
|
||||
+ startstop
|
||||
+ f"using waveform {h.waveformID} created."
|
||||
+ f"using waveform {h.waveform.ID} created."
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
@@ -589,7 +595,7 @@ class HertzianDipole(UserObjectMulti):
|
||||
+ f"polarity {h.polarisation} at {p2[0]:g}m, "
|
||||
+ f"{p2[1]:g}m, {p2[2]:g}m,"
|
||||
+ startstop
|
||||
+ f"using waveform {h.waveformID} created."
|
||||
+ f"using waveform {h.waveform.ID} created."
|
||||
)
|
||||
|
||||
grid.hertziandipoles.append(h)
|
||||
@@ -628,7 +634,7 @@ class MagneticDipole(UserObjectMulti):
|
||||
rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin)
|
||||
self.kwargs["p1"] = tuple(rot_pts[0, :])
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
polarisation = self.kwargs["polarisation"].lower()
|
||||
p1 = self.kwargs["p1"]
|
||||
@@ -637,30 +643,31 @@ class MagneticDipole(UserObjectMulti):
|
||||
logger.exception(f"{self.params_str()} requires at least five parameters.")
|
||||
raise
|
||||
|
||||
grid = uip.grid
|
||||
if self.do_rotate:
|
||||
self._do_rotate(grid)
|
||||
|
||||
# Check polarity & position parameters
|
||||
if polarisation not in ("x", "y", "z"):
|
||||
logger.exception(self.params_str() + (" polarisation must be " "x, y, or z."))
|
||||
logger.exception(self.params_str() + " polarisation must be x, y, or z.")
|
||||
raise ValueError
|
||||
if "2D TMx" in config.get_model_config().mode and polarisation in [
|
||||
"y",
|
||||
"z",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be x in " "2D TMx mode."))
|
||||
logger.exception(self.params_str() + " polarisation must be x in 2D TMx mode.")
|
||||
raise ValueError
|
||||
elif "2D TMy" in config.get_model_config().mode and polarisation in [
|
||||
"x",
|
||||
"z",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be y in " "2D TMy mode."))
|
||||
logger.exception(self.params_str() + " polarisation must be y in 2D TMy mode.")
|
||||
raise ValueError
|
||||
elif "2D TMz" in config.get_model_config().mode and polarisation in [
|
||||
"x",
|
||||
"y",
|
||||
]:
|
||||
logger.exception(self.params_str() + (" polarisation must be z in " "2D TMz mode."))
|
||||
logger.exception(self.params_str() + " polarisation must be z in 2D TMz mode.")
|
||||
raise ValueError
|
||||
|
||||
xcoord, ycoord, zcoord = uip.check_src_rx_point(p1, self.params_str())
|
||||
@@ -691,7 +698,7 @@ class MagneticDipole(UserObjectMulti):
|
||||
+ str(m.zcoord)
|
||||
+ ")"
|
||||
)
|
||||
m.waveformID = waveform_id
|
||||
m.waveform = grid.get_waveform_by_id(waveform_id)
|
||||
|
||||
try:
|
||||
# Check source start & source remove time parameters
|
||||
@@ -700,36 +707,35 @@ class MagneticDipole(UserObjectMulti):
|
||||
if start < 0:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" delay of the initiation " "of the source should not " "be less than zero.")
|
||||
+ " delay of the initiation of the source should not be less than zero."
|
||||
)
|
||||
raise ValueError
|
||||
if stop < 0:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" time to remove the " "source should not be " "less than zero.")
|
||||
self.params_str() + " time to remove the source should not be less than zero."
|
||||
)
|
||||
raise ValueError
|
||||
if stop - start <= 0:
|
||||
logger.exception(
|
||||
self.params_str()
|
||||
+ (" duration of the source " "should not be zero or " "less.")
|
||||
self.params_str() + " duration of the source should not be zero or less."
|
||||
)
|
||||
raise ValueError
|
||||
m.start = start
|
||||
m.stop = min(stop, grid.timewindow)
|
||||
m.stop = min(stop, model.timewindow)
|
||||
startstop = f" start time {m.start:g} secs, finish time {m.stop:g} secs "
|
||||
except KeyError:
|
||||
m.start = 0
|
||||
m.stop = grid.timewindow
|
||||
m.stop = model.timewindow
|
||||
startstop = " "
|
||||
|
||||
m.calculate_waveform_values(grid)
|
||||
iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations
|
||||
m.calculate_waveform_values(iterations, grid.dt)
|
||||
|
||||
logger.info(
|
||||
f"{self.grid_name(grid)}Magnetic dipole with polarity "
|
||||
f"{m.polarisation} at {p2[0]:g}m, {p2[1]:g}m, {p2[2]:g}m,"
|
||||
+ startstop
|
||||
+ f"using waveform {m.waveformID} created."
|
||||
+ f"using waveform {m.waveform.ID} created."
|
||||
)
|
||||
|
||||
grid.magneticdipoles.append(m)
|
||||
@@ -769,7 +775,7 @@ class TransmissionLine(UserObjectMulti):
|
||||
rot_pts = rotate_2point_object(rot_pol_pts, self.axis, self.angle, self.origin)
|
||||
self.kwargs["p1"] = tuple(rot_pts[0, :])
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
polarisation = self.kwargs["polarisation"].lower()
|
||||
p1 = self.kwargs["p1"]
|
||||
@@ -779,6 +785,7 @@ class TransmissionLine(UserObjectMulti):
|
||||
logger.exception(f"{self.params_str()} requires at least six parameters.")
|
||||
raise
|
||||
|
||||
grid = uip.grid
|
||||
if self.do_rotate:
|
||||
self._do_rotate(grid)
|
||||
|
||||
@@ -832,7 +839,8 @@ class TransmissionLine(UserObjectMulti):
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
t = TransmissionLineUser(grid)
|
||||
iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations
|
||||
t = TransmissionLineUser(iterations, grid.dt)
|
||||
t.polarisation = polarisation
|
||||
t.xcoord = xcoord
|
||||
t.ycoord = ycoord
|
||||
@@ -848,7 +856,7 @@ class TransmissionLine(UserObjectMulti):
|
||||
+ ")"
|
||||
)
|
||||
t.resistance = resistance
|
||||
t.waveformID = waveform_id
|
||||
t.waveform = grid.get_waveform_by_id(waveform_id)
|
||||
|
||||
try:
|
||||
# Check source start & source remove time parameters
|
||||
@@ -873,14 +881,14 @@ class TransmissionLine(UserObjectMulti):
|
||||
)
|
||||
raise ValueError
|
||||
t.start = start
|
||||
t.stop = min(stop, grid.timewindow)
|
||||
t.stop = min(stop, model.timewindow)
|
||||
startstop = f" start time {t.start:g} secs, finish time {t.stop:g} secs "
|
||||
except KeyError:
|
||||
t.start = 0
|
||||
t.stop = grid.timewindow
|
||||
t.stop = model.timewindow
|
||||
startstop = " "
|
||||
|
||||
t.calculate_waveform_values(grid)
|
||||
t.calculate_waveform_values(iterations, grid.dt)
|
||||
t.calculate_incident_V_I(grid)
|
||||
|
||||
logger.info(
|
||||
@@ -888,7 +896,7 @@ class TransmissionLine(UserObjectMulti):
|
||||
+ f"{t.polarisation} at {p2[0]:g}m, {p2[1]:g}m, "
|
||||
+ f"{p2[2]:g}m, resistance {t.resistance:.1f} Ohms,"
|
||||
+ startstop
|
||||
+ f"using waveform {t.waveformID} created."
|
||||
+ f"using waveform {t.waveform.ID} created."
|
||||
)
|
||||
|
||||
grid.transmissionlines.append(t)
|
||||
@@ -941,13 +949,14 @@ class Rx(UserObjectMulti):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
p1 = self.kwargs["p1"]
|
||||
except KeyError:
|
||||
logger.exception(self.params_str())
|
||||
raise
|
||||
|
||||
grid = uip.grid
|
||||
if self.do_rotate:
|
||||
self._do_rotate(grid)
|
||||
|
||||
@@ -958,6 +967,8 @@ class Rx(UserObjectMulti):
|
||||
r.xcoord, r.ycoord, r.zcoord = p
|
||||
r.xcoordorigin, r.ycoordorigin, r.zcoordorigin = p
|
||||
|
||||
iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations
|
||||
|
||||
try:
|
||||
r.ID = self.kwargs["id"]
|
||||
outputs = self.kwargs["outputs"]
|
||||
@@ -966,7 +977,7 @@ class Rx(UserObjectMulti):
|
||||
r.ID = f"{r.__class__.__name__}({str(r.xcoord)},{str(r.ycoord)},{str(r.zcoord)})"
|
||||
for key in RxUser.defaultoutputs:
|
||||
r.outputs[key] = np.zeros(
|
||||
grid.iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
else:
|
||||
outputs.sort()
|
||||
@@ -979,7 +990,7 @@ class Rx(UserObjectMulti):
|
||||
for field in outputs:
|
||||
if field in allowableoutputs:
|
||||
r.outputs[field] = np.zeros(
|
||||
grid.iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
else:
|
||||
logger.exception(
|
||||
@@ -1015,7 +1026,7 @@ class RxArray(UserObjectMulti):
|
||||
self.order = 8
|
||||
self.hash = "#rx_array"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
p1 = self.kwargs["p1"]
|
||||
p2 = self.kwargs["p2"]
|
||||
@@ -1063,6 +1074,7 @@ class RxArray(UserObjectMulti):
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
grid = uip.grid
|
||||
logger.info(
|
||||
f"{self.grid_name(grid)}Receiver array "
|
||||
f"{p3[0]:g}m, {p3[1]:g}m, {p3[2]:g}m, to "
|
||||
@@ -1070,6 +1082,8 @@ class RxArray(UserObjectMulti):
|
||||
f"{dx * grid.dx:g}m, {dy * grid.dy:g}m, {dz * grid.dz:g}m"
|
||||
)
|
||||
|
||||
iterations = grid.iterations if isinstance(grid, SubGridBaseGrid) else model.iterations
|
||||
|
||||
for x in range(xs, xf + 1, dx):
|
||||
for y in range(ys, yf + 1, dy):
|
||||
for z in range(zs, zf + 1, dz):
|
||||
@@ -1087,7 +1101,7 @@ class RxArray(UserObjectMulti):
|
||||
r.ID = f"{r.__class__.__name__}({str(x)},{str(y)},{str(z)})"
|
||||
for key in RxUser.defaultoutputs:
|
||||
r.outputs[key] = np.zeros(
|
||||
grid.iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
logger.info(
|
||||
f" Receiver at {p5[0]:g}m, {p5[1]:g}m, "
|
||||
@@ -1123,7 +1137,8 @@ class Snapshot(UserObjectMulti):
|
||||
self.order = 9
|
||||
self.hash = "#snapshot"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
grid = uip.grid
|
||||
if isinstance(grid, SubGridBaseGrid):
|
||||
logger.exception(f"{self.params_str()} do not add snapshots to subgrids.")
|
||||
raise ValueError
|
||||
@@ -1203,7 +1218,7 @@ class Snapshot(UserObjectMulti):
|
||||
f"{self.params_str()} the step size should not be less than the spatial discretisation."
|
||||
)
|
||||
raise ValueError
|
||||
if iterations <= 0 or iterations > grid.iterations:
|
||||
if iterations <= 0 or iterations > model.iterations:
|
||||
logger.exception(f"{self.params_str()} time value is not valid.")
|
||||
raise ValueError
|
||||
|
||||
@@ -1252,7 +1267,7 @@ class Material(UserObjectMulti):
|
||||
self.order = 10
|
||||
self.hash = "#material"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
er = self.kwargs["er"]
|
||||
se = self.kwargs["se"]
|
||||
@@ -1285,6 +1300,8 @@ class Material(UserObjectMulti):
|
||||
if sm < 0:
|
||||
logger.exception(f"{self.params_str()} requires a positive value for magnetic loss.")
|
||||
raise ValueError
|
||||
|
||||
grid = uip.grid
|
||||
if any(x.ID == material_id for x in grid.materials):
|
||||
logger.exception(f"{self.params_str()} with ID {material_id} already exists")
|
||||
raise ValueError
|
||||
@@ -1302,7 +1319,7 @@ class Material(UserObjectMulti):
|
||||
|
||||
m.er = er
|
||||
logger.info(
|
||||
f"{self.grid_name(grid)}Material {m.ID} with eps_r={m.er:g}, "
|
||||
f"{self.model_name(model)}Material {m.ID} with eps_r={m.er:g}, "
|
||||
f"sigma={m.se:g} S/m; mu_r={m.mr:g}, sigma*={m.sm:g} Ohm/m "
|
||||
f"created."
|
||||
)
|
||||
@@ -1329,7 +1346,7 @@ class AddDebyeDispersion(UserObjectMulti):
|
||||
self.order = 11
|
||||
self.hash = "#add_dispersion_debye"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
poles = self.kwargs["poles"]
|
||||
er_delta = self.kwargs["er_delta"]
|
||||
@@ -1344,6 +1361,7 @@ class AddDebyeDispersion(UserObjectMulti):
|
||||
raise ValueError
|
||||
|
||||
# Look up requested materials in existing list of material instances
|
||||
grid = uip.grid
|
||||
materials = [y for x in material_ids for y in grid.materials if y.ID == x]
|
||||
|
||||
if len(materials) != len(material_ids):
|
||||
@@ -1405,7 +1423,7 @@ class AddLorentzDispersion(UserObjectMulti):
|
||||
self.order = 12
|
||||
self.hash = "#add_dispersion_lorentz"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
poles = self.kwargs["poles"]
|
||||
er_delta = self.kwargs["er_delta"]
|
||||
@@ -1421,6 +1439,7 @@ class AddLorentzDispersion(UserObjectMulti):
|
||||
raise ValueError
|
||||
|
||||
# Look up requested materials in existing list of material instances
|
||||
grid = uip.grid
|
||||
materials = [y for x in material_ids for y in grid.materials if y.ID == x]
|
||||
|
||||
if len(materials) != len(material_ids):
|
||||
@@ -1484,7 +1503,7 @@ class AddDrudeDispersion(UserObjectMulti):
|
||||
self.order = 13
|
||||
self.hash = "#add_dispersion_drude"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
poles = self.kwargs["poles"]
|
||||
omega = self.kwargs["omega"]
|
||||
@@ -1499,6 +1518,7 @@ class AddDrudeDispersion(UserObjectMulti):
|
||||
raise ValueError
|
||||
|
||||
# Look up requested materials in existing list of material instances
|
||||
grid = uip.grid
|
||||
materials = [y for x in material_ids for y in grid.materials if y.ID == x]
|
||||
|
||||
if len(materials) != len(material_ids):
|
||||
@@ -1563,7 +1583,7 @@ class SoilPeplinski(UserObjectMulti):
|
||||
self.order = 14
|
||||
self.hash = "#soil_peplinski"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
sand_fraction = self.kwargs["sand_fraction"]
|
||||
clay_fraction = self.kwargs["clay_fraction"]
|
||||
@@ -1606,6 +1626,7 @@ class SoilPeplinski(UserObjectMulti):
|
||||
"fraction."
|
||||
)
|
||||
raise ValueError
|
||||
grid = uip.grid
|
||||
if any(x.ID == ID for x in grid.mixingmodels):
|
||||
logger.exception(f"{self.params_str()} with ID {ID} already exists")
|
||||
raise ValueError
|
||||
@@ -1652,7 +1673,7 @@ class MaterialRange(UserObjectMulti):
|
||||
self.order = 15
|
||||
self.hash = "#material_range"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
er_lower = self.kwargs["er_lower"]
|
||||
er_upper = self.kwargs["er_upper"]
|
||||
@@ -1710,7 +1731,7 @@ class MaterialRange(UserObjectMulti):
|
||||
logger.exception(
|
||||
f"{self.params_str()} requires a positive value for the upper range of magnetic loss."
|
||||
)
|
||||
|
||||
grid = uip.grid
|
||||
if any(x.ID == ID for x in grid.mixingmodels):
|
||||
logger.exception(f"{self.params_str()} with ID {ID} already exists")
|
||||
raise ValueError
|
||||
@@ -1746,14 +1767,14 @@ class MaterialList(UserObjectMulti):
|
||||
self.order = 16
|
||||
self.hash = "#material_list"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
list_of_materials = self.kwargs["list_of_materials"]
|
||||
ID = self.kwargs["id"]
|
||||
except KeyError:
|
||||
logger.exception(f"{self.params_str()} requires at at least 2 parameters.")
|
||||
raise
|
||||
|
||||
grid = uip.grid
|
||||
if any(x.ID == ID for x in grid.mixingmodels):
|
||||
logger.exception(f"{self.params_str()} with ID {ID} already exists")
|
||||
raise ValueError
|
||||
@@ -1801,7 +1822,7 @@ class GeometryView(UserObjectMulti):
|
||||
|
||||
return GeometryViewUser
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
p1 = self.kwargs["p1"]
|
||||
p2 = self.kwargs["p2"]
|
||||
@@ -1824,6 +1845,7 @@ class GeometryView(UserObjectMulti):
|
||||
xs, ys, zs = p1
|
||||
xf, yf, zf = p2
|
||||
|
||||
grid = uip.grid
|
||||
dx, dy, dz = uip.discretise_static_point(dl)
|
||||
|
||||
if dx < 0 or dy < 0 or dz < 0:
|
||||
@@ -1865,7 +1887,7 @@ class GeometryView(UserObjectMulti):
|
||||
f"base {g.filename} created."
|
||||
)
|
||||
|
||||
grid.geometryviews.append(g)
|
||||
model.geometryviews.append(g)
|
||||
|
||||
|
||||
class GeometryObjectsWrite(UserObjectMulti):
|
||||
@@ -1886,7 +1908,11 @@ class GeometryObjectsWrite(UserObjectMulti):
|
||||
self.order = 18
|
||||
self.hash = "#geometry_objects_write"
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
grid = uip.grid
|
||||
if isinstance(grid, SubGridBaseGrid):
|
||||
logger.exception(f"{self.params_str()} do not add geometry objects to subgrids.")
|
||||
raise ValueError
|
||||
try:
|
||||
p1 = self.kwargs["p1"]
|
||||
p2 = self.kwargs["p2"]
|
||||
@@ -1910,7 +1936,7 @@ class GeometryObjectsWrite(UserObjectMulti):
|
||||
f"{g.filename_materials}"
|
||||
)
|
||||
|
||||
grid.geometryobjectswrite.append(g)
|
||||
model.geometryobjects.append(g)
|
||||
|
||||
|
||||
class PMLCFS(UserObjectMulti):
|
||||
@@ -1942,7 +1968,7 @@ class PMLCFS(UserObjectMulti):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 19
|
||||
|
||||
def build(self, grid, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
alphascalingprofile = self.kwargs["alphascalingprofile"]
|
||||
alphascalingdirection = self.kwargs["alphascalingdirection"]
|
||||
@@ -2028,6 +2054,7 @@ class PMLCFS(UserObjectMulti):
|
||||
f"{cfssigma.min:g}, max: {cfssigma.max}) created."
|
||||
)
|
||||
|
||||
grid = uip.grid
|
||||
grid.pmls["cfs"].append(cfs)
|
||||
|
||||
if len(grid.pmls["cfs"]) > 2:
|
||||
|
@@ -15,7 +15,6 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
@@ -23,6 +22,7 @@ import numpy as np
|
||||
|
||||
import gprMax.config as config
|
||||
from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
from gprMax.model import Model
|
||||
from gprMax.user_inputs import MainGridUserInput
|
||||
|
||||
from .pml import PML
|
||||
@@ -51,7 +51,7 @@ class UserObjectSingle(ABC):
|
||||
setattr(self.props, k, v)
|
||||
|
||||
@abstractmethod
|
||||
def build(self, grid: FDTDGrid, uip: MainGridUserInput):
|
||||
def build(self, model: Model, uip: MainGridUserInput):
|
||||
pass
|
||||
|
||||
# TODO: Check if this is actually needed
|
||||
@@ -70,11 +70,11 @@ class Title(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 1
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
title = self.kwargs["name"]
|
||||
G.title = title
|
||||
logger.info(f"Model title: {G.title}")
|
||||
model.title = title
|
||||
logger.info(f"Model title: {model.title}")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -90,34 +90,33 @@ class Discretisation(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 2
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
G.dl = np.array(self.kwargs["p1"])
|
||||
G.dx, G.dy, G.dz = self.kwargs["p1"]
|
||||
model.dl = np.array(self.kwargs["p1"], dtype=float)
|
||||
except KeyError:
|
||||
logger.exception(f"{self.__str__()} discretisation requires a point")
|
||||
raise
|
||||
|
||||
if G.dl[0] <= 0:
|
||||
if model.dl[0] <= 0:
|
||||
logger.exception(
|
||||
f"{self.__str__()} discretisation requires the "
|
||||
f"x-direction spatial step to be greater than zero"
|
||||
)
|
||||
raise ValueError
|
||||
if G.dl[1] <= 0:
|
||||
if model.dl[1] <= 0:
|
||||
logger.exception(
|
||||
f"{self.__str__()} discretisation requires the "
|
||||
f"y-direction spatial step to be greater than zero"
|
||||
)
|
||||
raise ValueError
|
||||
if G.dl[2] <= 0:
|
||||
if model.dl[2] <= 0:
|
||||
logger.exception(
|
||||
f"{self.__str__()} discretisation requires the "
|
||||
f"z-direction spatial step to be greater than zero"
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
logger.info(f"Spatial discretisation: {G.dl[0]:g} x {G.dl[1]:g} x {G.dl[2]:g}m")
|
||||
logger.info(f"Spatial discretisation: {model.dl[0]:g} x {model.dl[1]:g} x {model.dl[2]:g}m")
|
||||
|
||||
|
||||
class Domain(UserObjectSingle):
|
||||
@@ -131,33 +130,37 @@ class Domain(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 3
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
G.nx, G.ny, G.nz = uip.discretise_point(self.kwargs["p1"])
|
||||
model.gnx, model.gny, model.gnz = uip.discretise_point(self.kwargs["p1"])
|
||||
model.G.nx = model.gnx
|
||||
model.G.ny = model.gny
|
||||
model.G.nz = model.gnz
|
||||
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 " f"every dimension")
|
||||
if model.gnx == 0 or model.gny == 0 or model.gnz == 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)"
|
||||
+ f"{self.kwargs['p1'][2]:g}m ({model.gnx:d} x {model.gny:d} x {model.gnz:d} = "
|
||||
+ f"{(model.gnx * model.gny * model.gnz):g} cells)"
|
||||
)
|
||||
|
||||
# Calculate time step at CFL limit; switch off appropriate PMLs for 2D
|
||||
if G.nx == 1:
|
||||
G = model.G
|
||||
if model.gnx == 1:
|
||||
config.get_model_config().mode = "2D TMx"
|
||||
G.pmls["thickness"]["x0"] = 0
|
||||
G.pmls["thickness"]["xmax"] = 0
|
||||
elif G.ny == 1:
|
||||
elif model.gny == 1:
|
||||
config.get_model_config().mode = "2D TMy"
|
||||
G.pmls["thickness"]["y0"] = 0
|
||||
G.pmls["thickness"]["ymax"] = 0
|
||||
elif G.nz == 1:
|
||||
elif model.gnz == 1:
|
||||
config.get_model_config().mode = "2D TMz"
|
||||
G.pmls["thickness"]["z0"] = 0
|
||||
G.pmls["thickness"]["zmax"] = 0
|
||||
@@ -187,7 +190,7 @@ class TimeStepStabilityFactor(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 4
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
f = self.kwargs["f"]
|
||||
except KeyError:
|
||||
@@ -201,10 +204,10 @@ class TimeStepStabilityFactor(UserObjectSingle):
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
G.dt_mod = f
|
||||
G.dt = G.dt * G.dt_mod
|
||||
model.dt_mod = f
|
||||
model.dt *= model.dt_mod
|
||||
|
||||
logger.info(f"Time step (modified): {G.dt:g} secs")
|
||||
logger.info(f"Time step (modified): {model.dt:g} secs")
|
||||
|
||||
|
||||
class TimeWindow(UserObjectSingle):
|
||||
@@ -219,33 +222,33 @@ class TimeWindow(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 5
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
# If number of iterations given
|
||||
# The +/- 1 used in calculating the number of iterations is to account for
|
||||
# the fact that the solver (iterations) loop runs from 0 to < G.iterations
|
||||
try:
|
||||
iterations = int(self.kwargs["iterations"])
|
||||
G.timewindow = (iterations - 1) * G.dt
|
||||
G.iterations = iterations
|
||||
model.timewindow = (iterations - 1) * model.dt
|
||||
model.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
|
||||
model.timewindow = tmp
|
||||
model.iterations = int(np.ceil(tmp / model.dt)) + 1
|
||||
else:
|
||||
logger.exception(self.__str__() + " must have a value greater than zero")
|
||||
raise ValueError
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not G.timewindow:
|
||||
if not model.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)")
|
||||
logger.info(f"Time window: {model.timewindow:g} secs ({model.iterations} iterations)")
|
||||
|
||||
|
||||
class OMPThreads(UserObjectSingle):
|
||||
@@ -260,7 +263,7 @@ class OMPThreads(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 6
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
n = self.kwargs["n"]
|
||||
except KeyError:
|
||||
@@ -296,7 +299,8 @@ class PMLProps(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 7
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
G = model.G
|
||||
try:
|
||||
G.pmls["formulation"] = self.kwargs["formulation"]
|
||||
if G.pmls["formulation"] not in PML.formulations:
|
||||
@@ -348,16 +352,16 @@ class SrcSteps(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 8
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
G.srcsteps = uip.discretise_point(self.kwargs["p1"])
|
||||
model.srcsteps = uip.discretise_point(self.kwargs["p1"])
|
||||
except KeyError:
|
||||
logger.exception(f"{self.__str__()} requires exactly three parameters")
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"Simple sources will step {G.srcsteps[0] * G.dx:g}m, "
|
||||
f"{G.srcsteps[1] * G.dy:g}m, {G.srcsteps[2] * G.dz:g}m "
|
||||
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."
|
||||
)
|
||||
|
||||
@@ -373,16 +377,16 @@ class RxSteps(UserObjectSingle):
|
||||
super().__init__(**kwargs)
|
||||
self.order = 9
|
||||
|
||||
def build(self, G, uip):
|
||||
def build(self, model, uip):
|
||||
try:
|
||||
G.rxsteps = uip.discretise_point(self.kwargs["p1"])
|
||||
model.rxsteps = uip.discretise_point(self.kwargs["p1"])
|
||||
except KeyError:
|
||||
logger.exception(f"{self.__str__()} requires exactly three parameters")
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"All receivers will step {G.rxsteps[0] * G.dx:g}m, "
|
||||
f"{G.rxsteps[1] * G.dy:g}m, {G.rxsteps[2] * G.dz:g}m "
|
||||
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."
|
||||
)
|
||||
|
||||
|
@@ -106,12 +106,12 @@ class Context:
|
||||
if not model_config.reuse_geometry():
|
||||
scene = self._get_scene(model_num)
|
||||
model = self._create_model()
|
||||
scene.create_internal_objects(model.G)
|
||||
scene.create_internal_objects(model)
|
||||
|
||||
model.build()
|
||||
|
||||
if not config.sim_config.geometry_only:
|
||||
solver = create_solver(model.G)
|
||||
solver = create_solver(model)
|
||||
model.solve(solver)
|
||||
del solver
|
||||
|
||||
|
@@ -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,6 +47,7 @@ def store_outputs(G):
|
||||
# Store current component
|
||||
else:
|
||||
func = globals()[output]
|
||||
# TODO: Test this still works - likely broken by removing 'Ix' etc functions
|
||||
rx.outputs[output][iteration] = func(rx.xcoord, rx.ycoord, rx.zcoord, Hx, Hy, Hz, G)
|
||||
|
||||
for tl in G.transmissionlines:
|
||||
@@ -52,26 +55,29 @@ def store_outputs(G):
|
||||
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)
|
||||
|
||||
@@ -88,18 +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)
|
||||
nsrc = len(
|
||||
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"]
|
||||
@@ -137,54 +145,3 @@ def write_hd5_data(basegrp, grid, is_subgrid=False):
|
||||
|
||||
for output in rx.outputs:
|
||||
basegrp["rxs/rx" + str(rxindex + 1) + "/" + output] = rx.outputs[output]
|
||||
|
||||
|
||||
def Ix(x, y, z, Hx, Hy, Hz, G):
|
||||
"""Calculates the x-component of current at a grid position.
|
||||
|
||||
Args:
|
||||
x, y, z: floats for coordinates of position in grid.
|
||||
Hx, Hy, Hz: numpy array of magnetic field values.
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
|
||||
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])
|
||||
|
||||
return Ix
|
||||
|
||||
|
||||
def Iy(x, y, z, Hx, Hy, Hz, G):
|
||||
"""Calculates the y-component of current at a grid position.
|
||||
|
||||
Args:
|
||||
x, y, z: floats for coordinates of position in grid.
|
||||
Hx, Hy, Hz: numpy array of magnetic field values.
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
|
||||
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])
|
||||
|
||||
return Iy
|
||||
|
||||
|
||||
def Iz(x, y, z, Hx, Hy, Hz, G):
|
||||
"""Calculates the z-component of current at a grid position.
|
||||
|
||||
Args:
|
||||
x, y, z: floats for coordinates of position in grid.
|
||||
Hx, Hy, Hz: numpy array of magnetic field values.
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
|
||||
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])
|
||||
|
||||
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
|
||||
@@ -82,7 +84,9 @@ class FractalSurface:
|
||||
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])
|
||||
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)
|
||||
@@ -157,7 +161,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 = []
|
||||
@@ -176,7 +182,9 @@ class FractalVolume:
|
||||
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])
|
||||
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)
|
||||
@@ -185,7 +193,11 @@ class FractalVolume:
|
||||
|
||||
# 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]
|
||||
[
|
||||
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
|
||||
@@ -225,7 +237,9 @@ class FractalVolume:
|
||||
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)
|
||||
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,
|
||||
@@ -254,7 +268,9 @@ class Grass:
|
||||
"""
|
||||
|
||||
self.numblades = numblades
|
||||
self.geometryparams = np.zeros((self.numblades, 6), dtype=config.sim_config.dtypes["float_or_double"])
|
||||
self.geometryparams = np.zeros(
|
||||
(self.numblades, 6), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
self.seed = seed
|
||||
self.set_geometry_parameters()
|
||||
|
||||
|
@@ -28,6 +28,7 @@ from evtk.vtk import VtkGroup, VtkImageData, VtkUnstructuredGrid
|
||||
from tqdm import tqdm
|
||||
|
||||
import gprMax.config as config
|
||||
from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
|
||||
from ._version import __version__
|
||||
from .cython.geometry_outputs import write_lines
|
||||
@@ -52,7 +53,8 @@ def save_geometry_views(gvs):
|
||||
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)}, "
|
||||
f"{gv.filename.name}{gv.vtkfiletype.ext}",
|
||||
ncols=get_terminal_width() - 1,
|
||||
file=sys.stdout,
|
||||
disable=not config.sim_config.general["progressbars"],
|
||||
@@ -96,7 +98,9 @@ class GeometryView:
|
||||
def set_filename(self):
|
||||
"""Constructs filename from user-supplied name and model run number."""
|
||||
parts = config.get_model_config().output_file_path.parts
|
||||
self.filename = Path(*parts[:-1], self.filenamebase + config.get_model_config().appendmodelnumber)
|
||||
self.filename = Path(
|
||||
*parts[:-1], self.filenamebase + config.get_model_config().appendmodelnumber
|
||||
)
|
||||
|
||||
|
||||
class GeometryViewLines(GeometryView):
|
||||
@@ -122,7 +126,12 @@ class GeometryViewLines(GeometryView):
|
||||
):
|
||||
# Require contiguous for evtk library
|
||||
ID = np.ascontiguousarray(
|
||||
self.grid.ID[:, self.xs : self.xf : self.dx, self.ys : self.yf : self.dy, self.zs : self.zf : self.dz]
|
||||
self.grid.ID[
|
||||
:,
|
||||
self.xs : self.xf : self.dx,
|
||||
self.ys : self.yf : self.dy,
|
||||
self.zs : self.zf : self.dz,
|
||||
]
|
||||
)
|
||||
else:
|
||||
# This array is contiguous by design
|
||||
@@ -158,7 +167,15 @@ class GeometryViewLines(GeometryView):
|
||||
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.nbytes = (
|
||||
x.nbytes
|
||||
+ y.nbytes
|
||||
+ z.nbytes
|
||||
+ lines.nbytes
|
||||
+ offsets_size
|
||||
+ connect_size
|
||||
+ cell_type_size
|
||||
)
|
||||
|
||||
vtk_data = {"x": x, "y": y, "z": z, "data": lines, "comments": comments}
|
||||
|
||||
@@ -205,7 +222,11 @@ class GeometryViewVoxels(GeometryView):
|
||||
):
|
||||
# Require contiguous for evtk library
|
||||
solid = np.ascontiguousarray(
|
||||
self.grid.solid[self.xs : self.xf : self.dx, self.ys : self.yf : self.dy, self.zs : self.zf : self.dz]
|
||||
self.grid.solid[
|
||||
self.xs : self.xf : self.dx,
|
||||
self.ys : self.yf : self.dy,
|
||||
self.zs : self.zf : self.dz,
|
||||
]
|
||||
)
|
||||
else:
|
||||
# This array is contiguous by design
|
||||
@@ -366,12 +387,18 @@ class GeometryObjects:
|
||||
self.filename_materials = self.filename_materials.with_suffix(".txt")
|
||||
|
||||
# Sizes of arrays to write necessary to update progress bar
|
||||
self.solidsize = (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
|
||||
self.IDsize = 6 * (self.nx + 1) * (self.ny + 1) * (self.nz + 1) * np.dtype(np.uint32).itemsize
|
||||
self.solidsize = (
|
||||
(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
|
||||
)
|
||||
self.IDsize = (
|
||||
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:
|
||||
@@ -381,20 +408,34 @@ class GeometryObjects:
|
||||
|
||||
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])
|
||||
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
|
||||
G.solid[self.xs : self.xf + 1, self.ys : self.yf + 1, self.zs : self.zf + 1].astype(
|
||||
"int16"
|
||||
)
|
||||
- minmat
|
||||
)
|
||||
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"] = 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
|
||||
]
|
||||
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"] = (
|
||||
G.ID[:, self.xs : self.xf + 1, self.ys : self.yf + 1, self.zs : self.zf + 1]
|
||||
- minmat
|
||||
)
|
||||
pbar.update(self.IDsize)
|
||||
|
||||
# Write materials list to a text file
|
||||
@@ -411,7 +452,9 @@ class GeometryObjects:
|
||||
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} "
|
||||
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):
|
||||
@@ -423,6 +466,8 @@ class GeometryObjects:
|
||||
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 += (
|
||||
f"{material.tau[pole]:g} " f"{material.alpha[pole]:g} "
|
||||
)
|
||||
dispersionstr += material.ID
|
||||
fmaterials.write(dispersionstr + "\n")
|
||||
|
@@ -21,25 +21,22 @@ import itertools
|
||||
import logging
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from typing import Iterable, List, Union
|
||||
from typing import Any, Iterable, List, Union
|
||||
|
||||
import humanize
|
||||
import numpy as np
|
||||
from terminaltables import SingleTable
|
||||
from tqdm import tqdm
|
||||
|
||||
from gprMax import config
|
||||
from gprMax.cython.yee_cell_build import build_electric_components, build_magnetic_components
|
||||
|
||||
# from gprMax.geometry_outputs import GeometryObjects, GeometryView
|
||||
from gprMax.materials import Material, process_materials
|
||||
from gprMax.fractals import FractalVolume
|
||||
from gprMax.materials import ListMaterial, Material, PeplinskiSoil, RangeMaterial, process_materials
|
||||
from gprMax.pml import CFS, PML, build_pml, print_pml_info
|
||||
from gprMax.receivers import Rx
|
||||
from gprMax.sources import HertzianDipole, MagneticDipole, Source, VoltageSource
|
||||
|
||||
# from gprMax.subgrids.grid import SubGridBaseGrid
|
||||
from gprMax.utilities.host_info import mem_check_build_all, mem_check_run_all
|
||||
from gprMax.snapshots import Snapshot
|
||||
from gprMax.sources import HertzianDipole, MagneticDipole, Source, TransmissionLine, VoltageSource
|
||||
from gprMax.utilities.utilities import fft_power, get_terminal_width, round_value
|
||||
from gprMax.waveforms import Waveform
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,22 +46,41 @@ class FDTDGrid:
|
||||
accessing regularly used parameters.
|
||||
"""
|
||||
|
||||
IDlookup = {"Ex": 0, "Ey": 1, "Ez": 2, "Hx": 3, "Hy": 4, "Hz": 5}
|
||||
|
||||
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
|
||||
self.dl = np.ones(3, dtype=float)
|
||||
self.dt = 0.0
|
||||
|
||||
# Field Arrays
|
||||
self.Ex: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Ey: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Ez: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Hx: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Hy: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Hz: np.ndarray[Any, np.dtype[np.single]]
|
||||
|
||||
# Dispersive Arrays
|
||||
self.Tx: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Ty: np.ndarray[Any, np.dtype[np.single]]
|
||||
self.Tz: np.ndarray[Any, np.dtype[np.single]]
|
||||
|
||||
# Geometry Arrays
|
||||
self.solid: np.ndarray[Any, np.dtype[np.uint32]]
|
||||
self.rigidE: np.ndarray[Any, np.dtype[np.int8]]
|
||||
self.rigidH: np.ndarray[Any, np.dtype[np.int8]]
|
||||
self.ID: np.ndarray[Any, np.dtype[np.uint32]]
|
||||
|
||||
# Update Coefficient Arrays
|
||||
self.updatecoeffsE: np.ndarray
|
||||
self.updatecoeffsH: np.ndarray
|
||||
self.updatecoeffsdispersive: np.ndarray
|
||||
|
||||
# PML parameters - set some defaults to use if not user provided
|
||||
self.pmls = {}
|
||||
@@ -78,126 +94,63 @@ class FDTDGrid:
|
||||
# corrections will be different.
|
||||
self.pmls["thickness"] = OrderedDict((key, 10) for key in PML.boundaryIDs)
|
||||
|
||||
# TODO: Add type information.
|
||||
# Currently importing GeometryObjects, GeometryView, and
|
||||
# SubGridBaseGrid cause cyclic dependencies
|
||||
# Materials used by this grid
|
||||
self.materials: List[Material] = []
|
||||
self.mixingmodels = []
|
||||
self.averagevolumeobjects = True
|
||||
self.fractalvolumes = []
|
||||
self.geometryviews = []
|
||||
self.geometryobjectswrite = []
|
||||
self.waveforms = []
|
||||
self.mixingmodels: List[Union[PeplinskiSoil, RangeMaterial, ListMaterial]] = []
|
||||
self.fractalvolumes: List[FractalVolume] = []
|
||||
|
||||
# Sources and receivers contained inside this grid
|
||||
self.waveforms: List[Waveform] = []
|
||||
self.voltagesources: List[VoltageSource] = []
|
||||
self.hertziandipoles: List[HertzianDipole] = []
|
||||
self.magneticdipoles: List[MagneticDipole] = []
|
||||
self.transmissionlines = []
|
||||
self.transmissionlines: List[TransmissionLine] = []
|
||||
self.rxs: List[Rx] = []
|
||||
self.srcsteps: List[float] = [0, 0, 0]
|
||||
self.rxsteps: List[float] = [0, 0, 0]
|
||||
self.snapshots = []
|
||||
self.subgrids = []
|
||||
self.snapshots: List[Snapshot] = []
|
||||
|
||||
self.averagevolumeobjects = True
|
||||
|
||||
@property
|
||||
def dx(self) -> float:
|
||||
return self.dl[0]
|
||||
|
||||
@dx.setter
|
||||
def dx(self, value: float):
|
||||
self.dl[0] = value
|
||||
|
||||
@property
|
||||
def dy(self) -> float:
|
||||
return self.dl[1]
|
||||
|
||||
@dy.setter
|
||||
def dy(self, value: float):
|
||||
self.dl[1] = value
|
||||
|
||||
@property
|
||||
def dz(self) -> float:
|
||||
return self.dl[2]
|
||||
|
||||
@dz.setter
|
||||
def dz(self, value: float):
|
||||
self.dl[2] = value
|
||||
|
||||
def build(self) -> None:
|
||||
# Print info on any subgrids
|
||||
for subgrid in self.subgrids:
|
||||
subgrid.print_info()
|
||||
|
||||
# Combine available grids
|
||||
grids = [self] + self.subgrids
|
||||
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
for grid in grids:
|
||||
# Set default CFS parameter for PMLs if not user provided
|
||||
if not grid.pmls["cfs"]:
|
||||
grid.pmls["cfs"] = [CFS()]
|
||||
logger.info(print_pml_info(grid))
|
||||
if not all(value == 0 for value in grid.pmls["thickness"].values()):
|
||||
grid._build_pmls()
|
||||
if grid.averagevolumeobjects:
|
||||
grid._build_components()
|
||||
grid._tm_grid_update()
|
||||
grid._update_voltage_source_materials()
|
||||
grid.initialise_field_arrays()
|
||||
grid.initialise_std_update_coeff_arrays()
|
||||
if config.get_model_config().materials["maxpoles"] > 0:
|
||||
grid.initialise_dispersive_arrays()
|
||||
grid.initialise_dispersive_update_coeff_array()
|
||||
grid._build_materials()
|
||||
|
||||
# Check to see if numerical dispersion might be a problem
|
||||
results = dispersion_analysis(grid)
|
||||
if results["error"]:
|
||||
logger.warning(
|
||||
f"\nNumerical dispersion analysis [{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 [{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[{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 [{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"
|
||||
)
|
||||
# Set default CFS parameter for PMLs if not user provided
|
||||
if not self.pmls["cfs"]:
|
||||
self.pmls["cfs"] = [CFS()]
|
||||
logger.info(print_pml_info(self))
|
||||
if not all(value == 0 for value in self.pmls["thickness"].values()):
|
||||
self._build_pmls()
|
||||
if self.averagevolumeobjects:
|
||||
self._build_components()
|
||||
self._tm_grid_update()
|
||||
self._create_voltage_source_materials()
|
||||
self.initialise_field_arrays()
|
||||
self.initialise_std_update_coeff_arrays()
|
||||
if config.get_model_config().materials["maxpoles"] > 0:
|
||||
self.initialise_dispersive_arrays()
|
||||
self.initialise_dispersive_update_coeff_array()
|
||||
self._build_materials()
|
||||
|
||||
def _build_pmls(self) -> None:
|
||||
pbar = tqdm(
|
||||
@@ -238,7 +191,7 @@ class FDTDGrid:
|
||||
elif config.get_model_config().mode == "2D TMz":
|
||||
self.tmz()
|
||||
|
||||
def _update_voltage_source_materials(self):
|
||||
def _create_voltage_source_materials(self):
|
||||
# Process any voltage sources (that have resistance) to create a new
|
||||
# material at the source location
|
||||
for voltagesource in self.voltagesources:
|
||||
@@ -256,36 +209,36 @@ class FDTDGrid:
|
||||
logger.info(materialstable.table)
|
||||
|
||||
def _update_positions(
|
||||
self, items: Iterable[Union[Source, Rx]], step_size: List[float], step_number: int
|
||||
self, items: Iterable[Union[Source, Rx]], step_size: List[int], step_number: int
|
||||
) -> None:
|
||||
if step_size[0] != 0 or step_size[1] != 0 or step_size[2] != 0:
|
||||
for item in items:
|
||||
if step_number == 0:
|
||||
if (
|
||||
item.xcoord + self.srcsteps[0] * config.sim_config.model_end < 0
|
||||
or item.xcoord + self.srcsteps[0] * config.sim_config.model_end > self.nx
|
||||
or item.ycoord + self.srcsteps[1] * config.sim_config.model_end < 0
|
||||
or item.ycoord + self.srcsteps[1] * config.sim_config.model_end > self.ny
|
||||
or item.zcoord + self.srcsteps[2] * config.sim_config.model_end < 0
|
||||
or item.zcoord + self.srcsteps[2] * config.sim_config.model_end > self.nz
|
||||
item.xcoord + step_size[0] * config.sim_config.model_end < 0
|
||||
or item.xcoord + step_size[0] * config.sim_config.model_end > self.nx
|
||||
or item.ycoord + step_size[1] * config.sim_config.model_end < 0
|
||||
or item.ycoord + step_size[1] * config.sim_config.model_end > self.ny
|
||||
or item.zcoord + step_size[2] * config.sim_config.model_end < 0
|
||||
or item.zcoord + step_size[2] * config.sim_config.model_end > self.nz
|
||||
):
|
||||
raise ValueError
|
||||
item.xcoord = item.xcoordorigin + step_number * step_size[0]
|
||||
item.ycoord = item.ycoordorigin + step_number * step_size[1]
|
||||
item.zcoord = item.zcoordorigin + step_number * step_size[2]
|
||||
|
||||
def update_simple_source_positions(self, step: int = 0) -> None:
|
||||
def update_simple_source_positions(self, step_size: List[int], step: int = 0) -> None:
|
||||
try:
|
||||
self._update_positions(
|
||||
itertools.chain(self.hertziandipoles, self.magneticdipoles), self.srcsteps, step
|
||||
itertools.chain(self.hertziandipoles, self.magneticdipoles), step_size, step
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.exception("Source(s) will be stepped to a position outside the domain.")
|
||||
raise ValueError from e
|
||||
|
||||
def update_receiver_positions(self, step: int = 0) -> None:
|
||||
def update_receiver_positions(self, step_size: List[int], step: int = 0) -> None:
|
||||
try:
|
||||
self._update_positions(self.rxs, self.rxsteps, step)
|
||||
self._update_positions(self.rxs, step_size, step)
|
||||
except ValueError as e:
|
||||
logger.exception("Receiver(s) will be stepped to a position outside the domain.")
|
||||
raise ValueError from e
|
||||
@@ -322,6 +275,9 @@ class FDTDGrid:
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_waveform_by_id(self, waveform_id: str) -> Waveform:
|
||||
return next(waveform for waveform in self.waveforms if waveform.ID == waveform_id)
|
||||
|
||||
def initialise_geometry_arrays(self):
|
||||
"""Initialise an array for volumetric material IDs (solid);
|
||||
boolean arrays for specifying whether materials can have dielectric
|
||||
@@ -333,7 +289,6 @@ class FDTDGrid:
|
||||
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."""
|
||||
@@ -556,138 +511,237 @@ class FDTDGrid:
|
||||
# binary representation of floating point number.
|
||||
self.dt = round_value(self.dt, decimalplaces=decimal.getcontext().prec - 1)
|
||||
|
||||
def calculate_Ix(self, x: int, y: int, z: int) -> float:
|
||||
"""Calculates the x-component of current at a grid position.
|
||||
|
||||
def dispersion_analysis(G):
|
||||
"""Analysis of numerical dispersion (Taflove et al, 2005, p112) -
|
||||
worse case of maximum frequency and minimum wavelength
|
||||
Args:
|
||||
x: x coordinate of position in grid
|
||||
y: y coordinate of position in grid
|
||||
z: z coordinate of position in grid
|
||||
"""
|
||||
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
if y == 0 or z == 0:
|
||||
Ix = 0
|
||||
else:
|
||||
Ix = self.dy * (self.Hy[x, y, z - 1] - self.Hy[x, y, z]) + self.dz * (
|
||||
self.Hz[x, y, z] - self.Hz[x, y - 1, z]
|
||||
)
|
||||
|
||||
Returns:
|
||||
results: dict of results from dispersion analysis.
|
||||
"""
|
||||
return Ix
|
||||
|
||||
# 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": ""}
|
||||
def calculate_Iy(self, x: int, y: int, z: int) -> float:
|
||||
"""Calculates the y-component of current at a grid position.
|
||||
|
||||
# Find maximum significant frequency
|
||||
if G.waveforms:
|
||||
for waveform in G.waveforms:
|
||||
if waveform.type in ["sine", "contsine"]:
|
||||
results["maxfreq"].append(4 * waveform.freq)
|
||||
Args:
|
||||
x: x coordinate of position in grid
|
||||
y: y coordinate of position in grid
|
||||
z: z coordinate of position in grid
|
||||
"""
|
||||
|
||||
elif waveform.type == "impulse":
|
||||
results["error"] = "impulse waveform used."
|
||||
if x == 0 or z == 0:
|
||||
Iy = 0
|
||||
else:
|
||||
Iy = self.dx * (self.Hx[x, y, z] - self.Hx[x, y, z - 1]) + self.dz * (
|
||||
self.Hz[x - 1, y, z] - self.Hz[x, y, z]
|
||||
)
|
||||
|
||||
elif waveform.type == "user":
|
||||
results["error"] = "user waveform detected."
|
||||
return Iy
|
||||
|
||||
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)
|
||||
def calculate_Iz(self, x: int, y: int, z: int) -> float:
|
||||
"""Calculates the y-component of current at a grid position.
|
||||
|
||||
# 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]
|
||||
Args:
|
||||
x: x coordinate of position in grid
|
||||
y: y coordinate of position in grid
|
||||
z: z coordinate of position in grid
|
||||
"""
|
||||
|
||||
# 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."
|
||||
)
|
||||
if x == 0 or y == 0:
|
||||
Iz = 0
|
||||
else:
|
||||
Iz = self.dx * (self.Hx[x, y - 1, z] - self.Hx[x, y, z]) + self.dy * (
|
||||
self.Hy[x, y, z] - self.Hy[x - 1, y, z]
|
||||
)
|
||||
|
||||
# Ignore case where someone is using a waveform with zero amplitude, i.e. on a receiver
|
||||
elif waveform.amp == 0:
|
||||
pass
|
||||
return Iz
|
||||
|
||||
# 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"]
|
||||
def dispersion_analysis(self, iterations: int):
|
||||
# Check to see if numerical dispersion might be a problem
|
||||
results = self._dispersion_analysis(iterations)
|
||||
if results["error"]:
|
||||
logger.warning(
|
||||
f"\nNumerical dispersion analysis [{self.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 [{self.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"]
|
||||
):
|
||||
# Numerical phase velocity
|
||||
vp = np.pi / (results["N"] * np.arcsin((1 / S) * np.sin((np.pi * S) / results["N"])))
|
||||
logger.warning(
|
||||
f"\n[{self.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 [{self.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"
|
||||
)
|
||||
|
||||
# Physical phase velocity error (percentage)
|
||||
results["deltavp"] = (((vp * config.c) - config.c) / config.c) * 100
|
||||
def _dispersion_analysis(self, iterations: int):
|
||||
"""Analysis of numerical dispersion (Taflove et al, 2005, p112) -
|
||||
worse case of maximum frequency and minimum wavelength
|
||||
|
||||
# Store rounded down value of grid sampling density
|
||||
results["N"] = int(np.floor(results["N"]))
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
|
||||
return results
|
||||
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 self.waveforms:
|
||||
for waveform in self.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 / self.dt)
|
||||
iterations = min(iterations, iterations)
|
||||
waveformvalues = np.zeros(iterations)
|
||||
for iteration in range(iterations):
|
||||
waveformvalues[iteration] = waveform.calculate_value(
|
||||
iteration * self.dt, self.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, self.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 self.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 self.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(self.dx, self.dy, self.dz)
|
||||
elif "2D" in config.get_model_config().mode:
|
||||
if self.nx == 1:
|
||||
delta = max(self.dy, self.dz)
|
||||
elif self.ny == 1:
|
||||
delta = max(self.dx, self.dz)
|
||||
elif self.nz == 1:
|
||||
delta = max(self.dx, self.dy)
|
||||
|
||||
# Courant stability factor
|
||||
S = (config.c * self.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
|
||||
|
187
gprMax/model.py
187
gprMax/model.py
@@ -17,9 +17,9 @@
|
||||
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import logging
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
import humanize
|
||||
import numpy as np
|
||||
@@ -28,20 +28,17 @@ from colorama import Fore, Style, init
|
||||
|
||||
from gprMax.grid.cuda_grid import CUDAGrid
|
||||
from gprMax.grid.opencl_grid import OpenCLGrid
|
||||
from gprMax.subgrids.grid import SubGridBaseGrid
|
||||
|
||||
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.fdtd_grid import FDTDGrid, dispersion_analysis
|
||||
from .materials import process_materials
|
||||
from .pml import CFS, build_pml, print_pml_info
|
||||
from .geometry_outputs import GeometryObjects, GeometryView, save_geometry_views
|
||||
from .grid.fdtd_grid import FDTDGrid
|
||||
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
|
||||
@@ -53,7 +50,27 @@ class Model:
|
||||
"""Builds and runs (solves) a model."""
|
||||
|
||||
def __init__(self):
|
||||
self.title = ""
|
||||
|
||||
self.gnx = 0
|
||||
self.gny = 0
|
||||
self.gnz = 0
|
||||
|
||||
self.dt_mod = 1.0 # Time step stability factor
|
||||
|
||||
self.iteration = 0 # Current iteration number
|
||||
self.iterations = 0 # Total number of iterations
|
||||
self.timewindow = 0.0
|
||||
|
||||
self.srcsteps: List[int] = [0, 0, 0]
|
||||
self.rxsteps: List[int] = [0, 0, 0]
|
||||
|
||||
self.G = self._create_grid()
|
||||
self.subgrids: List[SubGridBaseGrid] = []
|
||||
|
||||
self.geometryviews: List[GeometryView] = []
|
||||
self.geometryobjects: List[GeometryObjects] = []
|
||||
|
||||
# Monitor memory usage
|
||||
self.p = None
|
||||
|
||||
@@ -63,6 +80,70 @@ class Model:
|
||||
# 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) -> np.ndarray:
|
||||
return self.G.dl
|
||||
|
||||
@dl.setter
|
||||
def dl(self, value: np.ndarray):
|
||||
self.G.dl = value
|
||||
|
||||
@property
|
||||
def dt(self) -> float:
|
||||
return self.G.dt
|
||||
|
||||
@dt.setter
|
||||
def dt(self, value: float):
|
||||
self.G.dt = value
|
||||
|
||||
def _create_grid(self) -> FDTDGrid:
|
||||
"""Create grid object according to solver.
|
||||
|
||||
@@ -95,36 +176,92 @@ class Model:
|
||||
|
||||
# Adjust position of simple sources and receivers if required
|
||||
model_num = config.sim_config.current_model
|
||||
G.update_simple_source_positions(step=model_num)
|
||||
G.update_receiver_positions(step=model_num)
|
||||
G.update_simple_source_positions(self.srcsteps, step=model_num)
|
||||
G.update_receiver_positions(self.rxsteps, step=model_num)
|
||||
|
||||
# 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:
|
||||
if (
|
||||
not self.geometryviews
|
||||
and not self.geometryobjects
|
||||
and config.sim_config.args.geometry_only
|
||||
):
|
||||
logger.exception("\nNo geometry views or geometry objects found.")
|
||||
raise ValueError
|
||||
save_geometry_views(gvs)
|
||||
save_geometry_views(self.geometryviews)
|
||||
|
||||
if G.geometryobjectswrite:
|
||||
if self.geometryobjects:
|
||||
logger.info("")
|
||||
for i, go in enumerate(G.geometryobjectswrite):
|
||||
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(G.geometryobjectswrite)}, "
|
||||
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(G, pbar)
|
||||
go.write_hdf5(self.title, self.G, pbar)
|
||||
pbar.close()
|
||||
logger.info("")
|
||||
|
||||
def build_geometry(self):
|
||||
logger.info(config.get_model_config().inputfilestr)
|
||||
self.G.build()
|
||||
|
||||
# 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)
|
||||
|
||||
# TODO: Make this correctly set local nx, ny and nz when using MPI (likely use a function inside FDTDGrid/MPIGrid)
|
||||
self.G.nx = self.gnx
|
||||
self.G.ny = self.gny
|
||||
self.G.nz = self.gnz
|
||||
|
||||
for grid in grids:
|
||||
grid.build()
|
||||
grid.dispersion_analysis(self.iterations)
|
||||
|
||||
def _check_for_dispersive_materials(self, grids: List[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: List[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'\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)}"
|
||||
)
|
||||
|
||||
def reuse_geometry(self):
|
||||
s = (
|
||||
@@ -136,8 +273,8 @@ class Model:
|
||||
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
|
||||
self.iteration = 0 # Reset current iteration number
|
||||
for grid in [self.G] + self.subgrids:
|
||||
grid.reset_fields()
|
||||
|
||||
def write_output_data(self):
|
||||
@@ -146,13 +283,13 @@ class Model:
|
||||
"""
|
||||
|
||||
# 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]
|
||||
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.G)
|
||||
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.G.subgrids:
|
||||
for grid in [self.G] + self.subgrids:
|
||||
if grid.snapshots:
|
||||
save_snapshots(grid)
|
||||
|
||||
@@ -204,14 +341,14 @@ class Model:
|
||||
# Prepare iterator
|
||||
if config.sim_config.general["progressbars"]:
|
||||
iterator = tqdm(
|
||||
range(self.G.iterations),
|
||||
range(self.iterations),
|
||||
desc="|--->",
|
||||
ncols=get_terminal_width() - 1,
|
||||
file=sys.stdout,
|
||||
disable=not config.sim_config.general["progressbars"],
|
||||
)
|
||||
else:
|
||||
iterator = range(self.G.iterations)
|
||||
iterator = range(self.iterations)
|
||||
|
||||
# Run solver
|
||||
solver.solve(iterator)
|
||||
|
@@ -29,14 +29,14 @@ 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.xcoord: int
|
||||
self.ycoord: int
|
||||
self.zcoord: int
|
||||
self.xcoordorigin: int
|
||||
self.ycoordorigin: int
|
||||
self.zcoordorigin: int
|
||||
|
||||
|
||||
def htod_rx_arrays(G, queue=None):
|
||||
@@ -63,7 +63,8 @@ def htod_rx_arrays(G, queue=None):
|
||||
# Array to store field components for receivers on compute device -
|
||||
# rows are field components; columns are iterations; pages are receivers
|
||||
rxs = np.zeros(
|
||||
(len(Rx.allowableoutputs_dev), G.iterations, len(G.rxs)), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(len(Rx.allowableoutputs_dev), G.iterations, len(G.rxs)),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
|
||||
# Copy arrays to compute device
|
||||
|
@@ -15,10 +15,10 @@
|
||||
#
|
||||
# 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, Union
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from gprMax import config
|
||||
from gprMax.cmds_geometry.add_grass import AddGrass
|
||||
from gprMax.cmds_geometry.add_surface_roughness import AddSurfaceRoughness
|
||||
from gprMax.cmds_geometry.add_surface_water import AddSurfaceWater
|
||||
@@ -28,9 +28,10 @@ from gprMax.cmds_multiuse import UserObjectMulti
|
||||
from gprMax.cmds_singleuse import Discretisation, Domain, TimeWindow, UserObjectSingle
|
||||
from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
from gprMax.materials import create_built_in_materials
|
||||
from gprMax.model import Model
|
||||
from gprMax.subgrids.grid import SubGridBaseGrid
|
||||
from gprMax.subgrids.user_objects import SubGridBase as SubGridUserBase
|
||||
from gprMax.user_inputs import create_user_input_points
|
||||
from gprMax.user_inputs import MainGridUserInput, SubgridUserInput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,6 +40,9 @@ class Scene:
|
||||
"""Scene stores all of the user created objects."""
|
||||
|
||||
def __init__(self):
|
||||
self.multiple_cmds: List[UserObjectMulti] = []
|
||||
self.single_cmds: List[UserObjectSingle] = []
|
||||
self.geometry_cmds: List[UserObjectGeometry] = []
|
||||
self.multiple_cmds: List[UserObjectMulti] = []
|
||||
self.single_cmds: List[UserObjectSingle] = []
|
||||
self.geometry_cmds: List[UserObjectGeometry] = []
|
||||
@@ -61,8 +65,8 @@ class Scene:
|
||||
logger.exception("This object is unknown to gprMax")
|
||||
raise ValueError
|
||||
|
||||
def build_obj(self, obj, grid):
|
||||
"""Builds objects.
|
||||
def build_grid_obj(self, obj: UserObjectGeometry, grid: FDTDGrid):
|
||||
"""Builds objects in FDTDGrids.
|
||||
|
||||
Args:
|
||||
obj: user object
|
||||
@@ -75,13 +79,38 @@ class Scene:
|
||||
logger.exception("Error creating user input object")
|
||||
raise
|
||||
|
||||
def process_subgrid_cmds(self):
|
||||
def build_model_obj(
|
||||
self,
|
||||
obj: Union[UserObjectSingle, UserObjectMulti],
|
||||
model: Model,
|
||||
subgrid: Optional[FDTDGrid] = None,
|
||||
):
|
||||
"""Builds objects in models.
|
||||
|
||||
Args:
|
||||
obj: user object
|
||||
model: Model being built
|
||||
"""
|
||||
|
||||
grid = model.G if subgrid is None else subgrid
|
||||
uip = create_user_input_points(grid, obj)
|
||||
|
||||
try:
|
||||
obj.build(model, uip)
|
||||
except ValueError:
|
||||
logger.exception("Error creating user input object")
|
||||
raise
|
||||
|
||||
def process_subgrid_cmds(self, model: Model):
|
||||
"""Process all commands in any sub-grids."""
|
||||
|
||||
# Subgrid user objects
|
||||
subgrid_cmds = [
|
||||
sg_cmd for sg_cmd in self.multiple_cmds if isinstance(sg_cmd, SubGridUserBase)
|
||||
]
|
||||
subgrid_cmds = [
|
||||
sg_cmd for sg_cmd in self.multiple_cmds if isinstance(sg_cmd, SubGridUserBase)
|
||||
]
|
||||
|
||||
# Iterate through the user command objects under the subgrid user object
|
||||
for sg_cmd in subgrid_cmds:
|
||||
@@ -89,16 +118,19 @@ class Scene:
|
||||
# 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_cmds(sg_cmd.children_multiple, model, sg)
|
||||
self.process_geocmds(sg_cmd.children_geometry, sg)
|
||||
|
||||
def process_cmds(
|
||||
self, commands: Union[List[UserObjectMulti], List[UserObjectSingle]], grid: FDTDGrid
|
||||
self,
|
||||
commands: Union[List[UserObjectMulti], List[UserObjectSingle]],
|
||||
model: Model,
|
||||
subgrid: Optional[SubGridBaseGrid] = None,
|
||||
):
|
||||
"""Process list of commands."""
|
||||
cmds_sorted = sorted(commands, key=lambda cmd: cmd.order)
|
||||
for obj in cmds_sorted:
|
||||
self.build_obj(obj, grid)
|
||||
self.build_model_obj(obj, model, subgrid)
|
||||
|
||||
return self
|
||||
|
||||
@@ -107,7 +139,7 @@ class Scene:
|
||||
proc_cmds = []
|
||||
for obj in commands:
|
||||
if isinstance(obj, (FractalBox, AddGrass, AddSurfaceRoughness, AddSurfaceWater)):
|
||||
self.build_obj(obj, grid)
|
||||
self.build_grid_obj(obj, grid)
|
||||
if isinstance(obj, (FractalBox)):
|
||||
proc_cmds.append(obj)
|
||||
else:
|
||||
@@ -115,11 +147,11 @@ class Scene:
|
||||
|
||||
# Process all geometry commands
|
||||
for obj in proc_cmds:
|
||||
self.build_obj(obj, grid)
|
||||
self.build_grid_obj(obj, grid)
|
||||
|
||||
return self
|
||||
|
||||
def process_singlecmds(self, G: FDTDGrid):
|
||||
def process_singlecmds(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):
|
||||
@@ -137,29 +169,48 @@ class Scene:
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
self.process_cmds(cmds_unique, G)
|
||||
self.process_cmds(cmds_unique, model)
|
||||
|
||||
def create_internal_objects(self, G):
|
||||
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_singlecmds(model)
|
||||
|
||||
# Process main grid multiple commands
|
||||
self.process_cmds(self.multiple_cmds, G)
|
||||
self.process_cmds(self.multiple_cmds, 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_geocmds(self.geometry_cmds, model.G)
|
||||
|
||||
# Process all the commands for subgrids
|
||||
self.process_subgrid_cmds()
|
||||
self.process_subgrid_cmds(model)
|
||||
|
||||
|
||||
def create_user_input_points(
|
||||
grid: FDTDGrid, user_obj: Union[UserObjectSingle, UserObjectMulti, UserObjectGeometry]
|
||||
) -> Union[MainGridUserInput, SubgridUserInput]:
|
||||
"""Returns a point checker class based on the grid supplied."""
|
||||
|
||||
if isinstance(grid, SubGridBaseGrid):
|
||||
# Local object configuration trumps. User can turn off autotranslate for
|
||||
# specific objects.
|
||||
if not user_obj.autotranslate and config.sim_config.args.autotranslate:
|
||||
return MainGridUserInput(grid)
|
||||
|
||||
if config.sim_config.args.autotranslate:
|
||||
return SubgridUserInput(grid)
|
||||
else:
|
||||
return MainGridUserInput(grid)
|
||||
else:
|
||||
return MainGridUserInput(grid)
|
||||
|
@@ -19,6 +19,7 @@
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
@@ -55,7 +56,9 @@ def save_snapshots(grid):
|
||||
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} "
|
||||
f"of {len(grid.snapshots)}, "
|
||||
f"{snap.filename.name}",
|
||||
ncols=get_terminal_width() - 1,
|
||||
file=sys.stdout,
|
||||
disable=not config.sim_config.general["progressbars"],
|
||||
@@ -86,19 +89,19 @@ class Snapshot:
|
||||
|
||||
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],
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -142,7 +145,9 @@ class Snapshot:
|
||||
else:
|
||||
# If output is not required for snapshot just use a mimimal
|
||||
# size of array - still required to pass to Cython function
|
||||
self.snapfields[k] = np.zeros((1, 1, 1), dtype=config.sim_config.dtypes["float_or_double"])
|
||||
self.snapfields[k] = np.zeros(
|
||||
(1, 1, 1), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
|
||||
def store(self, G):
|
||||
"""Store (in memory) electric and magnetic field values for snapshot.
|
||||
@@ -186,7 +191,7 @@ class Snapshot:
|
||||
self.snapfields["Hz"],
|
||||
)
|
||||
|
||||
def write_file(self, pbar, G):
|
||||
def write_file(self, pbar: tqdm, G):
|
||||
"""Writes snapshot file either as VTK ImageData (.vti) format
|
||||
or HDF5 format (.h5) files
|
||||
|
||||
@@ -200,7 +205,7 @@ class Snapshot:
|
||||
elif self.fileext == ".h5":
|
||||
self.write_hdf5(pbar, G)
|
||||
|
||||
def write_vtk(self, pbar, G):
|
||||
def write_vtk(self, pbar: tqdm, G):
|
||||
"""Writes snapshot file in VTK ImageData (.vti) format.
|
||||
|
||||
Args:
|
||||
@@ -208,11 +213,19 @@ class Snapshot:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
|
||||
celldata = {k: self.snapfields[k] for k in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"] if self.outputs.get(k)}
|
||||
celldata = {
|
||||
k: self.snapfields[k]
|
||||
for k in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]
|
||||
if self.outputs.get(k)
|
||||
}
|
||||
|
||||
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)),
|
||||
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)),
|
||||
cellData=celldata,
|
||||
)
|
||||
@@ -225,7 +238,7 @@ class Snapshot:
|
||||
* np.dtype(config.sim_config.dtypes["float_or_double"]).itemsize
|
||||
)
|
||||
|
||||
def write_hdf5(self, pbar, G):
|
||||
def write_hdf5(self, pbar: tqdm, G):
|
||||
"""Writes snapshot file in HDF5 (.h5) format.
|
||||
|
||||
Args:
|
||||
@@ -235,7 +248,8 @@ class Snapshot:
|
||||
|
||||
f = h5py.File(self.filename, "w")
|
||||
f.attrs["gprMax"] = __version__
|
||||
f.attrs["Title"] = G.title
|
||||
# TODO: Output model name (title) and grid name? in snapshot output
|
||||
# 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
|
||||
@@ -271,35 +285,49 @@ def htod_snapshot_array(G, queue=None):
|
||||
if config.sim_config.general["solver"] == "cuda":
|
||||
# Blocks per grid - according to largest requested snapshot
|
||||
Snapshot.bpg = (
|
||||
int(np.ceil(((Snapshot.nx_max) * (Snapshot.ny_max) * (Snapshot.nz_max)) / Snapshot.tpb[0])),
|
||||
int(
|
||||
np.ceil(
|
||||
((Snapshot.nx_max) * (Snapshot.ny_max) * (Snapshot.nz_max)) / Snapshot.tpb[0]
|
||||
)
|
||||
),
|
||||
1,
|
||||
1,
|
||||
)
|
||||
elif config.sim_config.general["solver"] == "opencl":
|
||||
# Workgroup size - according to largest requested snapshot
|
||||
Snapshot.wgs = (int(np.ceil(((Snapshot.nx_max) * (Snapshot.ny_max) * (Snapshot.nz_max)))), 1, 1)
|
||||
Snapshot.wgs = (
|
||||
int(np.ceil(((Snapshot.nx_max) * (Snapshot.ny_max) * (Snapshot.nz_max)))),
|
||||
1,
|
||||
1,
|
||||
)
|
||||
|
||||
# 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)
|
||||
snapEx = np.zeros(
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
snapEy = np.zeros(
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
snapEz = np.zeros(
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
snapHx = np.zeros(
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
snapHy = np.zeros(
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
snapHz = np.zeros(
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
(numsnaps, Snapshot.nx_max, Snapshot.ny_max, Snapshot.nz_max),
|
||||
dtype=config.sim_config.dtypes["float_or_double"],
|
||||
)
|
||||
|
||||
# Copy arrays to compute device
|
||||
@@ -326,7 +354,9 @@ def htod_snapshot_array(G, queue=None):
|
||||
return snapEx_dev, snapEy_dev, snapEz_dev, snapHx_dev, snapHy_dev, snapHz_dev
|
||||
|
||||
|
||||
def dtoh_snapshot_array(snapEx_dev, snapEy_dev, snapEz_dev, snapHx_dev, snapHy_dev, snapHz_dev, i, snap):
|
||||
def dtoh_snapshot_array(
|
||||
snapEx_dev, snapEy_dev, snapEz_dev, snapHx_dev, snapHy_dev, snapHz_dev, i, snap
|
||||
):
|
||||
"""Copies snapshot array used on compute device back to snapshot objects and
|
||||
store in format for Paraview.
|
||||
|
||||
|
@@ -17,6 +17,7 @@
|
||||
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gprMax.config as config
|
||||
from gprMax.model import Model
|
||||
|
||||
from .grid.cuda_grid import CUDAGrid
|
||||
from .grid.fdtd_grid import FDTDGrid
|
||||
@@ -53,16 +54,17 @@ 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_magnetic_sources(iteration)
|
||||
if isinstance(self.updates, SubgridUpdates):
|
||||
self.updates.hsg_2()
|
||||
self.updates.update_electric_a()
|
||||
self.updates.update_electric_pml()
|
||||
self.updates.update_electric_sources()
|
||||
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()
|
||||
@@ -74,7 +76,7 @@ class Solver:
|
||||
self.updates.cleanup()
|
||||
|
||||
|
||||
def create_solver(grid: FDTDGrid) -> Solver:
|
||||
def create_solver(model: Model) -> Solver:
|
||||
"""Create configured solver object.
|
||||
|
||||
N.B. A large range of different functions exist to advance the time
|
||||
@@ -86,14 +88,14 @@ def create_solver(grid: FDTDGrid) -> Solver:
|
||||
substitution at runtime.
|
||||
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
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(grid)
|
||||
updates = create_subgrid_updates(model)
|
||||
if config.get_model_config().materials["maxpoles"] != 0:
|
||||
# Set dispersive update functions for both SubgridUpdates and
|
||||
# SubgridUpdaters subclasses
|
||||
|
@@ -21,8 +21,8 @@ from copy import deepcopy
|
||||
import numpy as np
|
||||
|
||||
import gprMax.config as config
|
||||
from gprMax.waveforms import Waveform
|
||||
|
||||
from .fields_outputs import Ix, Iy, Iz
|
||||
from .utilities.utilities import round_value
|
||||
|
||||
|
||||
@@ -30,40 +30,44 @@ 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.waveformID = None
|
||||
self.xcoord: int
|
||||
self.ycoord: int
|
||||
self.zcoord: int
|
||||
self.xcoordorigin: int
|
||||
self.ycoordorigin: int
|
||||
self.zcoordorigin: int
|
||||
self.start = 0.0
|
||||
self.stop = 0.0
|
||||
self.waveform: Waveform
|
||||
|
||||
def calculate_waveform_values(self, G):
|
||||
def calculate_waveform_values(self, iterations: int, dt: float):
|
||||
"""Calculates all waveform values for source for duration of simulation.
|
||||
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
# Waveform values for sources that need to be calculated on whole timesteps
|
||||
self.waveformvalues_wholedt = np.zeros((G.iterations), dtype=config.sim_config.dtypes["float_or_double"])
|
||||
self.waveformvalues_wholedt = np.zeros(
|
||||
iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
|
||||
# Waveform values for sources that need to be calculated on half timesteps
|
||||
self.waveformvalues_halfdt = np.zeros((G.iterations), dtype=config.sim_config.dtypes["float_or_double"])
|
||||
self.waveformvalues_halfdt = np.zeros(
|
||||
iterations, dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
|
||||
waveform = next(x for x in G.waveforms if x.ID == self.waveformID)
|
||||
|
||||
for iteration in range(G.iterations):
|
||||
time = G.dt * iteration
|
||||
for iteration in range(iterations):
|
||||
time = dt * iteration
|
||||
if time >= self.start and time <= self.stop:
|
||||
# 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_halfdt[iteration] = waveform.calculate_value(time + 0.5 * G.dt, G.dt)
|
||||
self.waveformvalues_wholedt[iteration] = self.waveform.calculate_value(time, dt)
|
||||
self.waveformvalues_halfdt[iteration] = self.waveform.calculate_value(
|
||||
time + 0.5 * dt, dt
|
||||
)
|
||||
|
||||
|
||||
class VoltageSource(Source):
|
||||
@@ -166,7 +170,7 @@ class HertzianDipole(Source):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.dl = None
|
||||
self.dl = 0.0
|
||||
|
||||
def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G):
|
||||
"""Updates electric field values for a Hertzian dipole.
|
||||
@@ -274,7 +278,9 @@ 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"])
|
||||
srcwaves = np.zeros((len(sources), G.iterations), dtype=config.sim_config.dtypes["float_or_double"])
|
||||
srcwaves = np.zeros(
|
||||
(len(sources), G.iterations), dtype=config.sim_config.dtypes["float_or_double"]
|
||||
)
|
||||
for i, src in enumerate(sources):
|
||||
srcinfo1[i, 0] = src.xcoord
|
||||
srcinfo1[i, 1] = src.ycoord
|
||||
@@ -324,14 +330,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
|
||||
@@ -339,11 +347,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
|
||||
@@ -353,10 +361,10 @@ class TransmissionLine(Source):
|
||||
|
||||
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.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_incident_V_I(self, G):
|
||||
"""Calculates the incident voltage and current with a long length
|
||||
@@ -367,7 +375,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)
|
||||
@@ -399,11 +407,15 @@ class TransmissionLine(Source):
|
||||
|
||||
# Update all the voltage values along the line
|
||||
self.voltage[1 : self.nl] -= (
|
||||
self.resistance * (config.c * G.dt / self.dl) * (self.current[1 : self.nl] - self.current[0 : self.nl - 1])
|
||||
self.resistance
|
||||
* (config.c * G.dt / self.dl)
|
||||
* (self.current[1 : self.nl] - self.current[0 : self.nl - 1])
|
||||
)
|
||||
|
||||
# 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)
|
||||
@@ -425,7 +437,9 @@ class TransmissionLine(Source):
|
||||
|
||||
# Update the current one cell before the position of the one-way injector excitation
|
||||
self.current[self.srcpos - 1] += (
|
||||
(1 / self.resistance) * (config.c * G.dt / self.dl) * self.waveformvalues_wholedt[iteration]
|
||||
(1 / self.resistance)
|
||||
* (config.c * G.dt / self.dl)
|
||||
* self.waveformvalues_wholedt[iteration]
|
||||
)
|
||||
|
||||
def update_electric(self, iteration, updatecoeffsE, ID, Ex, Ey, Ez, G):
|
||||
@@ -458,6 +472,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.
|
||||
@@ -478,12 +493,12 @@ 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)
|
||||
|
@@ -36,6 +36,8 @@ class SubGridBaseGrid(FDTDGrid, ABC):
|
||||
|
||||
# Name of the grid
|
||||
self.name = kwargs["id"]
|
||||
self.parent_grid: FDTDGrid
|
||||
self.iterations = 0
|
||||
|
||||
self.filter = kwargs["filter"]
|
||||
|
||||
|
@@ -18,6 +18,10 @@
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
@@ -25,24 +29,24 @@ 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
|
||||
|
@@ -43,6 +43,8 @@ class SubGridBase(UserObjectMulti):
|
||||
super().__init__(**kwargs)
|
||||
self.children_multiple: List[UserObjectMulti] = []
|
||||
self.children_geometry: List[UserObjectGeometry] = []
|
||||
self.children_multiple: List[UserObjectMulti] = []
|
||||
self.children_geometry: List[UserObjectGeometry] = []
|
||||
|
||||
def add(self, node: Union[UserObjectMulti, UserObjectGeometry]):
|
||||
"""Adds other user objects. Geometry and multi only."""
|
||||
@@ -86,24 +88,24 @@ class SubGridBase(UserObjectMulti):
|
||||
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: SubGridBaseGrid, main: FDTDGrid):
|
||||
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: SubGridBaseGrid, grid: FDTDGrid, uip: MainGridUserInput):
|
||||
def setup(self, sg: SubGridBaseGrid, model: Model, uip: MainGridUserInput):
|
||||
""" "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__())
|
||||
|
||||
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)
|
||||
@@ -121,29 +123,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")
|
||||
raise ValueError
|
||||
|
||||
# Reference the subgrid under the main grid to which it belongs
|
||||
grid.subgrids.append(sg)
|
||||
model.subgrids.append(sg)
|
||||
|
||||
|
||||
class SubGridHSG(SubGridBase):
|
||||
@@ -200,7 +200,7 @@ class SubGridHSG(SubGridBase):
|
||||
self.order = 18
|
||||
self.hash = "#subgrid_hsg"
|
||||
|
||||
def build(self, grid: FDTDGrid, uip: MainGridUserInput) -> SubGridHSGUser:
|
||||
def build(self, model: Model, uip: MainGridUserInput) -> SubGridHSGUser:
|
||||
sg = SubGridHSGUser(**self.kwargs)
|
||||
self.setup(sg, grid, uip)
|
||||
self.setup(sg, model, uip)
|
||||
return sg
|
||||
|
@@ -17,6 +17,9 @@
|
||||
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from importlib import import_module
|
||||
from typing import Generic
|
||||
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from gprMax import config
|
||||
from gprMax.cython.fields_updates_normal import update_electric as update_electric_cpu
|
||||
@@ -26,11 +29,13 @@ from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
from gprMax.updates.updates import Updates
|
||||
from gprMax.utilities.utilities import timer
|
||||
|
||||
GridType = TypeVar("GridType", bound=FDTDGrid, default=FDTDGrid)
|
||||
|
||||
class CPUUpdates(Updates):
|
||||
|
||||
class CPUUpdates(Generic[GridType], Updates):
|
||||
"""Defines update functions for CPU-based solver."""
|
||||
|
||||
def __init__(self, G: FDTDGrid):
|
||||
def __init__(self, G: GridType):
|
||||
"""
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
@@ -38,9 +43,9 @@ class CPUUpdates(Updates):
|
||||
|
||||
self.grid = G
|
||||
|
||||
def store_outputs(self):
|
||||
def store_outputs(self, iteration):
|
||||
"""Stores field component values for every receiver and transmission line."""
|
||||
store_outputs_cpu(self.grid)
|
||||
store_outputs_cpu(self.grid, iteration)
|
||||
|
||||
def store_snapshots(self, iteration):
|
||||
"""Stores any snapshots.
|
||||
@@ -74,11 +79,11 @@ class CPUUpdates(Updates):
|
||||
for pml in self.grid.pmls["slabs"]:
|
||||
pml.update_magnetic()
|
||||
|
||||
def update_magnetic_sources(self):
|
||||
def update_magnetic_sources(self, iteration):
|
||||
"""Updates magnetic field components from sources."""
|
||||
for source in self.grid.transmissionlines + self.grid.magneticdipoles:
|
||||
source.update_magnetic(
|
||||
self.grid.iteration,
|
||||
iteration,
|
||||
self.grid.updatecoeffsH,
|
||||
self.grid.ID,
|
||||
self.grid.Hx,
|
||||
@@ -134,7 +139,7 @@ class CPUUpdates(Updates):
|
||||
for pml in self.grid.pmls["slabs"]:
|
||||
pml.update_electric()
|
||||
|
||||
def update_electric_sources(self):
|
||||
def update_electric_sources(self, iteration):
|
||||
"""Updates electric field components from sources -
|
||||
update any Hertzian dipole sources last.
|
||||
"""
|
||||
@@ -142,7 +147,7 @@ class CPUUpdates(Updates):
|
||||
self.grid.voltagesources + self.grid.transmissionlines + self.grid.hertziandipoles
|
||||
):
|
||||
source.update_electric(
|
||||
self.grid.iteration,
|
||||
iteration,
|
||||
self.grid.updatecoeffsE,
|
||||
self.grid.ID,
|
||||
self.grid.Ex,
|
||||
@@ -150,7 +155,6 @@ class CPUUpdates(Updates):
|
||||
self.grid.Ez,
|
||||
self.grid,
|
||||
)
|
||||
self.grid.iteration += 1
|
||||
|
||||
def update_electric_b(self):
|
||||
"""If there are any dispersive materials do 2nd part of dispersive
|
||||
|
@@ -24,7 +24,7 @@ class Updates(ABC):
|
||||
"""Defines update functions for a solver."""
|
||||
|
||||
@abstractmethod
|
||||
def store_outputs(self) -> None:
|
||||
def store_outputs(self, iteration: int) -> None:
|
||||
"""Stores field component values for every receiver and transmission line."""
|
||||
pass
|
||||
|
||||
@@ -48,7 +48,7 @@ class Updates(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_magnetic_sources(self) -> None:
|
||||
def update_magnetic_sources(self, iteration: int) -> None:
|
||||
"""Updates magnetic field components from sources."""
|
||||
pass
|
||||
|
||||
@@ -63,7 +63,7 @@ class Updates(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_electric_sources(self) -> None:
|
||||
def update_electric_sources(self, iteration: int) -> None:
|
||||
"""Updates electric field components from sources -
|
||||
update any Hertzian dipole sources last.
|
||||
"""
|
||||
|
@@ -15,14 +15,17 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
import gprMax.config as config
|
||||
from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
from gprMax.subgrids.grid import SubGridBaseGrid
|
||||
|
||||
from .subgrids.grid import SubGridBaseGrid
|
||||
from .utilities.utilities import round_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,28 +39,13 @@ 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, default=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):
|
||||
@@ -92,7 +80,7 @@ class UserInput:
|
||||
return p * 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):
|
||||
@@ -108,7 +96,9 @@ class MainGridUserInput(UserInput):
|
||||
p = self.check_point(p, cmd_str, name)
|
||||
|
||||
if self.grid.within_pml(p):
|
||||
logger.warning(f"'{cmd_str}' sources and receivers should not normally be positioned within the PML.")
|
||||
logger.warning(
|
||||
f"'{cmd_str}' sources and receivers should not normally be positioned within the PML."
|
||||
)
|
||||
|
||||
return p
|
||||
|
||||
@@ -117,7 +107,9 @@ class MainGridUserInput(UserInput):
|
||||
p2 = self.check_point(p2, cmd_str, name="upper")
|
||||
|
||||
if np.greater(p1, p2).any():
|
||||
logger.exception(f"'{cmd_str}' the lower coordinates should be less than the upper coordinates.")
|
||||
logger.exception(
|
||||
f"'{cmd_str}' the lower coordinates should be less than the upper coordinates."
|
||||
)
|
||||
raise ValueError
|
||||
|
||||
return p1, p2
|
||||
@@ -142,7 +134,7 @@ class MainGridUserInput(UserInput):
|
||||
return super().discretise_point(p) * self.grid.dl
|
||||
|
||||
|
||||
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.
|
||||
@@ -152,7 +144,9 @@ class SubgridUserInput(MainGridUserInput):
|
||||
super().__init__(grid)
|
||||
|
||||
# Defines the region exposed to the user
|
||||
self.inner_bound = np.array([grid.n_boundary_cells_x, grid.n_boundary_cells_y, grid.n_boundary_cells_z])
|
||||
self.inner_bound = np.array(
|
||||
[grid.n_boundary_cells_x, grid.n_boundary_cells_y, grid.n_boundary_cells_z]
|
||||
)
|
||||
|
||||
self.outer_bound = np.subtract([grid.nx, grid.ny, grid.nz], self.inner_bound)
|
||||
|
||||
@@ -186,7 +180,9 @@ class SubgridUserInput(MainGridUserInput):
|
||||
# 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():
|
||||
logger.warning(f"'{cmd_str}' this object traverses the Outer Surface. This is an advanced feature.")
|
||||
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):
|
||||
|
121
tests/grid/test_fdtd_grid.py
普通文件
121
tests/grid/test_fdtd_grid.py
普通文件
@@ -0,0 +1,121 @@
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose, assert_array_equal
|
||||
from pytest import param
|
||||
|
||||
from gprMax.grid.fdtd_grid import FDTDGrid
|
||||
|
||||
|
||||
def get_current_in_3d_grid(
|
||||
get_current_func: Callable[[int, int, int], float], shape: tuple[int, ...]
|
||||
) -> np.ndarray:
|
||||
"""Helper function to get current as a 3D grid
|
||||
|
||||
Args:
|
||||
get_current_func: Function that returns the current value at a
|
||||
point on a grid.
|
||||
shape: shape of the grid
|
||||
|
||||
Returns:
|
||||
result: 3D grid containing current values
|
||||
"""
|
||||
result = np.empty(shape)
|
||||
for i in range(shape[0]):
|
||||
for j in range(shape[1]):
|
||||
for k in range(shape[2]):
|
||||
result[i, j, k] = get_current_func(i, j, k)
|
||||
return result
|
||||
|
||||
|
||||
def tile(a: float, b: float, n: int = 1) -> np.ndarray:
|
||||
"""Helper function to tile two numbers
|
||||
|
||||
Args:
|
||||
a: first value
|
||||
b: second value
|
||||
n: number of repititions
|
||||
|
||||
Returns:
|
||||
c: tiled numpy array
|
||||
"""
|
||||
return np.tile([[[a, b], [b, a]], [[b, a], [a, b]]], (n, n, n))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dy,dz,Hy,Hz,expected",
|
||||
[
|
||||
(0, 0, (1, 3), (0.5, 0.8), 0),
|
||||
(0.1, 0.5, (0, 0), (0.5, 0.8), -0.15),
|
||||
(0.1, 0.5, (1, 3), (0, 0), 0.2),
|
||||
(0.1, 0, (1, 3), (0.5, 0.8), 0.2),
|
||||
(0, 0.5, (1, 3), (0.5, 0.8), -0.15),
|
||||
(0.1, 0.5, (1, 3), (0.5, 0.8), 0.05),
|
||||
],
|
||||
)
|
||||
def test_calculate_Ix(dy, dz, Hy, Hz, expected, size=2):
|
||||
grid = FDTDGrid()
|
||||
grid.dy = dy
|
||||
grid.dz = dz
|
||||
grid.Hy = tile(Hy[0], Hy[1], size)
|
||||
grid.Hz = tile(Hz[0], Hz[1], size)
|
||||
|
||||
actual_current = get_current_in_3d_grid(grid.calculate_Ix, grid.Hy.shape)
|
||||
expected_current = tile(expected, -expected, size)
|
||||
expected_current[:, 0, :] = 0
|
||||
expected_current[:, :, 0] = 0
|
||||
|
||||
assert_allclose(actual_current, expected_current)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dx,dz,Hx,Hz,expected",
|
||||
[
|
||||
(0, 0, (1, 3), (0.5, 0.8), 0),
|
||||
(0.1, 0.5, (0, 0), (0.5, 0.8), -0.15),
|
||||
(0.1, 0.5, (1, 3), (0, 0), 0.2),
|
||||
(0.1, 0, (1, 3), (0.5, 0.8), 0.2),
|
||||
(0, 0.5, (1, 3), (0.5, 0.8), -0.15),
|
||||
(0.1, 0.5, (1, 3), (0.5, 0.8), 0.05),
|
||||
],
|
||||
)
|
||||
def test_calculate_Iy(dx, dz, Hx, Hz, expected, size=2):
|
||||
grid = FDTDGrid()
|
||||
grid.dx = dx
|
||||
grid.dz = dz
|
||||
grid.Hx = tile(Hx[0], Hx[1], size)
|
||||
grid.Hz = tile(Hz[0], Hz[1], size)
|
||||
|
||||
actual_current = get_current_in_3d_grid(grid.calculate_Iy, grid.Hx.shape)
|
||||
expected_current = tile(-expected, expected, size)
|
||||
expected_current[0, :, :] = 0
|
||||
expected_current[:, :, 0] = 0
|
||||
|
||||
assert_allclose(actual_current, expected_current)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dx,dy,Hx,Hy,expected",
|
||||
[
|
||||
(0, 0, (1, 3), (0.5, 0.8), 0),
|
||||
(0.1, 0.5, (0, 0), (0.5, 0.8), -0.15),
|
||||
(0.1, 0.5, (1, 3), (0, 0), 0.2),
|
||||
(0.1, 0, (1, 3), (0.5, 0.8), 0.2),
|
||||
(0, 0.5, (1, 3), (0.5, 0.8), -0.15),
|
||||
(0.1, 0.5, (1, 3), (0.5, 0.8), 0.05),
|
||||
],
|
||||
)
|
||||
def test_calculate_Iz(dx, dy, Hx, Hy, expected, size=2):
|
||||
grid = FDTDGrid()
|
||||
grid.dx = dx
|
||||
grid.dy = dy
|
||||
grid.Hx = tile(Hx[0], Hx[1], size)
|
||||
grid.Hy = tile(Hy[0], Hy[1], size)
|
||||
|
||||
actual_current = get_current_in_3d_grid(grid.calculate_Iz, grid.Hx.shape)
|
||||
expected_current = tile(expected, -expected, size)
|
||||
expected_current[0, :, :] = 0
|
||||
expected_current[:, 0, :] = 0
|
||||
|
||||
assert_allclose(actual_current, expected_current)
|
在新工单中引用
屏蔽一个用户