你已经派生过 gprMax
镜像自地址
https://gitee.com/sunhf/gprMax.git
已同步 2025-08-07 15:10:13 +08:00
Merge branch 'devel' of https://github.com/gprMax/gprMax into beta_1
这个提交包含在:
13
.github/FUNDING.yml
vendored
普通文件
13
.github/FUNDING.yml
vendored
普通文件
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: gprMax
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
二进制文件未显示。
二进制文件未显示。
@@ -41,7 +41,7 @@ gv2 = gprMax.GeometryView(p1=(ant_pos[0] - 0.170/2, ant_pos[1] - 0.108/2, ant_po
|
||||
p2=(ant_pos[0] + 0.170/2, ant_pos[1] + 0.108/2, ant_pos[2] + 0.010),
|
||||
dl=(dl, dl, dl), filename='antenna_like_GSSI_1500_pcb',
|
||||
output_type='f')
|
||||
scene.add(gv1)
|
||||
#scene.add(gv1)
|
||||
#scene.add(gv2)
|
||||
|
||||
# Run model
|
||||
|
二进制文件未显示。
@@ -89,17 +89,10 @@ gv1 = gprMax.GeometryView(p1=(0, 0, 0), p2=(x, y, z), dl=(dl, dl, dl),
|
||||
scene.add(gv1)
|
||||
|
||||
# Create some snapshots of entire domain
|
||||
# for i in range(5):
|
||||
# s = gprMax.Snapshot(p1=(0, 0, 0), p2=(x, y, z), dl=(dl, dl, dl),
|
||||
# time=(i + 0.5) * 1e-9,
|
||||
# filename=fn.with_suffix('').parts[-1] + '_' + str(i + 1))
|
||||
# scene.add(s)
|
||||
|
||||
for i in range(5):
|
||||
s = gprMax.Snapshot(p1=sg1, p2=sg2, dl=(dl_sg, dl_sg, dl_sg),
|
||||
s = gprMax.Snapshot(p1=(0, 0, 0), p2=(x, y, z), dl=(dl, dl, dl),
|
||||
time=(i + 0.5) * 1e-9,
|
||||
filename=fn.with_suffix('').parts[-1] + '_' + str(i + 1),
|
||||
outputs=['Ez'])
|
||||
subgrid.add(s)
|
||||
filename=fn.with_suffix('').parts[-1] + '_' + str(i + 1))
|
||||
scene.add(s)
|
||||
|
||||
gprMax.run(scenes=[scene], n=1, geometry_only=False, outputfile=fn, subgrid=True, autotranslate=True)
|
||||
|
@@ -25,8 +25,9 @@ from .cmds_geometry.triangle import Triangle
|
||||
from .cmds_multiuse import (PMLCFS, AddDebyeDispersion, AddDrudeDispersion,
|
||||
AddLorentzDispersion, GeometryObjectsWrite,
|
||||
GeometryView, HertzianDipole, MagneticDipole,
|
||||
Material, Rx, RxArray, Snapshot, SoilPeplinski,
|
||||
TransmissionLine, VoltageSource, Waveform, MaterialRange, MaterialList)
|
||||
Material, MaterialList, MaterialRange, Rx, RxArray,
|
||||
Snapshot, SoilPeplinski, TransmissionLine,
|
||||
VoltageSource, Waveform)
|
||||
from .cmds_singleuse import (Discretisation, Domain, ExcitationFile,
|
||||
OMPThreads, PMLProps, RxSteps, SrcSteps,
|
||||
TimeStepStabilityFactor, TimeWindow, Title)
|
||||
|
@@ -61,9 +61,9 @@ class UserObjectMulti:
|
||||
"""Readable user string as per hash commands."""
|
||||
s = ''
|
||||
for _, v in self.kwargs.items():
|
||||
if isinstance(v, tuple) or isinstance(v, list):
|
||||
if isinstance(v, (tuple, list)):
|
||||
v = ' '.join([str(el) for el in v])
|
||||
s += str(v) + ' '
|
||||
s += f'{str(v)} '
|
||||
|
||||
return f'{self.hash}: {s[:-1]}'
|
||||
|
||||
@@ -77,7 +77,7 @@ class UserObjectMulti:
|
||||
|
||||
def params_str(self):
|
||||
"""Readable string of parameters given to object."""
|
||||
return self.hash + ': ' + str(self.kwargs)
|
||||
return f'{self.hash}: {str(self.kwargs)}'
|
||||
|
||||
def grid_name(self, grid):
|
||||
"""Returns subgrid name for use with logging info. Returns an empty
|
||||
@@ -114,12 +114,12 @@ class Waveform(UserObjectMulti):
|
||||
try:
|
||||
wavetype = self.kwargs['wave_type'].lower()
|
||||
except KeyError:
|
||||
logger.exception(self.params_str() + (f" must have one of the "
|
||||
f"following types {','.join(WaveformUser.types)}."))
|
||||
logger.exception(f"{self.params_str()} must have one of the " +
|
||||
f"following types {','.join(WaveformUser.types)}.")
|
||||
raise
|
||||
if wavetype not in WaveformUser.types:
|
||||
logger.exception(self.params_str() + (f" must have one of the "
|
||||
f"following types {','.join(WaveformUser.types)}."))
|
||||
logger.exception(f"{self.params_str()} must have one of the " +
|
||||
f"following types {','.join(WaveformUser.types)}.")
|
||||
raise ValueError
|
||||
|
||||
if wavetype != 'user':
|
||||
@@ -817,16 +817,14 @@ class Rx(UserObjectMulti):
|
||||
outputs = [self.kwargs['outputs']]
|
||||
except KeyError:
|
||||
# If no ID or outputs are specified, use default
|
||||
r.ID = (r.__class__.__name__ + '(' + str(r.xcoord) + ',' +
|
||||
str(r.ycoord) + ',' + str(r.zcoord) + ')')
|
||||
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'])
|
||||
else:
|
||||
outputs.sort()
|
||||
# Get allowable outputs
|
||||
if (config.sim_config.general['solver'] =='cuda' or
|
||||
config.sim_config.general['solver'] =='opencl'):
|
||||
if config.sim_config.general['solver'] in ['cuda', 'opencl']:
|
||||
allowableoutputs = RxUser.allowableoutputs_dev
|
||||
else:
|
||||
allowableoutputs = RxUser.allowableoutputs
|
||||
@@ -836,14 +834,14 @@ class Rx(UserObjectMulti):
|
||||
r.outputs[field] = np.zeros(grid.iterations,
|
||||
dtype=config.sim_config.dtypes['float_or_double'])
|
||||
else:
|
||||
logger.exception(self.params_str() + ' contains an '
|
||||
'output type that is not allowable. '
|
||||
'Allowable outputs in current context are '
|
||||
logger.exception(f'{self.params_str()} contains an output '
|
||||
f'type that is not allowable. Allowable '
|
||||
f'outputs in current context are '
|
||||
f'{allowableoutputs}.')
|
||||
raise ValueError
|
||||
|
||||
logger.info(self.grid_name(grid) + f"Receiver at {p2[0]:g}m, "
|
||||
f"{p2[1]:g}m, {p2[2]:g}m with output component(s) "
|
||||
logger.info(f"{self.grid_name(grid)}Receiver at {p2[0]:g}m, {p2[1]:g}m, "
|
||||
f"{p2[2]:g}m with output component(s) "
|
||||
f"{', '.join(r.outputs)} created.")
|
||||
|
||||
grid.rxs.append(r)
|
||||
@@ -911,10 +909,10 @@ class RxArray(UserObjectMulti):
|
||||
'not be less than the spatial discretisation.')
|
||||
raise ValueError
|
||||
|
||||
logger.info(self.grid_name(grid) + f'Receiver array {p3[0]:g}m, '
|
||||
f'{p3[1]:g}m, {p3[2]:g}m, to {p4[0]:g}m, {p4[1]:g}m, '
|
||||
f'{p4[2]:g}m with steps {dx * grid.dx:g}m, '
|
||||
f'{dy * grid.dy:g}m, {dz * grid.dz:g}m')
|
||||
logger.info(f'{self.grid_name(grid)}Receiver array '
|
||||
f'{p3[0]:g}m, {p3[1]:g}m, {p3[2]:g}m, to '
|
||||
f'{p4[0]:g}m, {p4[1]:g}m, {p4[2]:g}m with steps '
|
||||
f'{dx * grid.dx:g}m, {dy * grid.dy:g}m, {dz * grid.dz:g}m')
|
||||
|
||||
for x in range(xs, xf + 1, dx):
|
||||
for y in range(ys, yf + 1, dy):
|
||||
@@ -930,8 +928,7 @@ class RxArray(UserObjectMulti):
|
||||
p5 = np.array([x, y, z])
|
||||
p5 = uip.descretised_to_continuous(p5)
|
||||
p5 = uip.round_to_grid_static_point(p5)
|
||||
r.ID = (r.__class__.__name__ + '(' + str(x) + ',' +
|
||||
str(y) + ',' + str(z) + ')')
|
||||
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'])
|
||||
logger.info(f" Receiver at {p5[0]:g}m, {p5[1]:g}m, "
|
||||
@@ -986,7 +983,7 @@ class Snapshot(UserObjectMulti):
|
||||
p4 = uip.round_to_grid_static_point(p2)
|
||||
p1, p2 = uip.check_box_points(p1, p2, self.params_str())
|
||||
except ValueError:
|
||||
logger.exception(self.params_str() + ' point is outside the domain.')
|
||||
logger.exception(f'{self.params_str()} point is outside the domain.')
|
||||
raise
|
||||
xs, ys, zs = p1
|
||||
xf, yf, zf = p2
|
||||
@@ -1027,10 +1024,10 @@ class Snapshot(UserObjectMulti):
|
||||
# Check and set output names
|
||||
for output in tmp:
|
||||
if output not in SnapshotUser.allowableoutputs.keys():
|
||||
logger.exception(self.params_str() + " contains an "
|
||||
"output type that is not allowable. "
|
||||
"Allowable outputs in current context are "
|
||||
f"{', '.join(SnapshotUser.allowableoutputs.keys())}.")
|
||||
logger.exception(f"{self.params_str()} contains an output "
|
||||
f"type that is not allowable. Allowable "
|
||||
f"outputs in current context are "
|
||||
f"{', '.join(SnapshotUser.allowableoutputs.keys())}.")
|
||||
raise ValueError
|
||||
else:
|
||||
outputs[output] = True
|
||||
@@ -1047,7 +1044,7 @@ class Snapshot(UserObjectMulti):
|
||||
'be less than the spatial discretisation.')
|
||||
raise ValueError
|
||||
if iterations <= 0 or iterations > grid.iterations:
|
||||
logger.exception(self.params_str() + ' time value is not valid.')
|
||||
logger.exception(f'{self.params_str()} time value is not valid.')
|
||||
raise ValueError
|
||||
|
||||
s = SnapshotUser(xs, ys, zs, xf, yf, zf, dx, dy, dz, iterations,
|
||||
@@ -1088,39 +1085,38 @@ class Material(UserObjectMulti):
|
||||
sm = self.kwargs['sm']
|
||||
material_id = self.kwargs['id']
|
||||
except KeyError:
|
||||
logger.exception(self.params_str() + ' requires exactly five '
|
||||
'parameters.')
|
||||
logger.exception(f'{self.params_str()} requires exactly five '
|
||||
f'parameters.')
|
||||
raise
|
||||
|
||||
if er < 1:
|
||||
logger.exception(self.params_str() + ' requires a positive value '
|
||||
'of one or greater for static (DC) permittivity.')
|
||||
logger.exception(f'{self.params_str()} requires a positive value of '
|
||||
f'one or greater for static (DC) permittivity.')
|
||||
raise ValueError
|
||||
if se != 'inf':
|
||||
se = float(se)
|
||||
if se < 0:
|
||||
logger.exception(self.params_str() + ' requires a positive '
|
||||
'value for electric conductivity.')
|
||||
logger.exception(f'{self.params_str()} requires a positive '
|
||||
f'value for electric conductivity.')
|
||||
raise ValueError
|
||||
else:
|
||||
se = float('inf')
|
||||
if mr < 1:
|
||||
logger.exception(self.params_str() + ' requires a positive value '
|
||||
'of one or greater for magnetic permeability.')
|
||||
logger.exception(f'{self.params_str()} requires a positive value of '
|
||||
f'one or greater for magnetic permeability.')
|
||||
raise ValueError
|
||||
if sm < 0:
|
||||
logger.exception(self.params_str() + ' requires a positive value '
|
||||
'for magnetic loss.')
|
||||
logger.exception(f'{self.params_str()} requires a positive value '
|
||||
f'for magnetic loss.')
|
||||
raise ValueError
|
||||
if any(x.ID == material_id for x in grid.materials):
|
||||
logger.exception(self.params_str() + f' with ID {material_id} '
|
||||
'already exists')
|
||||
logger.exception(f'{self.params_str()} with ID {material_id} '
|
||||
f'already exists')
|
||||
raise ValueError
|
||||
|
||||
# Create a new instance of the Material class material
|
||||
# (start index after pec & free_space)
|
||||
m = MaterialUser(len(grid.materials), material_id)
|
||||
m.er = er
|
||||
m.se = se
|
||||
m.mr = mr
|
||||
m.sm = sm
|
||||
@@ -1129,9 +1125,10 @@ class Material(UserObjectMulti):
|
||||
if m.se == float('inf'):
|
||||
m.averagable = False
|
||||
|
||||
logger.info(self.grid_name(grid) + f'Material {m.ID} with '
|
||||
f'eps_r={m.er:g}, sigma={m.se:g} S/m; mu_r={m.mr:g}, '
|
||||
f'sigma*={m.sm:g} Ohm/m created.')
|
||||
m.er = er
|
||||
logger.info(f'{self.grid_name(grid)}Material {m.ID} with eps_r={m.er:g}, '
|
||||
f'sigma={m.se:g} S/m; mu_r={m.mr:g}, sigma*={m.sm:g} Ohm/m '
|
||||
f'created.')
|
||||
|
||||
grid.materials.append(m)
|
||||
|
||||
@@ -1176,8 +1173,7 @@ class AddDebyeDispersion(UserObjectMulti):
|
||||
|
||||
if len(materials) != len(material_ids):
|
||||
notfound = [x for x in material_ids if x not in materials]
|
||||
logger.exception(self.params_str() + f' material(s) {notfound} do '
|
||||
'not exist')
|
||||
logger.exception(f'{self.params_str()} material(s) {notfound} do not exist')
|
||||
raise ValueError
|
||||
|
||||
for material in materials:
|
||||
@@ -1189,7 +1185,7 @@ class AddDebyeDispersion(UserObjectMulti):
|
||||
disp_material.type = 'debye'
|
||||
disp_material.poles = poles
|
||||
disp_material.averagable = False
|
||||
for i in range(0, poles):
|
||||
for i in range(poles):
|
||||
if tau[i] > 0:
|
||||
logger.debug('Not checking if relaxation times are '
|
||||
'greater than time-step.')
|
||||
@@ -1205,8 +1201,8 @@ class AddDebyeDispersion(UserObjectMulti):
|
||||
# Replace original material with newly created DispersiveMaterial
|
||||
grid.materials = [disp_material if mat.numID==material.numID else mat for mat in grid.materials]
|
||||
|
||||
logger.info(self.grid_name(grid) + f"Debye disperion added to {disp_material.ID} "
|
||||
f"with delta_eps_r={', '.join('%4.2f' % deltaer for deltaer in disp_material.deltaer)}, "
|
||||
logger.info(f"{self.grid_name(grid)}Debye disperion added to {disp_material.ID} "
|
||||
f"with delta_eps_r={', '.join('%4.2f' % deltaer for deltaer in disp_material.deltaer)}, "
|
||||
f"and tau={', '.join('%4.3e' % tau for tau in disp_material.tau)} secs created.")
|
||||
|
||||
|
||||
@@ -1252,8 +1248,7 @@ class AddLorentzDispersion(UserObjectMulti):
|
||||
|
||||
if len(materials) != len(material_ids):
|
||||
notfound = [x for x in material_ids if x not in materials]
|
||||
logger.exception(self.params_str() + f' material(s) {notfound} do '
|
||||
'not exist')
|
||||
logger.exception(f'{self.params_str()} material(s) {notfound} do not exist')
|
||||
raise ValueError
|
||||
|
||||
for material in materials:
|
||||
@@ -1265,7 +1260,7 @@ class AddLorentzDispersion(UserObjectMulti):
|
||||
disp_material.type = 'lorentz'
|
||||
disp_material.poles = poles
|
||||
disp_material.averagable = False
|
||||
for i in range(0, poles):
|
||||
for i in range(poles):
|
||||
if er_delta[i] > 0 and omega[i] > grid.dt and delta[i] > grid.dt:
|
||||
disp_material.deltaer.append(er_delta[i])
|
||||
disp_material.tau.append(omega[i])
|
||||
@@ -1283,10 +1278,11 @@ class AddLorentzDispersion(UserObjectMulti):
|
||||
# Replace original material with newly created DispersiveMaterial
|
||||
grid.materials = [disp_material if mat.numID==material.numID else mat for mat in grid.materials]
|
||||
|
||||
logger.info(self.grid_name(grid) + f"Lorentz disperion added to {disp_material.ID} "
|
||||
f"with delta_eps_r={', '.join('%4.2f' % deltaer for deltaer in disp_material.deltaer)}, "
|
||||
f"omega={', '.join('%4.3e' % omega for omega in disp_material.tau)} secs, "
|
||||
f"and gamma={', '.join('%4.3e' % delta for delta in disp_material.alpha)} created.")
|
||||
logger.info(
|
||||
f"{self.grid_name(grid)}Lorentz disperion added to {disp_material.ID} "
|
||||
f"with delta_eps_r={', '.join('%4.2f' % deltaer for deltaer in disp_material.deltaer)}, "
|
||||
f"omega={', '.join('%4.3e' % omega for omega in disp_material.tau)} secs, "
|
||||
f"and gamma={', '.join('%4.3e' % delta for delta in disp_material.alpha)} created.")
|
||||
|
||||
|
||||
class AddDrudeDispersion(UserObjectMulti):
|
||||
@@ -1327,8 +1323,7 @@ class AddDrudeDispersion(UserObjectMulti):
|
||||
|
||||
if len(materials) != len(material_ids):
|
||||
notfound = [x for x in material_ids if x not in materials]
|
||||
logger.exception(self.params_str() + f' material(s) {notfound} do '
|
||||
'not exist.')
|
||||
logger.exception(f'{self.params_str()} material(s) {notfound} do not exist.')
|
||||
raise ValueError
|
||||
|
||||
for material in materials:
|
||||
@@ -1340,7 +1335,7 @@ class AddDrudeDispersion(UserObjectMulti):
|
||||
disp_material.type = 'drude'
|
||||
disp_material.poles = poles
|
||||
disp_material.averagable = False
|
||||
for i in range(0, poles):
|
||||
for i in range(poles):
|
||||
if omega[i] > 0 and alpha[i] > grid.dt:
|
||||
disp_material.tau.append(omega[i])
|
||||
disp_material.alpha.append(alpha[i])
|
||||
@@ -1356,9 +1351,10 @@ class AddDrudeDispersion(UserObjectMulti):
|
||||
# Replace original material with newly created DispersiveMaterial
|
||||
grid.materials = [disp_material if mat.numID==material.numID else mat for mat in grid.materials]
|
||||
|
||||
logger.info(self.grid_name(grid) + f"Drude disperion added to {disp_material.ID} "
|
||||
f"with omega={', '.join('%4.3e' % omega for omega in disp_material.tau)} secs, "
|
||||
f"and gamma={', '.join('%4.3e' % alpha for alpha in disp_material.alpha)} secs created.")
|
||||
logger.info(
|
||||
f"{self.grid_name(grid)}Drude disperion added to {disp_material.ID} "
|
||||
f"with omega={', '.join('%4.3e' % omega for omega in disp_material.tau)} secs, "
|
||||
f"and gamma={', '.join('%4.3e' % alpha for alpha in disp_material.alpha)} secs created.")
|
||||
|
||||
|
||||
class SoilPeplinski(UserObjectMulti):
|
||||
@@ -1423,7 +1419,7 @@ class SoilPeplinski(UserObjectMulti):
|
||||
'fraction.')
|
||||
raise ValueError
|
||||
if any(x.ID == ID for x in grid.mixingmodels):
|
||||
logger.exception(self.params_str() + f' with ID {ID} already exists')
|
||||
logger.exception(f'{self.params_str()} with ID {ID} already exists')
|
||||
raise ValueError
|
||||
|
||||
# Create a new instance of the Material class material
|
||||
@@ -1519,7 +1515,8 @@ class MaterialRange(UserObjectMulti):
|
||||
|
||||
# Create a new instance of the Material class material
|
||||
# (start index after pec & free_space)
|
||||
s = RangeMaterialUser(ID, (er_lower, er_upper), (sigma_lower, sigma_upper), (mr_lower, mr_upper), (ro_lower, ro_upper))
|
||||
s = RangeMaterialUser(ID, (er_lower, er_upper), (sigma_lower, sigma_upper),
|
||||
(mr_lower, mr_upper), (ro_lower, ro_upper))
|
||||
|
||||
logger.info(self.grid_name(grid) + 'Material properties used to '
|
||||
f'create {s.ID} with range(s) {s.er[0]:g} to {s.er[1]:g}, relative permittivity '
|
||||
@@ -1553,8 +1550,7 @@ class MaterialList(UserObjectMulti):
|
||||
logger.exception(self.params_str() + ' requires at at least 2 '
|
||||
'parameters.')
|
||||
raise
|
||||
|
||||
|
||||
|
||||
if any(x.ID == ID for x in grid.mixingmodels):
|
||||
logger.exception(self.params_str() + f' with ID {ID} already exists')
|
||||
raise ValueError
|
||||
@@ -1570,7 +1566,6 @@ class MaterialList(UserObjectMulti):
|
||||
grid.mixingmodels.append(s)
|
||||
|
||||
|
||||
|
||||
class GeometryView(UserObjectMulti):
|
||||
"""Outputs to file(s) information about the geometry (mesh) of model.
|
||||
|
||||
@@ -1624,14 +1619,13 @@ class GeometryView(UserObjectMulti):
|
||||
p4 = uip.round_to_grid_static_point(p2)
|
||||
p1, p2 = uip.check_box_points(p1, p2, self.params_str())
|
||||
except ValueError:
|
||||
logger.exception(self.params_str() + ' point is outside the domain.')
|
||||
logger.exception(f'{self.params_str()} point is outside the domain.')
|
||||
raise
|
||||
xs, ys, zs = p1
|
||||
xf, yf, zf = p2
|
||||
|
||||
dx, dy, dz = uip.discretise_static_point(dl)
|
||||
|
||||
|
||||
if dx < 0 or dy < 0 or dz < 0:
|
||||
logger.exception(self.params_str() + ' the step size should not be '
|
||||
'less than zero.')
|
||||
@@ -1644,7 +1638,7 @@ class GeometryView(UserObjectMulti):
|
||||
logger.exception(self.params_str() + ' the step size should not '
|
||||
'be less than the spatial discretisation.')
|
||||
raise ValueError
|
||||
if output_type != 'n' and output_type != 'f':
|
||||
if output_type not in ['n', 'f']:
|
||||
logger.exception(self.params_str() + ' requires type to be either '
|
||||
'n (normal) or f (fine).')
|
||||
raise ValueError
|
||||
|
@@ -93,20 +93,20 @@ class Discretisation(UserObjectSingle):
|
||||
G.dl = np.array(self.kwargs['p1'])
|
||||
G.dx, G.dy, G.dz = self.kwargs['p1']
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' discretisation requires a point')
|
||||
logger.exception(f'{self.__str__()} discretisation requires a point')
|
||||
raise
|
||||
|
||||
if G.dl[0] <= 0:
|
||||
logger.exception(self.__str__() + ' discretisation requires the ' +
|
||||
'x-direction spatial step to be greater than zero')
|
||||
logger.exception(f'{self.__str__()} discretisation requires the '
|
||||
f'x-direction spatial step to be greater than zero')
|
||||
raise ValueError
|
||||
if G.dl[1] <= 0:
|
||||
logger.exception(self.__str__() + ' discretisation requires the ' +
|
||||
'y-direction spatial step to be greater than zero')
|
||||
logger.exception(f'{self.__str__()} discretisation requires the '
|
||||
f'y-direction spatial step to be greater than zero')
|
||||
raise ValueError
|
||||
if G.dl[2] <= 0:
|
||||
logger.exception(self.__str__() + ' discretisation requires the ' +
|
||||
'z-direction spatial step to be greater than zero')
|
||||
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')
|
||||
@@ -127,11 +127,12 @@ class Domain(UserObjectSingle):
|
||||
try:
|
||||
G.nx, G.ny, G.nz = uip.discretise_point(self.kwargs['p1'])
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' please specify a point')
|
||||
logger.exception(f'{self.__str__()} please specify a point')
|
||||
raise
|
||||
|
||||
if G.nx == 0 or G.ny == 0 or G.nz == 0:
|
||||
logger.exception(self.__str__() + ' requires at least one cell in every dimension')
|
||||
logger.exception(f'{self.__str__()} requires at least one cell in '
|
||||
f'every dimension')
|
||||
raise ValueError
|
||||
|
||||
logger.info(f"Domain size: {self.kwargs['p1'][0]:g} x {self.kwargs['p1'][1]:g} x " +
|
||||
@@ -181,12 +182,12 @@ class TimeStepStabilityFactor(UserObjectSingle):
|
||||
try:
|
||||
f = self.kwargs['f']
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' requires exactly one parameter')
|
||||
logger.exception(f'{self.__str__()} requires exactly one parameter')
|
||||
raise
|
||||
|
||||
if f <= 0 or f > 1:
|
||||
logger.exception(self.__str__() + ' requires the value of the time ' +
|
||||
'step stability factor to be between zero and one')
|
||||
logger.exception(f'{self.__str__()} requires the value of the time '
|
||||
f'step stability factor to be between zero and one')
|
||||
raise ValueError
|
||||
G.dt = G.dt * f
|
||||
|
||||
@@ -250,12 +251,12 @@ class OMPThreads(UserObjectSingle):
|
||||
try:
|
||||
n = self.kwargs['n']
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' requires exactly one parameter ' +
|
||||
'to specify the number of CPU OpenMP threads to use')
|
||||
logger.exception(f'{self.__str__()} requires exactly one parameter '
|
||||
f'to specify the number of CPU OpenMP threads to use')
|
||||
raise
|
||||
if n < 1:
|
||||
logger.exception(self.__str__() + ' requires the value to be an ' +
|
||||
'integer not less than one')
|
||||
logger.exception(f'{self.__str__()} requires the value to be an '
|
||||
f'integer not less than one')
|
||||
raise ValueError
|
||||
|
||||
config.get_model_config().ompthreads = set_omp_threads(n)
|
||||
@@ -302,7 +303,7 @@ class PMLProps(UserObjectSingle):
|
||||
G.pmls['thickness']['ymax'] = int(self.kwargs['ymax'])
|
||||
G.pmls['thickness']['zmax'] = int(self.kwargs['zmax'])
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' requires either one or six parameter(s)')
|
||||
logger.exception(f'{self.__str__()} requires either one or six parameter(s)')
|
||||
raise
|
||||
|
||||
if (2 * G.pmls['thickness']['x0'] >= G.nx or
|
||||
@@ -311,8 +312,8 @@ class PMLProps(UserObjectSingle):
|
||||
2 * G.pmls['thickness']['xmax'] >= G.nx or
|
||||
2 * G.pmls['thickness']['ymax'] >= G.ny or
|
||||
2 * G.pmls['thickness']['zmax'] >= G.nz):
|
||||
logger.exception(self.__str__() + ' has too many cells for the domain size')
|
||||
raise ValueError
|
||||
logger.exception(f'{self.__str__()} has too many cells for the domain size')
|
||||
raise ValueError
|
||||
|
||||
|
||||
class SrcSteps(UserObjectSingle):
|
||||
@@ -330,12 +331,12 @@ class SrcSteps(UserObjectSingle):
|
||||
try:
|
||||
G.srcsteps = uip.discretise_point(self.kwargs['p1'])
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' requires exactly three parameters')
|
||||
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'for each model run.')
|
||||
logger.info(f'Simple sources will step {G.srcsteps[0] * G.dx:g}m, ' +
|
||||
f'{G.srcsteps[1] * G.dy:g}m, {G.srcsteps[2] * G.dz:g}m ' +
|
||||
'for each model run.')
|
||||
|
||||
|
||||
class RxSteps(UserObjectSingle):
|
||||
@@ -353,12 +354,12 @@ class RxSteps(UserObjectSingle):
|
||||
try:
|
||||
G.rxsteps = uip.discretise_point(self.kwargs['p1'])
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' requires exactly three parameters')
|
||||
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'for each model run.')
|
||||
logger.info(f'All receivers will step {G.rxsteps[0] * G.dx:g}m, ' +
|
||||
f'{G.rxsteps[1] * G.dy:g}m, {G.rxsteps[2] * G.dz:g}m ' +
|
||||
'for each model run.')
|
||||
|
||||
|
||||
class ExcitationFile(UserObjectSingle):
|
||||
@@ -378,7 +379,7 @@ class ExcitationFile(UserObjectSingle):
|
||||
|
||||
def create(self, G, uip):
|
||||
try:
|
||||
kwargs = dict()
|
||||
kwargs = {}
|
||||
excitationfile = self.kwargs['filepath']
|
||||
kwargs['kind'] = self.kwargs['kind']
|
||||
kwargs['fill_value'] = self.kwargs['fill_value']
|
||||
@@ -389,7 +390,7 @@ class ExcitationFile(UserObjectSingle):
|
||||
args, varargs, keywords, defaults = inspect.getargspec(interpolate.interp1d)
|
||||
kwargs = dict(zip(reversed(args), reversed(defaults)))
|
||||
except KeyError:
|
||||
logger.exception(self.__str__() + ' requires either one or three parameter(s)')
|
||||
logger.exception(f'{self.__str__()} requires either one or three parameter(s)')
|
||||
raise
|
||||
|
||||
# See if file exists at specified path and if not try input file directory
|
||||
|
@@ -224,14 +224,14 @@ class SimulationConfig:
|
||||
# solver: cpu, cuda, opencl.
|
||||
# subgrid: whether the simulation uses sub-grids.
|
||||
# precision: data type for electromagnetic field output (single/double).
|
||||
# progressbars: progress bars on stdoout or not - switch off
|
||||
# progressbars when logging level is greater than
|
||||
# info (20)
|
||||
|
||||
self.general = {'solver': 'cpu',
|
||||
'subgrid': False,
|
||||
'precision': 'single'}
|
||||
|
||||
# Progress bars on stdoout or not - switch off progressbars when
|
||||
# logging level is greater than info (20)
|
||||
self.general['progressbars'] = False if args.log_level > 20 else True
|
||||
'precision': 'single',
|
||||
'progressbars': args.log_level <= 20}
|
||||
|
||||
self.em_consts = {'c': c, # Speed of light in free space (m/s)
|
||||
'e0': e0, # Permittivity of free space (F/m)
|
||||
|
@@ -277,14 +277,10 @@ class Comments():
|
||||
"""
|
||||
|
||||
# Comments for Paraview macro
|
||||
comments = {}
|
||||
|
||||
comments['gprMax_version'] = __version__
|
||||
comments['dx_dy_dz'] = self.dx_dy_dz_comment()
|
||||
comments['nx_ny_nz'] = self.nx_ny_nz_comment()
|
||||
|
||||
# Write the name and numeric ID for each material
|
||||
comments['Materials'] = self.materials_comment()
|
||||
comments = {'gprMax_version': __version__,
|
||||
'dx_dy_dz': self.dx_dy_dz_comment(),
|
||||
'nx_ny_nz': self.nx_ny_nz_comment(),
|
||||
'Materials': self.materials_comment()} # Write the name and numeric ID for each material
|
||||
|
||||
# Write information on PMLs, sources, and receivers
|
||||
if not self.materials_only:
|
||||
|
@@ -178,14 +178,14 @@ def write_processed_file(processedlines):
|
||||
for item in processedlines:
|
||||
f.write(f'{item}')
|
||||
|
||||
logger.info(f'Written input commands, after processing any Python code and ' +
|
||||
logger.info(f'Written input commands, after processing any Python code and '
|
||||
f'include commands, to file: {processedfile}\n')
|
||||
|
||||
|
||||
def check_cmd_names(processedlines, checkessential=True):
|
||||
"""Checks the validity of commands, i.e. are they gprMax commands,
|
||||
and that all essential commands are present.
|
||||
|
||||
|
||||
Args:
|
||||
processedlines: list of input commands after Python processing.
|
||||
checkessential: boolean to check for essential commands or not.
|
||||
@@ -212,6 +212,7 @@ def check_cmd_names(processedlines, checkessential=True):
|
||||
# - these will be lists within the dictionary
|
||||
multiplecmds = {key: [] for key in ['#geometry_view',
|
||||
'#geometry_objects_write', '#material',
|
||||
'#material_range', '#material_list',
|
||||
'#soil_peplinski',
|
||||
'#add_dispersion_debye',
|
||||
'#add_dispersion_lorentz',
|
||||
@@ -219,14 +220,14 @@ def check_cmd_names(processedlines, checkessential=True):
|
||||
'#waveform', '#voltage_source',
|
||||
'#hertzian_dipole', '#magnetic_dipole',
|
||||
'#transmission_line', '#rx', '#rx_array',
|
||||
'#snapshot', '#include_file', '#material_range', '#material_list']}
|
||||
'#snapshot', '#include_file']}
|
||||
|
||||
# Geometry object building commands that there can be multiple instances
|
||||
# of in a model - these will be lists within the dictionary
|
||||
geometrycmds = ['#geometry_objects_read', '#edge', '#plate', '#triangle',
|
||||
'#box', '#sphere', '#ellipsoid', '#cone', '#cylinder', '#cylindrical_sector',
|
||||
'#fractal_box', '#add_surface_roughness',
|
||||
'#add_surface_water', '#add_grass']
|
||||
'#box', '#sphere', '#ellipsoid', '#cone', '#cylinder',
|
||||
'#cylindrical_sector', '#fractal_box',
|
||||
'#add_surface_roughness', '#add_surface_water', '#add_grass']
|
||||
# List to store all geometry object commands in order from input file
|
||||
geometry = []
|
||||
|
||||
|
@@ -17,21 +17,22 @@
|
||||
# along with gprMax. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .cmds_geometry.add_grass import AddGrass
|
||||
from .cmds_geometry.add_surface_roughness import AddSurfaceRoughness
|
||||
from .cmds_geometry.add_surface_water import AddSurfaceWater
|
||||
from .cmds_geometry.box import Box
|
||||
from .cmds_geometry.cylinder import Cylinder
|
||||
from .cmds_geometry.cone import Cone
|
||||
from .cmds_geometry.cylinder import Cylinder
|
||||
from .cmds_geometry.cylindrical_sector import CylindricalSector
|
||||
from .cmds_geometry.edge import Edge
|
||||
from .cmds_geometry.ellipsoid import Ellipsoid
|
||||
from .cmds_geometry.fractal_box import FractalBox
|
||||
from .cmds_geometry.plate import Plate
|
||||
from .cmds_geometry.sphere import Sphere
|
||||
from .cmds_geometry.triangle import Triangle
|
||||
from .cmds_geometry.ellipsoid import Ellipsoid
|
||||
from .utilities.utilities import round_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -76,7 +77,8 @@ def process_geometrycmds(geometry):
|
||||
tmp = object.split()
|
||||
|
||||
if tmp[0] == '#geometry_objects_read:':
|
||||
from .cmds_geometry.geometry_objects_read import GeometryObjectsRead
|
||||
from .cmds_geometry.geometry_objects_read import \
|
||||
GeometryObjectsRead
|
||||
|
||||
if len(tmp) != 6:
|
||||
logger.exception("'" + ' '.join(tmp) + "'" +
|
||||
@@ -219,7 +221,6 @@ def process_geometrycmds(geometry):
|
||||
|
||||
scene_objects.append(cylinder)
|
||||
|
||||
|
||||
elif tmp[0] == '#cone:':
|
||||
if len(tmp) < 10:
|
||||
logger.exception("'" + ' '.join(tmp) + "'" +
|
||||
@@ -239,7 +240,7 @@ def process_geometrycmds(geometry):
|
||||
elif len(tmp) == 11:
|
||||
averaging = check_averaging(tmp[10].lower())
|
||||
cone = Cone(p1=p1, p2=p2, r1=r1, r2=r2, material_id=tmp[9],
|
||||
averaging=averaging)
|
||||
averaging=averaging)
|
||||
|
||||
# Uniaxial anisotropic case
|
||||
elif len(tmp) == 12:
|
||||
@@ -252,7 +253,6 @@ def process_geometrycmds(geometry):
|
||||
|
||||
scene_objects.append(cone)
|
||||
|
||||
|
||||
elif tmp[0] == '#cylindrical_sector:':
|
||||
if len(tmp) < 10:
|
||||
logger.exception("'" + ' '.join(tmp) + "'" +
|
||||
@@ -331,7 +331,6 @@ def process_geometrycmds(geometry):
|
||||
|
||||
scene_objects.append(sphere)
|
||||
|
||||
|
||||
elif tmp[0] == '#ellipsoid:':
|
||||
if len(tmp) < 8:
|
||||
logger.exception("'" + ' '.join(tmp) + "'" +
|
||||
@@ -345,17 +344,19 @@ def process_geometrycmds(geometry):
|
||||
|
||||
# Isotropic case with no user specified averaging
|
||||
if len(tmp) == 8:
|
||||
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr, material_id=tmp[7])
|
||||
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr,
|
||||
material_id=tmp[7])
|
||||
|
||||
# Isotropic case with user specified averaging
|
||||
elif len(tmp) == 9:
|
||||
averaging = check_averaging(tmp[8].lower())
|
||||
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr, material_id=tmp[7],
|
||||
averaging=averaging)
|
||||
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr,
|
||||
material_id=tmp[7], averaging=averaging)
|
||||
|
||||
# Uniaxial anisotropic case
|
||||
elif len(tmp) == 8:
|
||||
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr, material_id=tmp[7:])
|
||||
ellipsoid = Ellipsoid(p1=p1, xr=xr, yr=yr, zr=zr,
|
||||
material_id=tmp[7:])
|
||||
|
||||
else:
|
||||
logger.exception("'" + ' '.join(tmp) + "'" +
|
||||
@@ -364,8 +365,6 @@ def process_geometrycmds(geometry):
|
||||
|
||||
scene_objects.append(ellipsoid)
|
||||
|
||||
|
||||
|
||||
elif tmp[0] == '#fractal_box:':
|
||||
# Default is no dielectric smoothing for a fractal box
|
||||
|
||||
@@ -479,4 +478,4 @@ def process_geometrycmds(geometry):
|
||||
|
||||
scene_objects.append(grass)
|
||||
|
||||
return scene_objects
|
||||
return scene_objects
|
@@ -21,8 +21,9 @@ import logging
|
||||
from .cmds_multiuse import (AddDebyeDispersion, AddDrudeDispersion,
|
||||
AddLorentzDispersion, GeometryObjectsWrite,
|
||||
GeometryView, HertzianDipole, MagneticDipole,
|
||||
Material, Rx, RxArray, Snapshot, SoilPeplinski,
|
||||
TransmissionLine, VoltageSource, Waveform, MaterialRange, MaterialList)
|
||||
Material, MaterialList, MaterialRange, Rx, RxArray,
|
||||
Snapshot, SoilPeplinski, TransmissionLine,
|
||||
VoltageSource, Waveform)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -352,17 +353,16 @@ def process_multicmds(multicmds):
|
||||
' requires at exactly nine parameters')
|
||||
raise ValueError
|
||||
material_range = MaterialRange(er_lower=float(tmp[0]),
|
||||
er_upper=float(tmp[1]),
|
||||
sigma_lower=float(tmp[2]),
|
||||
sigma_upper=float(tmp[3]),
|
||||
mr_lower=float(tmp[4]),
|
||||
mr_upper=float(tmp[5]),
|
||||
ro_lower=float(tmp[6]),
|
||||
ro_upper=float(tmp[7]),
|
||||
id=tmp[8])
|
||||
er_upper=float(tmp[1]),
|
||||
sigma_lower=float(tmp[2]),
|
||||
sigma_upper=float(tmp[3]),
|
||||
mr_lower=float(tmp[4]),
|
||||
mr_upper=float(tmp[5]),
|
||||
ro_lower=float(tmp[6]),
|
||||
ro_upper=float(tmp[7]),
|
||||
id=tmp[8])
|
||||
scene_objects.append(material_range)
|
||||
|
||||
|
||||
cmdname = '#material_list'
|
||||
if multicmds[cmdname] is not None:
|
||||
for cmdinstance in multicmds[cmdname]:
|
||||
@@ -375,14 +375,11 @@ def process_multicmds(multicmds):
|
||||
|
||||
tokens = len(tmp)
|
||||
lmats = []
|
||||
for iter in range(0,tokens-1):
|
||||
for iter in range(tokens-1):
|
||||
lmats.append(tmp[iter])
|
||||
|
||||
|
||||
|
||||
material_list = MaterialList(list_of_materials=lmats,
|
||||
id=tmp[tokens-1])
|
||||
material_list = MaterialList(list_of_materials=lmats,
|
||||
id=tmp[tokens-1])
|
||||
scene_objects.append(material_list)
|
||||
|
||||
|
||||
return scene_objects
|
||||
return scene_objects
|
@@ -55,9 +55,8 @@ def process_singlecmds(singlecmds):
|
||||
if singlecmds[cmd] is not None:
|
||||
tmp = tuple(int(x) for x in singlecmds[cmd].split())
|
||||
if len(tmp) != 1:
|
||||
logger.exception(
|
||||
f'{cmd} requires exactly one parameter to specify the number of CPU OpenMP threads to use'
|
||||
)
|
||||
logger.exception(f'{cmd} requires exactly one parameter to specify '
|
||||
f'the number of CPU OpenMP threads to use')
|
||||
raise ValueError
|
||||
|
||||
omp_threads = OMPThreads(n=tmp[0])
|
||||
@@ -95,9 +94,8 @@ def process_singlecmds(singlecmds):
|
||||
if singlecmds[cmd] is not None:
|
||||
tmp = singlecmds[cmd].split()
|
||||
if len(tmp) != 1:
|
||||
logger.exception(
|
||||
f'{cmd} requires exactly one parameter to specify the time window. Either in seconds or number of iterations.'
|
||||
)
|
||||
logger.exception(f'{cmd} requires exactly one parameter to specify the '
|
||||
f'time window. Either in seconds or number of iterations.')
|
||||
raise ValueError
|
||||
tmp = tmp[0].lower()
|
||||
|
||||
|
@@ -213,79 +213,7 @@ class DispersiveMaterial(Material):
|
||||
er -= ersum
|
||||
|
||||
return er
|
||||
|
||||
|
||||
def process_materials(G):
|
||||
"""Processes complete list of materials - calculates update coefficients,
|
||||
stores in arrays, and builds text list of materials/properties
|
||||
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
|
||||
Returns:
|
||||
materialsdata: list of material IDs, names, and properties to
|
||||
print a table.
|
||||
"""
|
||||
|
||||
if config.get_model_config().materials['maxpoles'] == 0:
|
||||
materialsdata = [['\nID', '\nName', '\nType', '\neps_r', 'sigma\n[S/m]',
|
||||
'\nmu_r', 'sigma*\n[Ohm/m]', 'Dielectric\nsmoothable']]
|
||||
else:
|
||||
materialsdata = [['\nID', '\nName', '\nType', '\neps_r', 'sigma\n[S/m]',
|
||||
'Delta\neps_r', 'tau\n[s]', 'omega\n[Hz]', 'delta\n[Hz]',
|
||||
'gamma\n[Hz]', '\nmu_r', 'sigma*\n[Ohm/m]', 'Dielectric\nsmoothable']]
|
||||
|
||||
for material in G.materials:
|
||||
# Calculate update coefficients for specific material
|
||||
material.calculate_update_coeffsE(G)
|
||||
material.calculate_update_coeffsH(G)
|
||||
|
||||
# Add update coefficients to overall storage for all materials
|
||||
G.updatecoeffsE[material.numID, :] = material.CA, material.CBx, material.CBy, material.CBz, material.srce
|
||||
G.updatecoeffsH[material.numID, :] = material.DA, material.DBx, material.DBy, material.DBz, material.srcm
|
||||
|
||||
# Add update coefficients to overall storage for dispersive materials
|
||||
if hasattr(material, 'poles'):
|
||||
z = 0
|
||||
for pole in range(config.get_model_config().materials['maxpoles']):
|
||||
G.updatecoeffsdispersive[material.numID, z:z + 3] = (config.sim_config.em_consts['e0'] *
|
||||
material.eqt2[pole], material.eqt[pole], material.zt[pole])
|
||||
z += 3
|
||||
|
||||
# Construct information on material properties for printing table
|
||||
materialtext = [
|
||||
str(material.numID),
|
||||
material.ID[:50] if len(material.ID) > 50 else material.ID,
|
||||
material.type,
|
||||
f'{material.er:g}',
|
||||
f'{material.se:g}',
|
||||
]
|
||||
if config.get_model_config().materials['maxpoles'] > 0:
|
||||
if 'debye' in material.type:
|
||||
materialtext.append('\n'.join('{:g}'.format(deltaer) for deltaer in material.deltaer))
|
||||
materialtext.append('\n'.join('{:g}'.format(tau) for tau in material.tau))
|
||||
materialtext.extend(['', '', ''])
|
||||
elif 'lorentz' in material.type:
|
||||
materialtext.append(', '.join('{:g}'.format(deltaer) for deltaer in material.deltaer))
|
||||
materialtext.append('')
|
||||
materialtext.append(', '.join('{:g}'.format(tau) for tau in material.tau))
|
||||
materialtext.append(', '.join('{:g}'.format(alpha) for alpha in material.alpha))
|
||||
materialtext.append('')
|
||||
elif 'drude' in material.type:
|
||||
materialtext.extend(['', ''])
|
||||
materialtext.append(', '.join('{:g}'.format(tau) for tau in material.tau))
|
||||
materialtext.append('')
|
||||
materialtext.append(', '.join('{:g}'.format(alpha) for alpha in material.alpha))
|
||||
else:
|
||||
materialtext.extend(['', '', '', '', ''])
|
||||
|
||||
materialtext.extend(
|
||||
(f'{material.mr:g}', f'{material.sm:g}', material.averagable)
|
||||
)
|
||||
materialsdata.append(materialtext)
|
||||
|
||||
return materialsdata
|
||||
|
||||
|
||||
|
||||
class PeplinskiSoil:
|
||||
"""Soil objects that are characterised according to a mixing model
|
||||
@@ -351,12 +279,11 @@ class PeplinskiSoil:
|
||||
# The limiting values of the ranges are not included in this.
|
||||
|
||||
#mubins = np.linspace(self.mu[0], self.mu[1], nbins)
|
||||
mubins = np.linspace(self.mu[0], self.mu[1], nbins+1)
|
||||
mubins = np.linspace(self.mu[0], self.mu[1], nbins + 1)
|
||||
# Generate a range of volumetric water fraction values the mid-point of
|
||||
# each bin to make materials from
|
||||
#mumaterials = mubins + (mubins[1] - mubins[0]) / 2
|
||||
mumaterials = 0.5*(mubins[1:nbins+1] + mubins[0:nbins])
|
||||
|
||||
mumaterials = 0.5 * (mubins[1:nbins+1] + mubins[0:nbins])
|
||||
|
||||
# Create an iterator
|
||||
muiter = np.nditer(mumaterials, flags=['c_index'])
|
||||
@@ -400,18 +327,19 @@ class PeplinskiSoil:
|
||||
muiter.iternext()
|
||||
|
||||
|
||||
|
||||
class RangeMaterial:
|
||||
"""Material defined with a given range of parameters to be used for
|
||||
factal spatial disttibutions
|
||||
"""Material defined with a given range of parameters to be used for fractal
|
||||
spatial distributions.
|
||||
"""
|
||||
|
||||
def __init__(self, ID, er_range, sigma_range, mu_range, ro_range):
|
||||
"""
|
||||
Args:
|
||||
ID: string for name of the material.
|
||||
er_range: tuple of floats for relative permittivity range of the material.
|
||||
sigma_range: tuple of floats for electric conductivity range of the material.
|
||||
er_range: tuple of floats for relative permittivity range of the
|
||||
material.
|
||||
sigma_range: tuple of floats for electric conductivity range of the
|
||||
material.
|
||||
mu_range: tuple of floats for magnetic permeability of material.
|
||||
ro_range: tuple of floats for magnetic loss range of material.
|
||||
"""
|
||||
@@ -426,7 +354,6 @@ class RangeMaterial:
|
||||
# and assume that all must be sequentially numbered. This allows for more general mixing models
|
||||
self.matID = []
|
||||
|
||||
|
||||
def calculate_properties(self, nbins, G):
|
||||
"""Calculates the properties of the materials.
|
||||
|
||||
@@ -437,50 +364,50 @@ class RangeMaterial:
|
||||
|
||||
# Generate a set of relative permittivity bins based on the given range
|
||||
erbins = np.linspace(self.er[0], self.er[1], nbins+1)
|
||||
|
||||
# Generate a range of relative permittivity values the mid-point of
|
||||
# each bin to make materials from
|
||||
#ermaterials = erbins + np.abs((erbins[1] - erbins[0])) / 2
|
||||
ermaterials = 0.5*(erbins[1:nbins+1] + erbins[0:nbins])
|
||||
ermaterials = 0.5 * (erbins[1:nbins+1] + erbins[0:nbins])
|
||||
|
||||
# Generate a set of conductivity bins based on the given range
|
||||
sigmabins = np.linspace(self.sig[0], self.sig[1], nbins+1)
|
||||
sigmabins = np.linspace(self.sig[0], self.sig[1], nbins + 1)
|
||||
|
||||
# Generate a range of conductivity values the mid-point of
|
||||
# each bin to make materials from
|
||||
#sigmamaterials = sigmabins + (sigmabins[1] - sigmabins[0]) / 2
|
||||
sigmamaterials = 0.5*(sigmabins[1:nbins+1] + sigmabins[0:nbins])
|
||||
sigmamaterials = 0.5 * (sigmabins[1:nbins+1] + sigmabins[0:nbins])
|
||||
|
||||
# Generate a set of magnetic permeability bins based on the given range
|
||||
mubins = np.linspace(self.mu[0], self.mu[1], nbins+1)
|
||||
mubins = np.linspace(self.mu[0], self.mu[1], nbins + 1)
|
||||
|
||||
# Generate a range of magnetic permeability values the mid-point of
|
||||
# each bin to make materials from
|
||||
#mumaterials = mubins + np.abs((mubins[1] - mubins[0])) / 2
|
||||
mumaterials = 0.5*(mubins[1:nbins+1] + mubins[0:nbins])
|
||||
mumaterials = 0.5 * (mubins[1:nbins+1] + mubins[0:nbins])
|
||||
|
||||
# Generate a set of magnetic loss bins based on the given range
|
||||
robins = np.linspace(self.ro[0], self.ro[1], nbins+1)
|
||||
# Generate a range of magnetic loss values the mid-point of
|
||||
# each bin to make materials from
|
||||
robins = np.linspace(self.ro[0], self.ro[1], nbins + 1)
|
||||
|
||||
# Generate a range of magnetic loss values the mid-point of each bin to
|
||||
# make materials from
|
||||
#romaterials = robins + np.abs((robins[1] - robins[0])) / 2
|
||||
romaterials = 0.5*(robins[1:nbins+1] + robins[0:nbins])
|
||||
romaterials = 0.5 * (robins[1:nbins+1] + robins[0:nbins])
|
||||
|
||||
|
||||
# Iterate over the bins
|
||||
for iter in np.arange(0,nbins):
|
||||
|
||||
for iter in np.arange(nbins):
|
||||
# Relative permittivity
|
||||
er = ermaterials[iter]
|
||||
|
||||
# Effective conductivity
|
||||
se = sigmamaterials[iter]
|
||||
|
||||
# magnetic permeability
|
||||
# Magnetic permeability
|
||||
mr = mumaterials[iter]
|
||||
|
||||
# magnetic loss
|
||||
# Magnetic loss
|
||||
sm = romaterials[iter]
|
||||
|
||||
# Check to see if the material already exists before creating a new one
|
||||
requiredID = '|{:.4f}+{:.4f}+{:.4f}+{:.4f}|'.format(float(er),float(se),float(mr),float(sm))
|
||||
requiredID = f'|{float(er):.4f}+{float(se):.4f}+{float(mr):.4f}+{float(sm):.4f}|'
|
||||
material = next((x for x in G.materials if x.ID == requiredID), None)
|
||||
if iter == 0:
|
||||
if material:
|
||||
@@ -500,11 +427,10 @@ class RangeMaterial:
|
||||
self.matID.append(m.numID)
|
||||
|
||||
|
||||
|
||||
class ListMaterial:
|
||||
"""A list of predefined materials to be used for
|
||||
factal spatial disttibutions. This command does not create new materials but collects them to be used in a
|
||||
stochastic distribution by a fractal box.
|
||||
"""A list of predefined materials to be used for fractal spatial
|
||||
distributions. This command does not create new materials but collects
|
||||
them to be used in a stochastic distribution by a fractal box.
|
||||
"""
|
||||
|
||||
def __init__(self, ID, listofmaterials):
|
||||
@@ -525,21 +451,18 @@ class ListMaterial:
|
||||
|
||||
|
||||
def calculate_properties(self, nbins, G):
|
||||
"""Calculates the properties of the materials. No Debye is used but name kept the same as used in other
|
||||
class that needs Debye
|
||||
"""Calculates the properties of the materials.
|
||||
|
||||
Args:
|
||||
nbins: int for number of bins to use to create the different materials.
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
|
||||
|
||||
# Iterate over the bins
|
||||
for iter in np.arange(0,nbins):
|
||||
|
||||
# Check to see if the material already exists before creating a new one
|
||||
for iter in np.arange(nbins):
|
||||
#requiredID = '|{:}_in_{:}|'.format((self.mat[iter]),(self.ID))
|
||||
requiredID = self.mat[iter]
|
||||
# Check if the material already exists before creating a new one
|
||||
material = next((x for x in G.materials if x.ID == requiredID), None)
|
||||
self.matID.append(material.numID)
|
||||
|
||||
@@ -556,15 +479,11 @@ class ListMaterial:
|
||||
# m.numID = len(G.materials)
|
||||
# G.materials.append(m)
|
||||
|
||||
|
||||
if not material:
|
||||
logger.exception(self.__str__() + f' material(s) {material} do not exist')
|
||||
raise ValueError
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def create_built_in_materials(G):
|
||||
"""Creates pre-defined (built-in) materials.
|
||||
|
||||
@@ -572,8 +491,6 @@ def create_built_in_materials(G):
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
"""
|
||||
|
||||
G.n_built_in_materials = len(G.materials)
|
||||
|
||||
m = Material(0, 'pec')
|
||||
m.se = float('inf')
|
||||
m.type = 'builtin'
|
||||
@@ -584,8 +501,6 @@ def create_built_in_materials(G):
|
||||
m.type = 'builtin'
|
||||
G.materials.append(m)
|
||||
|
||||
G.n_built_in_materials = len(G.materials)
|
||||
|
||||
|
||||
def calculate_water_properties(T=25, S=0):
|
||||
"""Get extended Debye model properties for water.
|
||||
@@ -628,8 +543,6 @@ def create_water(G, T=25, S=0):
|
||||
|
||||
eri, er, tau, sig = calculate_water_properties(T, S)
|
||||
|
||||
G.n_built_in_materials = len(G.materials)
|
||||
|
||||
m = DispersiveMaterial(len(G.materials), 'water')
|
||||
m.averagable = False
|
||||
m.type = 'builtin, debye'
|
||||
@@ -642,8 +555,6 @@ def create_water(G, T=25, S=0):
|
||||
if config.get_model_config().materials['maxpoles'] == 0:
|
||||
config.get_model_config().materials['maxpoles'] = 1
|
||||
|
||||
G.n_built_in_materials = len(G.materials)
|
||||
|
||||
|
||||
def create_grass(G):
|
||||
"""Creates single-pole Debye model for grass
|
||||
@@ -658,8 +569,6 @@ def create_grass(G):
|
||||
tau = 1.0793e-11
|
||||
sig = 0
|
||||
|
||||
G.n_built_in_materials = len(G.materials)
|
||||
|
||||
m = DispersiveMaterial(len(G.materials), 'grass')
|
||||
m.averagable = False
|
||||
m.type = 'builtin, debye'
|
||||
@@ -672,4 +581,70 @@ def create_grass(G):
|
||||
if config.get_model_config().materials['maxpoles'] == 0:
|
||||
config.get_model_config().materials['maxpoles'] = 1
|
||||
|
||||
G.n_built_in_materials = len(G.materials)
|
||||
|
||||
def process_materials(G):
|
||||
"""Processes complete list of materials - calculates update coefficients,
|
||||
stores in arrays, and builds text list of materials/properties
|
||||
|
||||
Args:
|
||||
G: FDTDGrid class describing a grid in a model.
|
||||
|
||||
Returns:
|
||||
materialsdata: list of material IDs, names, and properties to
|
||||
print a table.
|
||||
"""
|
||||
|
||||
if config.get_model_config().materials['maxpoles'] == 0:
|
||||
materialsdata = [['\nID', '\nName', '\nType', '\neps_r', 'sigma\n[S/m]',
|
||||
'\nmu_r', 'sigma*\n[Ohm/m]', 'Dielectric\nsmoothable']]
|
||||
else:
|
||||
materialsdata = [['\nID', '\nName', '\nType', '\neps_r', 'sigma\n[S/m]',
|
||||
'Delta\neps_r', 'tau\n[s]', 'omega\n[Hz]', 'delta\n[Hz]',
|
||||
'gamma\n[Hz]', '\nmu_r', 'sigma*\n[Ohm/m]', 'Dielectric\nsmoothable']]
|
||||
|
||||
for material in G.materials:
|
||||
# Calculate update coefficients for specific material
|
||||
material.calculate_update_coeffsE(G)
|
||||
material.calculate_update_coeffsH(G)
|
||||
|
||||
# Add update coefficients to overall storage for all materials
|
||||
G.updatecoeffsE[material.numID, :] = material.CA, material.CBx, material.CBy, material.CBz, material.srce
|
||||
G.updatecoeffsH[material.numID, :] = material.DA, material.DBx, material.DBy, material.DBz, material.srcm
|
||||
|
||||
# Add update coefficients to overall storage for dispersive materials
|
||||
if hasattr(material, 'poles'):
|
||||
z = 0
|
||||
for pole in range(config.get_model_config().materials['maxpoles']):
|
||||
G.updatecoeffsdispersive[material.numID, z:z + 3] = (config.sim_config.em_consts['e0'] *
|
||||
material.eqt2[pole], material.eqt[pole], material.zt[pole])
|
||||
z += 3
|
||||
|
||||
# Construct information on material properties for printing table
|
||||
materialtext = [str(material.numID),
|
||||
material.ID[:50] if len(material.ID) > 50 else material.ID,
|
||||
material.type,
|
||||
f'{material.er:g}',
|
||||
f'{material.se:g}']
|
||||
if config.get_model_config().materials['maxpoles'] > 0:
|
||||
if 'debye' in material.type:
|
||||
materialtext.append('\n'.join('{:g}'.format(deltaer) for deltaer in material.deltaer))
|
||||
materialtext.append('\n'.join('{:g}'.format(tau) for tau in material.tau))
|
||||
materialtext.extend(['', '', ''])
|
||||
elif 'lorentz' in material.type:
|
||||
materialtext.append(', '.join('{:g}'.format(deltaer) for deltaer in material.deltaer))
|
||||
materialtext.append('')
|
||||
materialtext.append(', '.join('{:g}'.format(tau) for tau in material.tau))
|
||||
materialtext.append(', '.join('{:g}'.format(alpha) for alpha in material.alpha))
|
||||
materialtext.append('')
|
||||
elif 'drude' in material.type:
|
||||
materialtext.extend(['', ''])
|
||||
materialtext.append(', '.join('{:g}'.format(tau) for tau in material.tau))
|
||||
materialtext.append('')
|
||||
materialtext.append(', '.join('{:g}'.format(alpha) for alpha in material.alpha))
|
||||
else:
|
||||
materialtext.extend(['', '', '', '', ''])
|
||||
|
||||
materialtext.extend((f'{material.mr:g}', f'{material.sm:g}', material.averagable))
|
||||
materialsdata.append(materialtext)
|
||||
|
||||
return materialsdata
|
@@ -123,11 +123,7 @@ class SubGridBase(UserObjectMulti):
|
||||
self.subgrid = sg
|
||||
|
||||
# Copy over built in materials
|
||||
sg.materials = [
|
||||
copy(m)
|
||||
for m in grid.materials
|
||||
if m.numID in range(grid.n_built_in_materials + 1)
|
||||
]
|
||||
sg.materials = [copy(m) for m in grid.materials if m.type == 'builtin']
|
||||
|
||||
# Don't mix and match different subgrid types
|
||||
for sg_made in grid.subgrids:
|
||||
|
@@ -53,19 +53,22 @@ class Waveform:
|
||||
"""Calculates coefficients (used to calculate values) for specific
|
||||
waveforms.
|
||||
"""
|
||||
if self.freq is None:
|
||||
raise ValueError("Frequency is not specified")
|
||||
|
||||
if (self.type == 'gaussian' or self.type == 'gaussiandot' or
|
||||
self.type == 'gaussiandotnorm' or self.type == 'gaussianprime' or
|
||||
self.type == 'gaussiandoubleprime'):
|
||||
if self.type in ['gaussian',
|
||||
'gaussiandot',
|
||||
'gaussiandotnorm',
|
||||
'gaussianprime',
|
||||
'gaussiandoubleprime']:
|
||||
self.chi = 1 / self.freq
|
||||
self.zeta = 2 * np.pi**2 * self.freq**2
|
||||
elif (self.type == 'gaussiandotdot' or
|
||||
self.type == 'gaussiandotdotnorm' or self.type == 'ricker'):
|
||||
elif self.type in ['gaussiandotdot', 'gaussiandotdotnorm', 'ricker']:
|
||||
self.chi = np.sqrt(2) / self.freq
|
||||
self.zeta = np.pi**2 * self.freq**2
|
||||
|
||||
def calculate_value(self, time, dt):
|
||||
"""Calculates value of the waveform at a specific time.
|
||||
"""Calculates the value of the waveform at a specific time.
|
||||
|
||||
Args:
|
||||
time: float for absolute time.
|
||||
@@ -82,7 +85,7 @@ class Waveform:
|
||||
delay = time - self.chi
|
||||
ampvalue = np.exp(-self.zeta * delay**2)
|
||||
|
||||
elif self.type == 'gaussiandot' or self.type == 'gaussianprime':
|
||||
elif self.type in ['gaussiandot', 'gaussianprime']:
|
||||
delay = time - self.chi
|
||||
ampvalue = -2 * self.zeta * delay * np.exp(-self.zeta * delay**2)
|
||||
|
||||
@@ -91,7 +94,7 @@ class Waveform:
|
||||
normalise = np.sqrt(np.exp(1) / (2 * self.zeta))
|
||||
ampvalue = -2 * self.zeta * delay * np.exp(-self.zeta * delay**2) * normalise
|
||||
|
||||
elif self.type == 'gaussiandotdot' or self.type == 'gaussiandoubleprime':
|
||||
elif self.type in ['gaussiandotdot', 'gaussiandoubleprime']:
|
||||
delay = time - self.chi
|
||||
ampvalue = (2 * self.zeta * (2 * self.zeta * delay**2 - 1) *
|
||||
np.exp(-self.zeta * delay**2))
|
||||
@@ -116,8 +119,8 @@ class Waveform:
|
||||
elif self.type == 'contsine':
|
||||
rampamp = 0.25
|
||||
ramp = rampamp * time * self.freq
|
||||
if ramp > 1:
|
||||
ramp = 1
|
||||
ramp = min(ramp, 1)
|
||||
|
||||
ampvalue = ramp * np.sin(2 * np.pi * self.freq * time)
|
||||
|
||||
elif self.type == 'impulse':
|
||||
@@ -126,7 +129,7 @@ class Waveform:
|
||||
ampvalue = 1
|
||||
elif time >= dt:
|
||||
ampvalue = 0
|
||||
|
||||
|
||||
elif self.type == 'user':
|
||||
ampvalue = self.userfunc(time)
|
||||
|
||||
|
38
setup.py
38
setup.py
@@ -37,8 +37,9 @@ if sys.version_info[:2] < MIN_PYTHON_VERSION:
|
||||
|
||||
# Importing gprMax _version__.py before building can cause issues.
|
||||
with open('gprMax/_version.py', 'r') as fd:
|
||||
version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
|
||||
fd.read(), re.MULTILINE).group(1)
|
||||
version = re.search(
|
||||
r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE
|
||||
)[1]
|
||||
|
||||
def build_dispersive_material_templates():
|
||||
"""Function to generate Cython .pyx file for dispersive media update.
|
||||
@@ -46,7 +47,7 @@ def build_dispersive_material_templates():
|
||||
functions.
|
||||
"""
|
||||
|
||||
iswin = True if sys.platform == 'win32' else False
|
||||
iswin = (sys.platform == 'win32')
|
||||
|
||||
env = Environment(loader = FileSystemLoader(os.path.join('gprMax', 'cython')), )
|
||||
|
||||
@@ -151,7 +152,7 @@ if 'cleanall' in sys.argv:
|
||||
print(f'Removed: {os.path.abspath(libfile)}')
|
||||
except OSError:
|
||||
print(f'Could not remove: {os.path.abspath(libfile)}')
|
||||
|
||||
|
||||
# Remove build, dist, egg and __pycache__ directories
|
||||
shutil.rmtree(Path.cwd().joinpath('build'), ignore_errors=True)
|
||||
shutil.rmtree(Path.cwd().joinpath('dist'), ignore_errors=True)
|
||||
@@ -163,7 +164,7 @@ if 'cleanall' in sys.argv:
|
||||
# Remove 'gprMax/cython/fields_updates_dispersive.jinja' if its there
|
||||
if os.path.isfile(cython_disp_file):
|
||||
os.remove(cython_disp_file)
|
||||
|
||||
|
||||
# Now do a normal clean
|
||||
sys.argv[1] = 'clean' # this is what distutils understands
|
||||
|
||||
@@ -176,10 +177,6 @@ else:
|
||||
linker_args = []
|
||||
libraries = []
|
||||
|
||||
# Compiler options - macOS - needs gcc (usually via HomeBrew) because the
|
||||
# default compiler LLVM (clang) does not
|
||||
# support OpenMP. With gcc -fopenmp option
|
||||
# implies -pthread
|
||||
elif sys.platform == 'darwin':
|
||||
# Check for Intel or Apple M series CPU
|
||||
cpuID = subprocess.check_output("sysctl -n machdep.cpu.brand_string",
|
||||
@@ -209,7 +206,7 @@ else:
|
||||
'to be installed - easily done through the Homebrew package ' +
|
||||
'manager (http://brew.sh). Note: gcc with OpenMP support ' +
|
||||
'is required.')
|
||||
|
||||
|
||||
# Minimum supported macOS deployment target
|
||||
MIN_MACOS_VERSION = '10.13'
|
||||
try:
|
||||
@@ -219,12 +216,14 @@ else:
|
||||
pass
|
||||
os.environ['MIN_SUPPORTED_MACOSX_DEPLOYMENT_TARGET'] = MIN_MACOS_VERSION
|
||||
# Sometimes worth testing with '-fstrict-aliasing', '-fno-common'
|
||||
compile_args = ['-O3', '-w', '-fopenmp', '-march=native',
|
||||
'-mmacosx-version-min=' + MIN_MACOS_VERSION]
|
||||
linker_args = ['-fopenmp', '-mmacosx-version-min=' + MIN_MACOS_VERSION]
|
||||
compile_args = ['-O3',
|
||||
'-w',
|
||||
'-fopenmp',
|
||||
'-march=native',
|
||||
f'-mmacosx-version-min={MIN_MACOS_VERSION}']
|
||||
linker_args = ['-fopenmp', f'-mmacosx-version-min={MIN_MACOS_VERSION}']
|
||||
libraries=['gomp']
|
||||
|
||||
# Compiler options - Linux
|
||||
elif sys.platform == 'linux':
|
||||
compile_args = ['-O3', '-w', '-fopenmp', '-march=native']
|
||||
linker_args = ['-fopenmp']
|
||||
@@ -256,17 +255,17 @@ else:
|
||||
# Parse long_description from README.rst file.
|
||||
with open('README.rst','r') as fd:
|
||||
long_description = fd.read()
|
||||
|
||||
|
||||
setup(name='gprMax',
|
||||
version=version,
|
||||
author='Craig Warren, Antonis Giannopoulos, and John Hartley',
|
||||
url='http://www.gprmax.com',
|
||||
description='Electromagnetic Modelling Software based on the ' +
|
||||
'Finite-Difference Time-Domain (FDTD) method',
|
||||
description='Electromagnetic Modelling Software based on the '
|
||||
+ 'Finite-Difference Time-Domain (FDTD) method',
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/x-rst",
|
||||
license='GPLv3+',
|
||||
python_requires='>' + str(MIN_PYTHON_VERSION[0]) + '.' + str(MIN_PYTHON_VERSION[1]),
|
||||
python_requires=f'>{str(MIN_PYTHON_VERSION[0])}.{str(MIN_PYTHON_VERSION[1])}',
|
||||
install_requires=['colorama',
|
||||
'cython',
|
||||
'h5py',
|
||||
@@ -289,4 +288,5 @@ else:
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Cython',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Scientific/Engineering'])
|
||||
'Topic :: Scientific/Engineering'],
|
||||
)
|
||||
|
在新工单中引用
屏蔽一个用户