Skip to content

Commit 1a582f7

Browse files
committed
[ENH] Refactor gravity inversion pipeline and add flexibility
Refactored `setup_geophysics` to accept dynamic density inputs and improved `calculate_scale_shift` with robust validation and detailed docstrings. Simplified the gravity inversion example by removing redundant steps and adopting modular utilities.
1 parent 0dca07d commit 1a582f7

File tree

4 files changed

+92
-76
lines changed

4 files changed

+92
-76
lines changed

docs/dev_logs/2025_May.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@
4848
- This would be useful to be able to visualize whatever the likelihood function represents independent of the probabilistic model.
4949
-
5050
### What would I want to have to do StonePark example as flowless as possible?
51-
1. A way to save the model already with the correct **nugget effects** instead of having to "load nuggets" (TODO in gempy)
52-
2. Likelihood function plug and play
53-
3. Some sort of pre-analysis if the model looks good. Maybe some metrics about how long it takes to run the model.
51+
1. [x] A way to save the model already with the correct **nugget effects** instead of having to "load nuggets" (TODO in gempy)
52+
2. [ ] Likelihood function plug and play
53+
3. [ ] Some sort of pre-analysis if the model looks good. Maybe some metrics about how long it takes to run the model.
5454
1. Condition number
5555
2. Acceptance rate of the posterior
5656
4. Easy pykeops setting using .env

