Skip to content

Commit 9dd2de4

Browse files
authored
Merge pull request #6 from ubcgif/DCResistivity
Dc resistivity implementation
2 parents 6cefdc9 + 36693ea commit 9dd2de4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+12360
-111
lines changed

.codecov.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
coverage:
2+
ignore:
3+
- "simpegtorch/discretize/mixins/mpl_mod.py"

.github/environment_ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ dependencies:
1010
- matplotlib-base
1111
- libdlf
1212
- typing_extensions
13+
- simpeg
14+
1315

1416
# solver
1517
# uncomment the next line if you are on an intel platform

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dependencies:
77
- numpy>=1.22
88
- scipy>=1.8
99
- pymatsolver>=0.3
10+
- simpeg
1011
- pytorch
1112
- matplotlib-base
1213
- libdlf
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"""
2+
Simple DC resistivity example with pseudosection plotting.
3+
4+
Creates a realistic dipole-dipole survey and plots the apparent resistivity pseudosection.
5+
"""
6+
7+
import torch
8+
import matplotlib.pyplot as plt
9+
from matplotlib.colors import LogNorm
10+
11+
# simpeg-torch imports
12+
from simpegtorch.discretize import TensorMesh
13+
from simpegtorch.electromagnetics.resistivity import (
14+
Simulation3DNodal,
15+
SrcDipole,
16+
RxDipole,
17+
Survey,
18+
)
19+
from simpegtorch.electromagnetics.utils import (
20+
apparent_resistivity_from_voltage,
21+
pseudo_locations,
22+
plot_pseudosection,
23+
)
24+
from simpegtorch.utils import (
25+
get_indices_sphere,
26+
active_from_xyz,
27+
create_flat_topography,
28+
InjectActiveCells,
29+
)
30+
31+
# Set PyTorch settings
32+
torch.set_default_dtype(torch.float64)
33+
torch.set_default_device("cuda" if torch.cuda.is_available() else "cpu")
34+
print("Creating DC pseudosection example...")
35+
36+
# =============================================================================
37+
# Create Mesh
38+
# =============================================================================
39+
40+
# Define mesh parameters - smaller for faster computation
41+
dx = dy = dz = 25.0 # 25m cells
42+
nx, ny, nz = 40, 20, 20 # 40x20x20 = 16,000 cells
43+
44+
# Create cell sizes
45+
hx = torch.full((nx,), dx)
46+
hy = torch.full((ny,), dy)
47+
hz = torch.full((nz,), dz)
48+
49+
# Create mesh origin
50+
origin = torch.tensor([-nx * dx / 2, -ny * dy / 2, -500.0]) # 500m below surface
51+
52+
# Create the tensor mesh
53+
mesh = TensorMesh(
54+
[hx, hy, hz],
55+
origin=origin,
56+
device=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
57+
)
58+
print(f"Mesh: {mesh.nC} cells ({nx} x {ny} x {nz})")
59+
60+
# =============================================================================
61+
# Define Model
62+
# =============================================================================
63+
64+
# Create flat topography
65+
topo_xyz = create_flat_topography(
66+
x_extent=(-1000, 1000),
67+
y_extent=(-250, 250),
68+
elevation=0.0,
69+
n_points_x=21,
70+
n_points_y=11,
71+
)
72+
73+
# Get active cells
74+
active_cells = active_from_xyz(mesh, topo_xyz)
75+
n_active = torch.sum(active_cells).item()
76+
77+
# Create mapping
78+
air_resistivity = 1e8
79+
active_mapping = InjectActiveCells(mesh, active_cells, valInactive=air_resistivity)
80+
81+
# Define 3-layer model
82+
background_resistivity = 100.0 # 100 Ohm-m
83+
conductor_resistivity = 10.0 # 10 Ohm-m conductive body
84+
resistor_resistivity = 1000.0 # 1000 Ohm-m resistive body
85+
86+
# Create background model
87+
active_model = torch.full((n_active,), background_resistivity, dtype=torch.float64)
88+
89+
# Add conductive body at (-150, 0, -100) with radius 75m
90+
conductor_center = [-150.0, 0.0, -100.0]
91+
conductor_radius = 75.0
92+
active_centers = mesh.cell_centers[active_cells]
93+
conductor_mask = get_indices_sphere(conductor_center, conductor_radius, active_centers)
94+
active_model[conductor_mask] = conductor_resistivity
95+
96+
# Add resistive body at (150, 0, -100) with radius 75m
97+
resistor_center = [150.0, 0.0, -100.0]
98+
resistor_radius = 75.0
99+
resistor_mask = get_indices_sphere(resistor_center, resistor_radius, active_centers)
100+
active_model[resistor_mask] = resistor_resistivity
101+
102+
print(f"Active model range: {active_model.min():.1f} to {active_model.max():.1f} Ohm-m")
103+
104+
# Convert to conductivity for simulation
105+
active_conductivity = 1.0 / active_model
106+
full_conductivity = active_mapping * active_conductivity
107+
108+
# =============================================================================
109+
# Create Manual Dipole-Dipole Survey
110+
# =============================================================================
111+
112+
# Create a dipole-dipole survey manually for better control
113+
electrode_spacing = 50.0 # 25m spacing
114+
n_electrodes = 17 # 17 electrodes along line
115+
electrodes_x = (
116+
torch.arange(n_electrodes) * electrode_spacing
117+
- (n_electrodes - 1) * electrode_spacing / 2
118+
)
119+
electrodes_y = torch.zeros(n_electrodes)
120+
electrodes_z = torch.zeros(n_electrodes)
121+
122+
electrode_locations = torch.stack([electrodes_x, electrodes_y, electrodes_z], dim=1)
123+
print(
124+
f"Survey line: {n_electrodes} electrodes from {electrodes_x[0]:.0f}m to {electrodes_x[-1]:.0f}m"
125+
)
126+
127+
# Create dipole-dipole survey
128+
sources = []
129+
n_spacings = 6 # Number of receiver spacings per source
130+
131+
for i in range(n_electrodes - 3): # Need at least 4 electrodes for dipole-dipole
132+
# Source dipole: electrodes i and i+1
133+
src_a = electrode_locations[i]
134+
src_b = electrode_locations[i + 1]
135+
136+
receivers = []
137+
for n in range(1, n_spacings + 1): # n = 1, 2, 3, ...
138+
# Receiver dipole: electrodes at spacing n from source
139+
rx_idx_m = i + 1 + n
140+
rx_idx_n = i + 2 + n
141+
142+
if rx_idx_n < n_electrodes: # Check bounds
143+
rx_m = electrode_locations[rx_idx_m].unsqueeze(0)
144+
rx_n = electrode_locations[rx_idx_n].unsqueeze(0)
145+
146+
# Create receiver
147+
rx = RxDipole(locations_m=rx_m, locations_n=rx_n, data_type="volt")
148+
receivers.append(rx)
149+
150+
if receivers: # Only create source if there are receivers
151+
# Create source
152+
src = SrcDipole(
153+
receiver_list=receivers,
154+
location_a=src_a,
155+
location_b=src_b,
156+
data_type="volt",
157+
)
158+
sources.append(src)
159+
160+
# Create survey
161+
survey = Survey(sources)
162+
print(f"Survey: {survey.nSrc} sources, {survey.nD} data points")
163+
164+
# =============================================================================
165+
# Run Forward Simulation
166+
# =============================================================================
167+
168+
try:
169+
print("\n=== RUNNING FORWARD SIMULATION ===")
170+
simulation = Simulation3DNodal(mesh, survey=survey)
171+
172+
# Run forward simulation
173+
print("Computing forward data...")
174+
dpred = simulation.dpred(active_mapping * active_model)
175+
print(f"Predicted data shape: {dpred.shape}")
176+
print(f"Data range: {dpred.min():.2e} to {dpred.max():.2e} V/A")
177+
178+
# Calculate apparent resistivities
179+
from simpegtorch.electromagnetics.utils.static_utils import geometric_factor
180+
181+
G = geometric_factor(survey, space_type="half space")
182+
print(f"Geometric factor range: {G.min():.2e} to {G.max():.2e}")
183+
184+
# Check for issues
185+
if torch.any(torch.isinf(G)) or torch.any(torch.isnan(G)):
186+
print("Warning: Geometric factor has inf/nan values")
187+
print(f"Inf count: {torch.sum(torch.isinf(G))}")
188+
print(f"NaN count: {torch.sum(torch.isnan(G))}")
189+
print(f"Zero count: {torch.sum(G == 0)}")
190+
191+
apparent_resistivity = apparent_resistivity_from_voltage(
192+
survey, dpred, space_type="half space"
193+
)
194+
print(
195+
f"Apparent resistivity range: {apparent_resistivity.min():.6f} to {apparent_resistivity.max():.6f} Ohm-m"
196+
)
197+
198+
# =============================================================================
199+
# Create Plots
200+
# =============================================================================
201+
202+
print("\nCreating plots...")
203+
204+
# Create figure
205+
fig = plt.figure(figsize=(15, 8))
206+
207+
# Plot 1: Model cross-section
208+
ax1 = plt.subplot(2, 2, 1)
209+
full_resistivity = active_mapping * active_model
210+
mesh.plot_slice(
211+
full_resistivity,
212+
normal="Y",
213+
slice_loc=0.0,
214+
ax=ax1,
215+
pcolor_opts={"cmap": "RdYlBu_r", "norm": LogNorm(vmin=10, vmax=1000)},
216+
)
217+
ax1.set_title("True Resistivity Model")
218+
ax1.set_xlabel("X (m)")
219+
ax1.set_ylabel("Z (m)")
220+
221+
# Plot 2: Survey layout
222+
ax2 = plt.subplot(2, 2, 2)
223+
ax2.scatter(
224+
electrode_locations[:, 0].cpu().detach(),
225+
electrode_locations[:, 1].cpu().detach(),
226+
c="red",
227+
s=30,
228+
marker="^",
229+
)
230+
ax2.set_title("Survey Layout")
231+
ax2.set_xlabel("X (m)")
232+
ax2.set_ylabel("Y (m)")
233+
ax2.grid(True)
234+
ax2.axis("equal")
235+
236+
# Plot 3: Apparent resistivity pseudosection
237+
ax3 = plt.subplot(2, 1, 2)
238+
239+
# Get pseudo-locations for plotting
240+
pseudo_locs = pseudo_locations(survey)
241+
242+
# Plot pseudosection using the plot_pseudosection utility
243+
plot_pseudosection(
244+
survey,
245+
dobs=apparent_resistivity,
246+
plot_type="contourf",
247+
ax=ax3,
248+
scale="log",
249+
cbar_label="Apparent Resistivity (Ohm-m)",
250+
contourf_opts={
251+
"cmap": "RdYlBu_r",
252+
"levels": 20,
253+
},
254+
data_locations=True, # Show data points
255+
)
256+
257+
ax3.set_title("Apparent Resistivity Pseudosection")
258+
ax3.set_xlabel("X (m)")
259+
ax3.set_ylabel("Elevation (m)")
260+
ax3.grid(True, alpha=0.3)
261+
262+
plt.tight_layout()
263+
plt.savefig(
264+
"../dc_pseudosection_simple.png",
265+
dpi=150,
266+
bbox_inches="tight",
267+
)
268+
plt.show()
269+
270+
# =============================================================================
271+
# Summary
272+
# =============================================================================
273+
274+
print("\n" + "=" * 60)
275+
print("PSEUDOSECTION EXAMPLE SUMMARY")
276+
print("=" * 60)
277+
print("Survey Configuration:")
278+
print(f" - {n_electrodes} electrodes with {electrode_spacing}m spacing")
279+
print(f" - Dipole-dipole array with {n_spacings} spacings")
280+
print(f" - {survey.nSrc} sources, {survey.nD} data points")
281+
282+
print("\nModel:")
283+
print(f" - Background: {background_resistivity} Ohm-m")
284+
print(f" - Conductor: {conductor_resistivity} Ohm-m at x={conductor_center[0]}m")
285+
print(f" - Resistor: {resistor_resistivity} Ohm-m at x={resistor_center[0]}m")
286+
287+
print("\nResults:")
288+
print(f" - Voltage range: {dpred.min():.2e} to {dpred.max():.2e} V/A")
289+
print(
290+
f" - Apparent resistivity: {apparent_resistivity.min():.6f} to {apparent_resistivity.max():.6f} Ohm-m"
291+
)
292+
293+
print("\n✅ SUCCESS: DC pseudosection example completed!")
294+
295+
except Exception as e:
296+
print(f"\nError: {e}")
297+
import traceback
298+
299+
traceback.print_exc()

0 commit comments

Comments
 (0)