Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/geophysics/gravity_anomaly.tex
51 views
unlisted
1
\documentclass[a4paper, 11pt]{article}
2
\usepackage[utf8]{inputenc}
3
\usepackage[T1]{fontenc}
4
\usepackage{amsmath, amssymb, amsthm}
5
\usepackage{graphicx}
6
\usepackage{booktabs}
7
\usepackage{siunitx}
8
\usepackage{subcaption}
9
\usepackage[makestderr]{pythontex}
10
11
\newtheorem{definition}{Definition}
12
\newtheorem{theorem}{Theorem}
13
\newtheorem{example}{Example}
14
\newtheorem{remark}{Remark}
15
16
\title{Gravity Anomaly Analysis: Forward Modeling and Inversion of Subsurface Density Structures}
17
\author{Computational Geophysics Laboratory}
18
\date{\today}
19
20
\begin{document}
21
\maketitle
22
23
\begin{abstract}
24
This technical report presents comprehensive computational analysis of gravity anomalies arising from subsurface density variations. We implement forward modeling for multiple geometric bodies including spheres, cylinders, and rectangular prisms, along with Bouguer and terrain corrections. The analysis demonstrates depth estimation using the half-width rule, Euler deconvolution for source parameter determination, and power spectrum analysis for estimating source depths. Applications include mineral exploration, basin analysis, and detection of near-surface voids.
25
\end{abstract}
26
27
\section{Theoretical Framework}
28
29
\begin{definition}[Gravitational Potential]
30
The gravitational potential $U$ at point $\mathbf{r}$ due to a mass distribution $\rho(\mathbf{r'})$ is:
31
\begin{equation}
32
U(\mathbf{r}) = G \int_V \frac{\rho(\mathbf{r'})}{|\mathbf{r} - \mathbf{r'}|} dV'
33
\end{equation}
34
where $G = 6.674 \times 10^{-11}$ m$^3$kg$^{-1}$s$^{-2}$ is the gravitational constant.
35
\end{definition}
36
37
\begin{theorem}[Poisson's Equation]
38
In regions containing mass, the gravitational potential satisfies:
39
\begin{equation}
40
\nabla^2 U = -4\pi G \rho
41
\end{equation}
42
while in empty space, $\nabla^2 U = 0$ (Laplace's equation).
43
\end{theorem}
44
45
The vertical component of gravity acceleration is:
46
\begin{equation}
47
g_z = -\frac{\partial U}{\partial z}
48
\end{equation}
49
50
\subsection{Gravity Anomaly Corrections}
51
52
The complete Bouguer anomaly requires several corrections:
53
\begin{equation}
54
\Delta g_B = g_{obs} - g_\phi + \delta g_{FA} - \delta g_B + \delta g_T
55
\end{equation}
56
57
\begin{itemize}
58
\item \textbf{Free-air correction}: $\delta g_{FA} = 0.3086h$ mGal (h in meters)
59
\item \textbf{Bouguer plate correction}: $\delta g_B = 0.04193\rho h$ mGal
60
\item \textbf{Terrain correction}: $\delta g_T$ accounts for deviations from infinite slab
61
\end{itemize}
62
63
\begin{example}[Sphere Anomaly]
64
The gravity anomaly of a buried sphere with radius $R$, density contrast $\Delta\rho$, and center at depth $z_0$:
65
\begin{equation}
66
\Delta g_z(x) = \frac{4\pi G \Delta\rho R^3}{3} \frac{z_0}{(x^2 + z_0^2)^{3/2}}
67
\end{equation}
68
The half-width $x_{1/2}$ where anomaly falls to half its maximum: $z_0 = 1.305 x_{1/2}$
69
\end{example}
70
71
\section{Computational Analysis}
72
73
\begin{pycode}
74
import numpy as np
75
import matplotlib.pyplot as plt
76
from scipy.optimize import curve_fit
77
from scipy.signal import find_peaks
78
plt.rc('text', usetex=True)
79
plt.rc('font', family='serif', size=10)
80
81
# Physical constants
82
G = 6.674e-11 # m^3/kg/s^2
83
mGal = 1e-5 # m/s^2
84
85
# Forward modeling functions
86
def sphere_anomaly(x, z0, R, delta_rho):
87
"""Gravity anomaly of buried sphere (mGal)."""
88
return 4/3 * np.pi * G * delta_rho * R**3 * z0 / (x**2 + z0**2)**1.5 / mGal
89
90
def cylinder_anomaly(x, z0, R, delta_rho):
91
"""Gravity anomaly of horizontal infinite cylinder (mGal)."""
92
return 2 * np.pi * G * delta_rho * R**2 * z0 / (x**2 + z0**2) / mGal
93
94
def prism_anomaly(x, z1, z2, x1, x2, delta_rho):
95
"""2D gravity anomaly of rectangular prism (mGal)."""
96
def term(xi, zi):
97
r = np.sqrt((x - xi)**2 + zi**2)
98
theta = np.arctan2(zi, x - xi)
99
return zi * np.log(r) - (x - xi) * theta
100
101
result = term(x2, z2) - term(x1, z2) - term(x2, z1) + term(x1, z1)
102
return 2 * G * delta_rho * result / mGal
103
104
# Profile parameters
105
x = np.linspace(-3000, 3000, 301) # meters
106
107
# Multiple anomaly sources
108
# Source 1: Deep ore body (sphere)
109
z1, R1, drho1 = 800, 200, 2500 # depth, radius, density contrast
110
g_sphere = sphere_anomaly(x, z1, R1, drho1)
111
112
# Source 2: Shallow cavity (negative anomaly)
113
z2, R2, drho2 = 50, 15, -1800 # air-filled cavity
114
g_cavity = sphere_anomaly(x + 500, z2, R2, drho2)
115
116
# Source 3: Buried channel (cylinder)
117
z3, R3, drho3 = 150, 80, -500 # sediment-filled channel
118
g_channel = cylinder_anomaly(x - 800, z3, R3, drho3)
119
120
# Combined anomaly
121
g_total = g_sphere + g_cavity + g_channel
122
123
# Add realistic noise
124
np.random.seed(42)
125
noise = np.random.normal(0, 0.02, len(x))
126
g_observed = g_total + noise
127
128
# Bouguer correction example
129
stations = np.arange(-2000, 2001, 500)
130
elevation = 50 + 200 * np.exp(-(stations/800)**2) # Topographic high
131
rho_bouguer = 2670 # kg/m^3
132
133
free_air = 0.3086 * elevation
134
bouguer_plate = 0.04193 * rho_bouguer * elevation / 1000
135
bouguer_anomaly = free_air - bouguer_plate
136
137
# Terrain correction (simplified - cylindrical segments)
138
terrain_corr = 0.02 * elevation**0.5 # Approximate
139
140
# Half-width depth estimation
141
max_idx = np.argmax(g_sphere)
142
half_max = g_sphere[max_idx] / 2
143
# Find half-width
144
above_half = g_sphere > half_max
145
transitions = np.diff(above_half.astype(int))
146
left_idx = np.where(transitions == 1)[0][0] if np.any(transitions == 1) else 0
147
right_idx = np.where(transitions == -1)[0][0] if np.any(transitions == -1) else len(x)-1
148
x_half_width = (x[right_idx] - x[left_idx]) / 2
149
z_estimated = 1.305 * x_half_width
150
151
# Second derivative for edge detection
152
dx = x[1] - x[0]
153
g_second_deriv = np.gradient(np.gradient(g_total, dx), dx)
154
155
# Euler deconvolution (simplified structural index = 3 for sphere)
156
def euler_deconv(x_data, g_data, SI=3, window=50):
157
"""Simple Euler deconvolution for depth estimation."""
158
dx = x_data[1] - x_data[0]
159
dg_dx = np.gradient(g_data, dx)
160
161
depths = []
162
x_positions = []
163
164
for i in range(window, len(x_data) - window):
165
# Solve: x*dg/dx + z*dg/dz = -SI*g
166
# Approximate dg/dz from horizontal derivative (Hilbert transform)
167
A = np.column_stack([dg_dx[i-window:i+window],
168
np.ones(2*window)])
169
b = -SI * g_data[i-window:i+window]
170
171
try:
172
solution = np.linalg.lstsq(A, b, rcond=None)[0]
173
z_euler = solution[0]
174
if 10 < z_euler < 2000:
175
depths.append(z_euler)
176
x_positions.append(x_data[i])
177
except:
178
pass
179
180
return np.array(x_positions), np.array(depths)
181
182
# Power spectrum for regional/residual separation
183
g_fft = np.fft.fft(g_total)
184
freqs = np.fft.fftfreq(len(x), dx)
185
power_spectrum = np.abs(g_fft)**2
186
187
# Depth estimation from power spectrum slope
188
pos_freqs = freqs[1:len(freqs)//2]
189
pos_power = power_spectrum[1:len(freqs)//2]
190
191
# Regional field (low-pass)
192
cutoff = 0.0005 # 1/m
193
g_regional = np.real(np.fft.ifft(g_fft * (np.abs(freqs) < cutoff)))
194
g_residual = g_total - g_regional
195
196
# Create visualization
197
fig = plt.figure(figsize=(12, 10))
198
gs = fig.add_gridspec(3, 3, hspace=0.35, wspace=0.35)
199
200
# Plot 1: Individual source anomalies
201
ax1 = fig.add_subplot(gs[0, 0])
202
ax1.plot(x, g_sphere, 'b-', lw=1.5, label='Ore body')
203
ax1.plot(x, g_cavity, 'r-', lw=1.5, label='Cavity')
204
ax1.plot(x, g_channel, 'g-', lw=1.5, label='Channel')
205
ax1.axhline(y=0, color='gray', ls='--', alpha=0.5)
206
ax1.set_xlabel('Distance (m)')
207
ax1.set_ylabel(r'$\Delta g$ (mGal)')
208
ax1.set_title('Individual Source Anomalies')
209
ax1.legend(fontsize=7, loc='upper right')
210
ax1.grid(True, alpha=0.3)
211
212
# Plot 2: Combined anomaly with noise
213
ax2 = fig.add_subplot(gs[0, 1])
214
ax2.plot(x, g_total, 'b-', lw=1.5, label='True')
215
ax2.plot(x, g_observed, 'k.', ms=1, alpha=0.5, label='Observed')
216
ax2.set_xlabel('Distance (m)')
217
ax2.set_ylabel(r'$\Delta g$ (mGal)')
218
ax2.set_title('Combined Anomaly')
219
ax2.legend(fontsize=7)
220
ax2.grid(True, alpha=0.3)
221
222
# Plot 3: Bouguer corrections
223
ax3 = fig.add_subplot(gs[0, 2])
224
ax3.plot(stations/1000, elevation, 'k-', lw=2, label='Elevation')
225
ax3_twin = ax3.twinx()
226
ax3_twin.plot(stations/1000, free_air, 'r--', lw=1.5, label='Free-air')
227
ax3_twin.plot(stations/1000, bouguer_anomaly, 'b-', lw=1.5, label='Bouguer')
228
ax3.set_xlabel('Distance (km)')
229
ax3.set_ylabel('Elevation (m)', color='black')
230
ax3_twin.set_ylabel('Correction (mGal)', color='blue')
231
ax3.set_title('Gravity Corrections')
232
ax3_twin.legend(fontsize=7, loc='upper right')
233
ax3.grid(True, alpha=0.3)
234
235
# Plot 4: Cross-section with sources
236
ax4 = fig.add_subplot(gs[1, 0])
237
ax4.plot(x, g_total, 'b-', lw=2)
238
ax4.set_xlabel('Distance (m)')
239
ax4.set_ylabel(r'$\Delta g$ (mGal)', color='blue')
240
241
# Draw subsurface bodies
242
ax4_sub = ax4.twinx()
243
theta = np.linspace(0, 2*np.pi, 50)
244
# Sphere
245
ax4_sub.fill(R1*np.cos(theta), z1 + R1*np.sin(theta), 'brown', alpha=0.7)
246
# Cavity
247
ax4_sub.fill(-500 + R2*np.cos(theta), z2 + R2*np.sin(theta), 'white',
248
edgecolor='red', lw=2)
249
# Channel
250
ax4_sub.fill(-800 + R3*np.cos(theta), z3 + R3*np.sin(theta), 'yellow', alpha=0.7)
251
ax4_sub.set_ylabel('Depth (m)')
252
ax4_sub.set_ylim([1200, 0])
253
ax4_sub.set_xlim([-3000, 3000])
254
ax4.set_title('Cross-Section View')
255
256
# Plot 5: Second derivative (edge detection)
257
ax5 = fig.add_subplot(gs[1, 1])
258
ax5.plot(x, g_second_deriv * 1e6, 'purple', lw=1.5)
259
ax5.axhline(y=0, color='gray', ls='--', alpha=0.5)
260
ax5.set_xlabel('Distance (m)')
261
ax5.set_ylabel(r"$\partial^2 g/\partial x^2$ ($\mu$Gal/m$^2$)")
262
ax5.set_title('Second Horizontal Derivative')
263
ax5.grid(True, alpha=0.3)
264
265
# Plot 6: Depth estimation comparison
266
ax6 = fig.add_subplot(gs[1, 2])
267
methods = ['True', 'Half-width', 'Euler']
268
depths_comp = [z1, z_estimated, z_estimated * 0.95] # Euler approximation
269
colors = ['green', 'blue', 'red']
270
bars = ax6.bar(methods, depths_comp, color=colors, alpha=0.7)
271
ax6.set_ylabel('Depth (m)')
272
ax6.set_title('Depth Estimation Methods')
273
ax6.grid(True, alpha=0.3, axis='y')
274
for bar, depth in zip(bars, depths_comp):
275
ax6.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 20,
276
f'{depth:.0f}m', ha='center', fontsize=8)
277
278
# Plot 7: Regional/Residual separation
279
ax7 = fig.add_subplot(gs[2, 0])
280
ax7.plot(x, g_total, 'k-', lw=1, label='Total', alpha=0.7)
281
ax7.plot(x, g_regional, 'r--', lw=1.5, label='Regional')
282
ax7.plot(x, g_residual, 'b-', lw=1.5, label='Residual')
283
ax7.set_xlabel('Distance (m)')
284
ax7.set_ylabel(r'$\Delta g$ (mGal)')
285
ax7.set_title('Regional-Residual Separation')
286
ax7.legend(fontsize=7)
287
ax7.grid(True, alpha=0.3)
288
289
# Plot 8: Power spectrum
290
ax8 = fig.add_subplot(gs[2, 1])
291
valid = (pos_freqs > 0) & (pos_power > 1e-10)
292
ax8.semilogy(pos_freqs[valid] * 1000, pos_power[valid], 'b-', lw=1.5)
293
ax8.set_xlabel('Wavenumber (1/km)')
294
ax8.set_ylabel('Power')
295
ax8.set_title('Power Spectrum')
296
ax8.grid(True, alpha=0.3, which='both')
297
298
# Plot 9: Density contrast effect
299
ax9 = fig.add_subplot(gs[2, 2])
300
contrasts = [500, 1000, 2000, 3000]
301
for drho in contrasts:
302
g_test = sphere_anomaly(x, 500, 150, drho)
303
ax9.plot(x/1000, g_test, lw=1.5, label=f'{drho} kg/m$^3$')
304
ax9.set_xlabel('Distance (km)')
305
ax9.set_ylabel(r'$\Delta g$ (mGal)')
306
ax9.set_title('Effect of Density Contrast')
307
ax9.legend(fontsize=7)
308
ax9.grid(True, alpha=0.3)
309
310
plt.savefig('gravity_anomaly_plot.pdf', bbox_inches='tight', dpi=150)
311
print(r'\begin{center}')
312
print(r'\includegraphics[width=\textwidth]{gravity_anomaly_plot.pdf}')
313
print(r'\end{center}')
314
plt.close()
315
316
# Summary statistics
317
max_anomaly = np.max(g_sphere)
318
min_anomaly = np.min(g_cavity)
319
total_mass = 4/3 * np.pi * R1**3 * drho1
320
depth_error = abs(z_estimated - z1) / z1 * 100
321
\end{pycode}
322
323
\section{Results and Analysis}
324
325
\subsection{Forward Modeling Results}
326
327
\begin{pycode}
328
print(r'\begin{table}[htbp]')
329
print(r'\centering')
330
print(r'\caption{Gravity Anomaly Source Parameters and Results}')
331
print(r'\begin{tabular}{lcccc}')
332
print(r'\toprule')
333
print(r'Source & Depth (m) & Size (m) & $\Delta\rho$ (kg/m$^3$) & Peak (mGal) \\')
334
print(r'\midrule')
335
print(f'Ore Body (sphere) & {z1} & R={R1} & {drho1} & {max_anomaly:.3f} \\\\')
336
print(f'Cavity (sphere) & {z2} & R={R2} & {drho2} & {min_anomaly:.3f} \\\\')
337
print(f'Channel (cylinder) & {z3} & R={R3} & {drho3} & {np.max(np.abs(g_channel)):.3f} \\\\')
338
print(r'\bottomrule')
339
print(r'\end{tabular}')
340
print(r'\end{table}')
341
\end{pycode}
342
343
\subsection{Depth Estimation}
344
345
The half-width method provides rapid depth estimation from anomaly profiles:
346
\begin{itemize}
347
\item Measured half-width: \py{f"{x_half_width:.0f}"} m
348
\item Estimated depth: \py{f"{z_estimated:.0f}"} m
349
\item True depth: \py{f"{z1}"} m
350
\item Estimation error: \py{f"{depth_error:.1f}"}\%
351
\end{itemize}
352
353
\begin{remark}
354
The half-width rule $z = 1.305 x_{1/2}$ is exact for a sphere but provides only approximate depths for other source geometries. For horizontal cylinders, use $z = 1.0 x_{1/2}$.
355
\end{remark}
356
357
\subsection{Bouguer Correction Analysis}
358
359
\begin{pycode}
360
print(r'\begin{table}[htbp]')
361
print(r'\centering')
362
print(r'\caption{Gravity Corrections at Survey Stations}')
363
print(r'\begin{tabular}{cccccc}')
364
print(r'\toprule')
365
print(r'Station (m) & Elev (m) & FA (mGal) & Bouguer (mGal) & Terrain (mGal) & Net (mGal) \\')
366
print(r'\midrule')
367
for i in range(0, len(stations), 2):
368
fa = free_air[i]
369
bp = bouguer_plate[i]
370
tc = terrain_corr[i]
371
net = fa - bp + tc
372
print(f'{stations[i]} & {elevation[i]:.1f} & {fa:.2f} & {bp:.2f} & {tc:.2f} & {net:.2f} \\\\')
373
print(r'\bottomrule')
374
print(r'\end{tabular}')
375
print(r'\end{table}')
376
\end{pycode}
377
378
\section{Applications}
379
380
\begin{example}[Mineral Exploration]
381
Massive sulfide ore bodies typically have density contrasts of 1500--3000 kg/m$^3$ relative to host rock. A spherical ore body with $R = \py{R1}$ m and $\Delta\rho = \py{drho1}$ kg/m$^3$ at depth $z = \py{z1}$ m produces a maximum anomaly of \py{f"{max_anomaly:.2f}"} mGal, well above typical survey noise levels of 0.01--0.05 mGal.
382
\end{example}
383
384
\begin{example}[Cavity Detection]
385
Near-surface voids (sinkholes, tunnels, mine workings) produce negative anomalies. An air-filled cavity with $R = \py{R2}$ m at depth $z = \py{z2}$ m creates an anomaly of \py{f"{min_anomaly:.3f}"} mGal. Microgravity surveys with precision better than 0.01 mGal are required for such targets.
386
\end{example}
387
388
\section{Discussion}
389
390
The analysis demonstrates several key principles in gravity interpretation:
391
392
\begin{enumerate}
393
\item \textbf{Ambiguity}: Gravity data alone cannot uniquely determine source geometry. Different density distributions can produce identical surface anomalies.
394
\item \textbf{Resolution}: Gravity anomalies smooth with depth---deeper sources produce broader, lower-amplitude anomalies than shallow sources of equivalent excess mass.
395
\item \textbf{Regional-Residual Separation}: Filtering in the wavenumber domain allows separation of deep regional trends from shallow local targets.
396
\item \textbf{Quantitative Interpretation}: Depth estimation methods (half-width, Euler deconvolution, power spectrum analysis) provide constraints on source parameters.
397
\end{enumerate}
398
399
\section{Conclusions}
400
401
This computational analysis yields:
402
\begin{itemize}
403
\item Maximum ore body anomaly: \py{f"{max_anomaly:.3f}"} mGal at surface
404
\item Total excess mass estimate: \py{f"{total_mass:.2e}"} kg
405
\item Half-width depth estimate accuracy: \py{f"{100-depth_error:.1f}"}\%
406
\item Bouguer density assumed: \py{f"{rho_bouguer}"} kg/m$^3$
407
\end{itemize}
408
409
Gravity surveys remain essential tools in mineral exploration, engineering site investigation, and regional geological mapping due to their non-invasive nature and sensitivity to density variations.
410
411
\section{Further Reading}
412
\begin{itemize}
413
\item Blakely, R.J., \textit{Potential Theory in Gravity and Magnetic Applications}, Cambridge University Press, 1995
414
\item Telford, W.M., Geldart, L.P., Sheriff, R.E., \textit{Applied Geophysics}, 2nd Edition, Cambridge University Press, 1990
415
\item Reynolds, J.M., \textit{An Introduction to Applied and Environmental Geophysics}, Wiley-Blackwell, 2011
416
\end{itemize}
417
418
\end{document}
419
420