|
| 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