examples/examples/_aux_func_gravity_inversion.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ def initialize_geo_model(structural_elements: list[gp.data.StructuralElement], e
8080

8181
return geo_model
8282

83-
def setup_geophysics(env_path: str, geo_model: gp.data.GeoModel):
83+
def setup_geophysics(env_path: str, geo_model: gp.data.GeoModel, densities: list[float]):
8484
import pandas as pd
8585
from dotenv import dotenv_values
86+
from gempy_engine.core.backend_tensor import BackendTensor
87+
8688
config = dotenv_values()
8789

8890
df = pd.read_csv(
@@ -97,8 +99,7 @@ def setup_geophysics(env_path: str, geo_model: gp.data.GeoModel):
9799
interesting_columns = df[['X', 'Y', 'Bouguer_267_complete']]
98100
# %%
99101
device_location = interesting_columns[['X', 'Y']]
100-
# stack 0 to the z axis
101-
device_location['Z'] = 0
102+
device_location['Z'] = 0 # * stack 0 to the z axis
102103

103104
gp.set_centered_grid(
104105
grid=geo_model.grid,
@@ -110,10 +111,9 @@ def setup_geophysics(env_path: str, geo_model: gp.data.GeoModel):
110111
gravity_gradient = gp.calculate_gravity_gradient(geo_model.grid.centered_grid)
111112

112113
# %%
113-
from gempy_engine.core.backend_tensor import BackendTensor
114114
geo_model.geophysics_input = gp.data.GeophysicsInput(
115115
tz=BackendTensor.t.array(gravity_gradient),
116-
densities=BackendTensor.t.array([2.61, 2.92, 3.1, 2.92, 2.61, 2.61]),
116+
densities=BackendTensor.t.array(densities),
117117
)
118118

119119
return interesting_columns

examples/examples/gravity_inversion.py

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,45 @@
44
55
This example demonstrates a probabilistic inversion of a geological model using Bayesian methods.
66
"""
7-
7+
import numpy as np
88
import os
9-
import time
109

1110
import arviz as az
12-
import numpy as np
11+
import dotenv
12+
import gempy_engine
13+
import gempy_viewer as gpv
1314
import pyro
1415
import pyro.distributions as dist
1516
import torch
16-
import xarray as xr
17-
from dotenv import dotenv_values
17+
from gempy_engine.core.backend_tensor import BackendTensor
1818
from matplotlib import pyplot as plt
1919
from pyro.infer import MCMC, NUTS, Predictive
2020

2121
import gempy as gp
22-
import gempy_engine
23-
import gempy_viewer as gpv
24-
25-
from examples.examples._aux_func_gravity_inversion import calculate_scale_shift, gaussian_kernel, initialize_geo_model, setup_geophysics
26-
from examples.examples._aux_func_gravity_inversion_II import process_file
27-
28-
from gempy_engine.core.backend_tensor import BackendTensor
22+
from examples.examples._aux_func_gravity_inversion import setup_geophysics
23+
from gempy_probability.modules.likelihoods.gravity_likelihoods import calculate_scale_shift, gaussian_kernel
2924
from gempy_probability.modules.plot.plot_posterior import default_red, default_blue
3025

26+
dotenv.load_dotenv()
27+
3128
# %%
3229
# Config
3330
seed = 123456
3431
torch.manual_seed(seed)
3532
pyro.set_rng_seed(seed)
3633

37-
# %%
38-
# Start the timer for benchmarking purposes
39-
start_time = time.time()
40-
41-
# %%
42-
# Load necessary configuration and paths from environment variables
43-
config = dotenv_values()
44-
path = config.get("PATH_TO_MODEL_1_Subsurface")
45-
46-
# %%
47-
# Initialize lists to store structural elements for the geological model
48-
structural_elements = []
49-
global_extent = None
50-
color_gen = gp.data.ColorsGenerator()
51-
52-
# %%
53-
# Process each .nc file in the specified directory for model construction
54-
for filename in os.listdir(path):
55-
base, ext = os.path.splitext(filename)
56-
if ext == '.nc':
57-
structural_element, global_extent = process_file(os.path.join(path, filename), global_extent, color_gen)
58-
structural_elements.append(structural_element)
59-
60-
# %%
61-
# Configure GemPy for geological modeling with PyTorch backend
62-
BackendTensor.change_backend_gempy(engine_backend=gp.data.AvailableBackends.PYTORCH, dtype="float64")
63-
64-
geo_model = initialize_geo_model(
65-
structural_elements=structural_elements,
66-
extent=(np.array(global_extent)),
67-
topography=(xr.open_dataset(os.path.join(path, "Topography.nc"))),
68-
load_nuggets=False
69-
)
70-
71-
# TODO: Here is where we are setting the nuggets
72-
# * ============================================
34+
model_path = os.getenv("PATH_TO_NUGGET_TEST_MODEL")
35+
model_file = os.path.join(model_path, "nugget_effect_optimization.gempy")
36+
geo_model = gp.API.load_model(model_file)
7337

7438
# %%
39+
# Forward Gravity
40+
# ---------------
7541
# Setup geophysics configuration for the model
7642
geophysics_input = setup_geophysics(
7743
env_path="PATH_TO_MODEL_1_BOUGUER",
78-
geo_model=geo_model
44+
geo_model=geo_model,
45+
densities=[2.61, 2.92, 3.1, 2.92, 2.61, 2.61, 2.61]
7946
)
8047

8148
# %%
@@ -87,12 +54,13 @@
8754

8855
# %%
8956
# Compute the geological model
90-
sol = gp.compute_model(
57+
sol: gp.data.Solutions = gp.compute_model(
9158
gempy_model=geo_model,
9259
engine_config=gp.data.GemPyEngineConfig(
9360
backend=gp.data.AvailableBackends.PYTORCH,
9461
dtype='float64'
95-
)
62+
),
63+
validate_serialization=False # TODO: [ ] Validate this serialization
9664
)
9765

9866
# %%
@@ -106,12 +74,11 @@
10674

10775
# %%
10876
# Calculate and adapt the observed gravity data for model comparison
109-
grav = sol.gravity
110-
s, c = calculate_scale_shift(
111-
a=geophysics_input["Bouguer_267_complete"].values,
112-
b=(grav.detach().numpy())
77+
scale_factor, shift_term = calculate_scale_shift(
78+
measured=geophysics_input["Bouguer_267_complete"].values,
79+
simulated=sol.gravity.detach().numpy()
11380
)
114-
adapted_observed_grav = s * geophysics_input["Bouguer_267_complete"] + c
81+
adapted_observed_grav: np.dtype[float] = scale_factor * geophysics_input["Bouguer_267_complete"] + shift_term
11582

11683
# %%
11784
# Plot the 2D gravity data for visualization
@@ -127,7 +94,10 @@
12794
plt.colorbar(sc, label="mGal")
12895
plt.show()
12996

97+
13098
# %%
99+
# Define Probabilistic model
100+
# --------------------------
131101
# Define hyperparameters for the Bayesian geological model
132102
length_scale_prior = torch.tensor(1_000.0)
133103
variance_prior = torch.tensor(25.0 ** 2)
@@ -141,6 +111,7 @@
141111
densities=prior_tensor,
142112
)
143113

114+
bar
144115

145116
# %%
146117
# Define the Pyro probabilistic model for inversion

gempy_probability/modules/likelihoods/gravity_likelihoods/_utils.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,66 @@
11
import numpy as np
2-
from sklearn.linear_model import LinearRegression
2+
from typing import Tuple
33

4-
def calculate_scale_shift(a: np.ndarray, b: np.ndarray) -> tuple:
5-
# Reshape arrays for sklearn
6-
a_reshaped = a.reshape(-1, 1)
7-
b_reshaped = b.reshape(-1, 1)
4+
def calculate_scale_shift(
5+
measured: np.ndarray,
6+
simulated: np.ndarray
7+
) -> Tuple[float, float]:
8+
"""
9+
Compute the best-fit scale (slope) and shift (intercept) so that:
10+
simulated ≈ scale_factor * measured + shift_term
811
9-
# Linear regression
10-
model = LinearRegression().fit(a_reshaped, b_reshaped)
12+
Parameters
13+
----------
14+
measured : array-like, shape (n,)
15+
The observed (independent) values.
16+
simulated : array-like, shape (n,)
17+
The simulated or target (dependent) values.
1118
12-
# Get scale and shift
13-
s = model.coef_[0][0]
14-
c = model.intercept_[0]
19+
Returns
20+
-------
21+
scale_factor : float
22+
Multiplicative factor.
23+
shift_term : float
24+
Additive offset.
1525
16-
return s, c
26+
Raises
27+
------
28+
ValueError
29+
If lengths differ or fewer than 2 valid points remain.
30+
"""
31+
# Flatten inputs
32+
measured = np.asarray(measured).ravel()
33+
simulated = np.asarray(simulated).ravel()
1734

35+
# Check same length
36+
if measured.size != simulated.size:
37+
raise ValueError(
38+
f"Length mismatch: measured has {measured.size}, "
39+
f"simulated has {simulated.size}"
40+
)
41+
42+
# Mask out NaN or infinite entries
43+
valid_mask = np.isfinite(measured) & np.isfinite(simulated)
44+
measured_valid = measured[valid_mask]
45+
simulated_valid = simulated[valid_mask]
46+
47+
if measured_valid.size < 2:
48+
raise ValueError(
49+
"Need at least 2 valid (non-NaN/Inf) data points"
50+
)
51+
52+
# Build design matrix [measured_valid, 1]
53+
design_matrix = np.vstack([
54+
measured_valid,
55+
np.ones_like(measured_valid)
56+
]).T
57+
58+
# Solve least squares: design_matrix @ [scale_factor, shift_term] ≈ simulated_valid
59+
(scale_factor, shift_term), *_ = np.linalg.lstsq(design_matrix,
60+
simulated_valid,
61+
rcond=None)
62+
63+
return float(scale_factor), float(shift_term)
1864

1965
def gaussian_kernel(locations, length_scale, variance):
2066
import torch
@@ -24,4 +70,3 @@ def gaussian_kernel(locations, length_scale, variance):
2470
# Compute the covariance matrix using the Gaussian kernel
2571
covariance_matrix = variance * torch.exp(-0.5 * distance_squared / length_scale ** 2)
2672
return covariance_matrix
27-

0 commit comments

Comments
 (0)