Skip to content

Commit efcfc23

Browse files
committed
Fixed basic implementaiton of FDEM simulation
1 parent f762d11 commit efcfc23

File tree

3 files changed

+72
-53
lines changed

3 files changed

+72
-53
lines changed

examples/test_fdem_functionality.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ def create_test_mesh():
3838
"""Create a simple 3D test mesh"""
3939
print("Creating test mesh...")
4040

41-
# Create a 20x20x10 mesh with 5m cells
42-
nx, ny, nz = 20, 20, 10
43-
hx = torch.ones(nx) * 5.0 # 5m cell size
44-
hy = torch.ones(ny) * 5.0
45-
hz = torch.ones(nz) * 5.0
41+
# Create a small 6x6x4 mesh with 10m cells for faster testing
42+
nx, ny, nz = 6, 6, 4
43+
hx = torch.ones(nx) * 10.0 # 10m cell size
44+
hy = torch.ones(ny) * 10.0
45+
hz = torch.ones(nz) * 10.0
4646

4747
mesh = TensorMesh([hx, hy, hz])
4848
print(f" Mesh shape: {mesh.shape_cells}")
@@ -64,9 +64,9 @@ def test_basic_magnetic_dipole():
6464
mesh = create_test_mesh()
6565

6666
# Create receiver locations (line profile)
67-
rx_x = torch.linspace(10, 90, 9) # 9 receivers from 10m to 90m
68-
rx_y = torch.ones_like(rx_x) * 50 # At y=50m
69-
rx_z = torch.ones_like(rx_x) * 2.5 # At z=2.5m (just above surface)
67+
rx_x = torch.linspace(15, 45, 4) # 4 receivers from 15m to 45m
68+
rx_y = torch.ones_like(rx_x) * 30 # At y=30m (center)
69+
rx_z = torch.ones_like(rx_x) * 5.0 # At z=5m (just above surface)
7070
rx_locs = torch.stack([rx_x, rx_y, rx_z], dim=1)
7171

7272
print(f"Receiver locations: {rx_locs.shape}")
@@ -81,7 +81,7 @@ def test_basic_magnetic_dipole():
8181
source = MagneticDipole(
8282
receiver_list=receivers,
8383
frequency=1000.0, # 1 kHz
84-
location=torch.tensor([50.0, 50.0, 0.0]), # Center of domain at surface
84+
location=torch.tensor([30.0, 30.0, 0.0]), # Center of domain at surface
8585
moment=1.0,
8686
orientation="z",
8787
)
@@ -98,10 +98,10 @@ def test_basic_magnetic_dipole():
9898
# Add a conductive block
9999
cell_centers = mesh.cell_centers
100100
block_mask = (
101-
(cell_centers[:, 0] > 40)
102-
& (cell_centers[:, 0] < 60)
103-
& (cell_centers[:, 1] > 40)
104-
& (cell_centers[:, 1] < 60)
101+
(cell_centers[:, 0] > 20)
102+
& (cell_centers[:, 0] < 40)
103+
& (cell_centers[:, 1] > 20)
104+
& (cell_centers[:, 1] < 40)
105105
& (cell_centers[:, 2] > 10)
106106
& (cell_centers[:, 2] < 30)
107107
)
@@ -147,10 +147,10 @@ def test_multiple_frequencies():
147147
mesh = create_test_mesh()
148148

149149
# Single receiver location
150-
rx_loc = torch.tensor([[50.0, 50.0, 2.5]])
150+
rx_loc = torch.tensor([[30.0, 30.0, 5.0]])
151151

152152
sources = []
153-
frequencies = [100.0, 1000.0, 10000.0] # 100 Hz, 1 kHz, 10 kHz
153+
frequencies = [1000.0, 10000.0] # 1 kHz, 10 kHz (reduced to 2 frequencies)
154154

155155
for freq in frequencies:
156156
receivers = [
@@ -161,7 +161,7 @@ def test_multiple_frequencies():
161161
source = MagneticDipole(
162162
receiver_list=receivers,
163163
frequency=freq,
164-
location=torch.tensor([50.0, 50.0, 0.0]),
164+
location=torch.tensor([30.0, 30.0, 0.0]),
165165
moment=1.0,
166166
orientation="z",
167167
)
@@ -202,7 +202,7 @@ def test_different_sources():
202202
print("=" * 60)
203203

204204
mesh = create_test_mesh()
205-
rx_loc = torch.tensor([[60.0, 50.0, 2.5]])
205+
rx_loc = torch.tensor([[40.0, 30.0, 5.0]])
206206

207207
# Test each source type
208208
source_tests = []
@@ -213,7 +213,7 @@ def test_different_sources():
213213
mag_dipole = MagneticDipole(
214214
receiver_list=receivers,
215215
frequency=1000.0,
216-
location=torch.tensor([50.0, 50.0, 0.0]),
216+
location=torch.tensor([30.0, 30.0, 0.0]),
217217
moment=1.0,
218218
orientation="z",
219219
)
@@ -234,7 +234,7 @@ def test_different_sources():
234234
elec_dipole = ElectricDipole(
235235
receiver_list=receivers,
236236
frequency=1000.0,
237-
location=torch.tensor([50.0, 50.0, 0.0]),
237+
location=torch.tensor([30.0, 30.0, 0.0]),
238238
current=1.0,
239239
length=1.0,
240240
orientation="z",
@@ -256,7 +256,7 @@ def test_different_sources():
256256
loop_source = LoopSource(
257257
receiver_list=receivers,
258258
frequency=1000.0,
259-
location=torch.tensor([50.0, 50.0, 0.0]),
259+
location=torch.tensor([30.0, 30.0, 0.0]),
260260
radius=10.0,
261261
current=1.0,
262262
orientation="z",
@@ -283,7 +283,7 @@ def test_receivers():
283283
print("=" * 60)
284284

285285
mesh = create_test_mesh()
286-
rx_loc = torch.tensor([[60.0, 50.0, 2.5]])
286+
rx_loc = torch.tensor([[40.0, 30.0, 5.0]])
287287

288288
receiver_tests = []
289289

@@ -317,7 +317,7 @@ def test_receivers():
317317
source = MagneticDipole(
318318
receiver_list=[receiver],
319319
frequency=1000.0,
320-
location=torch.tensor([50.0, 50.0, 0.0]),
320+
location=torch.tensor([30.0, 30.0, 0.0]),
321321
moment=1.0,
322322
orientation="z",
323323
)

fdem_test_results.png

75.5 KB
Loading

simpegtorch/electromagnetics/FDEM/receivers.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ def __init__(self, locations, orientation="z", component="real"):
1010
----------
1111
locations : array_like
1212
Receiver locations (n_receivers, 3)
13-
orientation : str
14-
Field component orientation ('x', 'y', 'z')
13+
orientation : str or array_like
14+
Field component orientation. Can be 'x', 'y', 'z' or a 3-vector like [0, 0, 1]
1515
component : str
1616
Complex component ('real', 'imag')
1717
"""
@@ -21,7 +21,16 @@ def __init__(self, locations, orientation="z", component="real"):
2121
self.locations = torch.tensor(locations, dtype=torch.float64)
2222
if self.locations.ndim == 1:
2323
self.locations = self.locations.reshape(1, -1)
24-
self.orientation = orientation
24+
25+
# Convert orientation to vector format
26+
if isinstance(orientation, str):
27+
orient_map = {"x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1]}
28+
self.orientation = torch.tensor(
29+
orient_map[orientation], dtype=torch.float64
30+
)
31+
else:
32+
self.orientation = torch.tensor(orientation, dtype=torch.float64)
33+
2534
self.component = component
2635

2736
@property
@@ -33,6 +42,36 @@ def evaluate(self, fields, simulation):
3342
"""Extract data from fields at receiver locations"""
3443
raise NotImplementedError("evaluate must be implemented in derived classes")
3544

45+
def _get_projection_matrix(self, mesh, projected_grid):
46+
"""
47+
Get projection matrix from mesh to receivers (following SimPEG pattern).
48+
49+
Parameters
50+
----------
51+
mesh : TensorMesh
52+
The mesh
53+
projected_grid : str
54+
55+
Returns
56+
-------
57+
torch.Tensor
58+
Projection matrix
59+
"""
60+
# Initialize zero matrix
61+
n_grid = getattr(mesh, f"n_{projected_grid[:-1]}")
62+
P = torch.zeros(self.locations.shape[0], n_grid, dtype=torch.complex128)
63+
64+
# Component-wise interpolation like SimPEG
65+
for strength, comp in zip(self.orientation, ["x", "y", "z"]):
66+
if strength != 0.0:
67+
location_type = projected_grid + comp # e.g., "faces_x"
68+
P_comp = mesh.get_interpolation_matrix(
69+
self.locations, location_type=location_type
70+
)
71+
P = P + strength * P_comp.to(dtype=torch.complex128)
72+
73+
return P
74+
3675

3776
class PointMagneticFluxDensity(BaseFDEMReceiver):
3877
"""Point receiver for magnetic flux density measurements."""
@@ -53,29 +92,17 @@ def evaluate(self, fields, simulation):
5392
torch.Tensor
5493
Data at receiver locations
5594
"""
56-
# Get interpolation matrix from faces to receiver locations
57-
P = simulation.mesh.get_interpolation_matrix(self.locations, location_type="F")
95+
# Get projection matrix using component-wise interpolation (like SimPEG)
96+
P = self._get_projection_matrix(simulation.mesh, "faces_")
5897

5998
# Interpolate fields to receiver locations
6099
b_interp = P @ fields
61100

62-
# For 3D, need to extract the correct component
63-
if simulation.mesh.dim == 3:
64-
# Reshape to (n_receivers, 3) for vector field
65-
b_vector = b_interp.reshape(-1, 3)
66-
67-
# Extract component
68-
comp_idx = {"x": 0, "y": 1, "z": 2}[self.orientation]
69-
b_comp = b_vector[:, comp_idx]
70-
else:
71-
# For 2D, assume z-component
72-
b_comp = b_interp
73-
74101
# Return real or imaginary part
75102
if self.component == "real":
76-
return b_comp.real
103+
return b_interp.real
77104
else:
78-
return b_comp.imag
105+
return b_interp.imag
79106

80107

81108
class PointMagneticFluxDensitySecondary(PointMagneticFluxDensity):
@@ -105,7 +132,7 @@ def evaluate(self, fields, simulation):
105132
E = -iωμ⁻¹∇×B
106133
"""
107134
# Get curl matrix
108-
C = simulation.mesh.edge_curl
135+
C = simulation.mesh.edge_curl.to(dtype=torch.complex128)
109136

110137
# Compute electric field from magnetic flux density
111138
# E = -iω * μ⁻¹ * C.T * B
@@ -118,20 +145,12 @@ def evaluate(self, fields, simulation):
118145
e_field = -1j * omega / mu0 * (C.T @ fields)
119146

120147
# Interpolate to receiver locations
121-
P = simulation.mesh.get_interpolation_matrix(self.locations, location_type="E")
148+
P = self._get_projection_matrix(simulation.mesh, "edges_")
122149

123150
e_interp = P @ e_field
124151

125-
# Extract component
126-
if simulation.mesh.dim == 3:
127-
e_vector = e_interp.reshape(-1, 3)
128-
comp_idx = {"x": 0, "y": 1, "z": 2}[self.orientation]
129-
e_comp = e_vector[:, comp_idx]
130-
else:
131-
e_comp = e_interp
132-
133152
# Return real or imaginary part
134153
if self.component == "real":
135-
return e_comp.real
154+
return e_interp.real
136155
else:
137-
return e_comp.imag
156+
return e_interp.imag

0 commit comments

Comments
 (0)