Bishop Basement Model#
Import packages#
[1]:
# set EPSG for plotting functions
import os
import pathlib
import pickle
import string
import numpy as np
import polartoolkit as ptk
import scipy as sp
import verde as vd
import xarray as xr
import invert4geom
os.environ["POLARTOOLKIT_EPSG"] = "10598"
/home/mdtanker/miniforge3/envs/invert4geom/lib/python3.12/site-packages/UQpy/__init__.py:6: UserWarning:
pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
Get data#
Here we will load a commonly used synthetic gravity and basement topography model, called the Bishop Model. It includes topography of the Moho and the sediment-basement contact, and the forward modelled gravity effect of each, providing a synthetic observed gravity dataset. The forward gravity is calculated with 6 layers of sediment which increase in density from 2100 kg/m3 at 0m to 2600 kg/m3 at the basement surface. Then 3 layers of increasing density from 2700 kg/m3 to 3300 kg/m3 at the
Moho surface.
[2]:
grid = invert4geom.load_bishop_model(coarsen_factor=50)
grid = grid.rename({"gravity": "gravity_anomaly_no_noise"})
# contaminate gravity with 0.2 mGal of random noise
grid["gravity_anomaly"], stddev = invert4geom.contaminate(
grid.gravity_anomaly_no_noise,
stddev=0.2,
percent=False,
seed=0,
)
grid["uncert"] = xr.full_like(grid.gravity_anomaly, stddev)
grid["upward"] = xr.full_like(grid.gravity_anomaly, 10)
grid
[2]:
<xarray.Dataset> Size: 74kB
Dimensions: (northing: 40, easting: 38)
Coordinates:
* northing (northing) float64 320B 1.459e+05 ... 5.359e+05
* easting (easting) float64 304B 6.9e+03 ... 3.769e+05
Data variables:
basement_topo (northing, easting) float64 12kB -6.199e+03 ......
moho_topo (northing, easting) float64 12kB -2.767e+04 ......
gravity_anomaly_no_noise (northing, easting) float64 12kB 99.08 ... 107.0
gravity_anomaly (northing, easting) float64 12kB 99.11 ... 106.9
uncert (northing, easting) float64 12kB 0.2 0.2 ... 0.2
upward (northing, easting) float64 12kB 10.0 ... 10.0[3]:
fig = ptk.plot_grid(
grid.basement_topo,
fig_height=10,
title="True basement model",
reverse_cpt=True,
cmap="rain",
cbar_label="m",
hist=True,
frame=["nSWe", "xaf10000", "yaf10000"],
)
fig = ptk.plot_grid(
grid.moho_topo,
fig=fig,
origin_shift="x",
fig_height=10,
title="True moho model",
reverse_cpt=True,
cmap="rain",
cbar_label="m",
hist=True,
frame=["nSwE", "xaf10000", "yaf10000"],
)
fig = ptk.plot_grid(
grid.gravity_anomaly,
fig=fig,
origin_shift="x",
fig_height=10,
title="Observed gravity",
reverse_cpt=True,
cmap="viridis",
cbar_label="mGal",
hist=True,
frame=["nSwE", "xaf10000", "yaf10000"],
)
fig.show()
Define model domain parameters#
To account for edge effects (decreasing gravity towards the edge of prism model), we will use a buffer region so the prism model edge is further away from the inversion domain (the gravity data extent).
[4]:
# get full region of gravity data
full_region = vd.get_region((grid.easting.values, grid.northing.values))
full_region
[4]:
(np.float64(6900.0),
np.float64(376900.0),
np.float64(145900.0),
np.float64(535900.0))
[5]:
# zoom in by 10 km from each side
inversion_region = vd.pad_region(full_region, -10e3)
inversion_region
[5]:
(np.float64(16900.0),
np.float64(366900.0),
np.float64(155900.0),
np.float64(525900.0))
[6]:
grav_grid = grid[["gravity_anomaly", "uncert", "upward"]]
# only retain data within the inversion region
grav_grid = grav_grid.sel(
easting=slice(inversion_region[0], inversion_region[1]),
northing=slice(inversion_region[2], inversion_region[3]),
)
Observed gravity data#
In this scenario, we are treating the area as having no surface topography (surface elevation is flat and equal to the ellipsoid). In this case, there is no terrain mass effect, and therefore the gravity disturbance is equal to the topo-free disturbance.
[7]:
grav_data = invert4geom.create_data(grav_grid)
print(f"Gravity region (W,E,S,N): {grav_data.region}")
print(f"Gravity spacing: {grav_data.spacing} m")
grav_data
Gravity region (W,E,S,N): (16900.0, 366900.0, 155900.0, 525900.0)
Gravity spacing: 10000.0 m
[7]:
<xarray.Dataset> Size: 33kB
Dimensions: (northing: 38, easting: 36)
Coordinates:
* northing (northing) float64 304B 1.559e+05 1.659e+05 ... 5.259e+05
* easting (easting) float64 288B 1.69e+04 2.69e+04 ... 3.669e+05
Data variables:
gravity_anomaly (northing, easting) float64 11kB 99.56 101.2 ... 108.6
uncert (northing, easting) float64 11kB 0.2 0.2 0.2 ... 0.2 0.2
upward (northing, easting) float64 11kB 10.0 10.0 ... 10.0 10.0
Attributes:
region: (16900.0, 366900.0, 155900.0, 525900.0)
spacing: 10000.0
buffer_width: 40000.0
inner_region: (56900.0, 326900.0, 195900.0, 485900.0)
dataset_type: data
model_type: prisms
coord_names: ('easting', 'northing')Create โa-prioriโ basement measurements#
[8]:
# create grid of 36 points
coords = vd.grid_coordinates(
region=vd.pad_region(inversion_region, -30e3),
shape=(6, 6),
)
da = vd.make_xarray_grid(coords, data=np.ones_like(coords[0]), data_names="upward")
constraint_points = vd.grid_to_table(da).drop(columns=["upward"])
# sample true topography at these points
constraint_points = invert4geom.sample_grids(
constraint_points,
grid.basement_topo,
"true_upward",
)
constraint_points["upward"] = constraint_points.true_upward
# re-sample depths with uncertainty to emulate measurement errors
# set each points uncertainty equal to 2% of depth
uncert = np.abs(0.02 * constraint_points.upward)
constraint_points.loc[constraint_points.index, "true_uncert"] = uncert
constraint_points = invert4geom.randomly_sample_data(
seed=0,
data_df=constraint_points,
data_col="upward",
uncert_col="true_uncert",
)
# create weights column
constraint_points["weight"] = 1 / (constraint_points.true_uncert**2)
# create estimated uncertainty column
uncert = np.abs(0.02 * constraint_points.upward)
constraint_points.loc[constraint_points.index, "uncert"] = uncert
constraint_points.head()
[8]:
| northing | easting | true_upward | upward | true_uncert | weight | uncert | |
|---|---|---|---|---|---|---|---|
| 0 | 185900.0 | 46900.0 | -6209.559082 | -6193.944497 | 124.191182 | 0.000065 | 123.878890 |
| 1 | 185900.0 | 104900.0 | -6719.067441 | -6736.819871 | 134.381349 | 0.000055 | 134.736397 |
| 2 | 185900.0 | 162900.0 | -7276.991871 | -7183.784863 | 145.539837 | 0.000047 | 143.675697 |
| 3 | 185900.0 | 220900.0 | -8420.454391 | -8402.788258 | 168.409088 | 0.000035 | 168.055765 |
| 4 | 185900.0 | 278900.0 | -8821.850945 | -8916.362853 | 176.437019 | 0.000032 | 178.327257 |
Create starting basement model#
[9]:
# grid the sampled values using verde
starting_topography_kwargs = dict(
method="splines",
region=full_region,
spacing=grav_data.spacing,
constraints_df=constraint_points,
# dampings=np.logspace(-40, 0, 200),
dampings=None,
# weights=constraint_points.weight,
)
starting_topography = invert4geom.create_topography(
**starting_topography_kwargs,
)
[10]:
_ = ptk.grid_compare(
grid.basement_topo,
starting_topography.upward,
region=inversion_region,
grid1_name="True topography",
grid2_name="Starting topography",
robust=True,
hist=True,
inset=False,
verbose="q",
title="difference",
reverse_cpt=True,
cmap="rain",
points=constraint_points,
points_style="x.3c",
)
[11]:
# sample the inverted topography at the constraint points
constraint_points = invert4geom.sample_grids(
constraint_points,
starting_topography.upward,
"starting_topography",
)
rmse = invert4geom.rmse(
constraint_points.true_upward - constraint_points.starting_topography
)
print(f"RMSE: {rmse:.2f} m")
RMSE: 84.87 m
[12]:
# create model object
model = invert4geom.create_model(
zref=constraint_points.upward.mean(),
density_contrast=2800 - 2300,
topography=starting_topography,
)
model.inv.plot_model(
color_by="density",
zscale=20,
)
Gravity misfit#
All inversions in Invert4Geom are based on a gravity misfit, not a gravity anomaly. This means before the inversion, we must create a starting prism model, forward model itโs gravity effect, remove it from the gravity anomaly, and get a gravity misfit.
However, if we know nothing about the starting model, it can simply be a flat layer of zero thickness, as we will use here. In this case, the forward gravity would just be zero so there is no need to perform the forward modelling. The misfit is therefore just equal to the topo-free disturbance.
Forward gravity of starting prism layer#
[13]:
# calculate forward gravity of starting prism layer
grav_data.inv.forward_gravity(model)
[14]:
_ = ptk.grid_compare(
grav_data.gravity_anomaly,
grav_data.forward_gravity,
grid1_name="Observed gravity",
grid2_name="Starting gravity",
robust=True,
hist=True,
inset=False,
verbose="q",
title="Gravity misfit",
rmse_in_title=False,
points=constraint_points,
points_style="x.3c",
)
Regional estimation - constraint point minimization#
[15]:
# use the constraints to find the best regional field
regional_grav_kwargs = dict(
method="constraints",
grid_method="eq_sources",
constraints_df=constraint_points,
# constraints_weights_column="weight",
cv=True,
cv_kwargs=dict(
n_trials=50,
damping_limits=(1e-60, 10),
depth_limits=(100, 1000e3),
progressbar=False,
fname="../tmp/bishop_model_regional_sep",
),
# damping=None,
# depth="default",
block_size=None,
)
grav_data.inv.regional_separation(
**regional_grav_kwargs,
)
grav_data.inv.df.describe()
[15]:
| northing | easting | gravity_anomaly | uncert | upward | forward_gravity | misfit | reg | res | starting_forward_gravity | starting_misfit | starting_reg | starting_res | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 1368.000000 | 1368.000000 | 1368.000000 | 1.368000e+03 | 1368.0 | 1368.000000 | 1368.000000 | 1368.000000 | 1368.000000 | 1368.000000 | 1368.000000 | 1368.000000 | 1368.000000 |
| mean | 340900.000000 | 191900.000000 | 115.021219 | 2.000000e-01 | 10.0 | 0.896367 | 114.124852 | 114.529292 | -0.404440 | 0.896367 | 114.124852 | 114.529292 | -0.404440 |
| std | 109698.662868 | 103920.936691 | 8.668170 | 8.329718e-17 | 0.0 | 40.280643 | 39.807129 | 39.976472 | 5.720895 | 40.280643 | 39.807129 | 39.976472 | 5.720895 |
| min | 155900.000000 | 16900.000000 | 91.608025 | 2.000000e-01 | 10.0 | -59.217044 | 32.942960 | 31.710159 | -21.857700 | -59.217044 | 32.942960 | 31.710159 | -21.857700 |
| 25% | 245900.000000 | 104400.000000 | 109.730411 | 2.000000e-01 | 10.0 | -27.678731 | 80.084974 | 81.831830 | -2.748203 | -27.678731 | 80.084974 | 81.831830 | -2.748203 |
| 50% | 340900.000000 | 191900.000000 | 115.379669 | 2.000000e-01 | 10.0 | -8.689017 | 122.087862 | 122.950898 | -0.421042 | -8.689017 | 122.087862 | 122.950898 | -0.421042 |
| 75% | 435900.000000 | 279400.000000 | 120.316522 | 2.000000e-01 | 10.0 | 35.669196 | 144.293965 | 142.981497 | 1.877564 | 35.669196 | 144.293965 | 142.981497 | 1.877564 |
| max | 525900.000000 | 366900.000000 | 142.955701 | 2.000000e-01 | 10.0 | 82.655013 | 179.574375 | 177.935318 | 34.954172 | 82.655013 | 179.574375 | 177.935318 | 34.954172 |
[16]:
grav_data.inv.plot_anomalies()
Single inversion#
Perform a single inversion to experiment with values of stopping criteria.
[17]:
# setup the inversion
inv = invert4geom.Inversion(
grav_data,
model,
solver_damping=0.01,
# set stopping criteria
l2_norm_tolerance=0.5,
delta_l2_norm_tolerance=1.008,
)
[18]:
# run the inversion
inv.invert(
plot_dynamic_convergence=True,
results_fname="../tmp/bishop_model",
)
[19]:
inv.stats_df
[19]:
| iteration | rmse | l2_norm | delta_l2_norm | iter_time_sec | |
|---|---|---|---|---|---|
| 0 | 0.0 | 4.269698 | 2.066325 | inf | NaN |
| 1 | 1.0 | 1.474512 | 1.214295 | 1.701666 | 1.454722 |
| 2 | 2.0 | 0.721347 | 0.849321 | 1.429723 | 0.253134 |
| 3 | 3.0 | 0.447640 | 0.669059 | 1.269427 | 0.262714 |
| 4 | 4.0 | 0.325153 | 0.570222 | 1.173330 | 0.245501 |
| 5 | 5.0 | 0.260119 | 0.510019 | 1.118042 | 0.249381 |
| 6 | 6.0 | 0.220256 | 0.469315 | 1.086731 | 0.252319 |
[20]:
inv.plot_inversion_results(
iters_to_plot=2,
)
[21]:
_ = ptk.grid_compare(
grid.basement_topo,
inv.model.topography,
region=inversion_region,
grid1_name="True topography",
grid2_name="Inverted topography",
robust=True,
hist=True,
inset=False,
verbose="q",
title="difference",
reverse_cpt=True,
cmap="rain",
points=constraint_points,
points_style="x.3c",
)
Damping parameter cross validation#
[22]:
inv.reinitialize_inversion()
# resample data at 1/2 spacing to include test points for cross-validation
inv.data = invert4geom.add_test_points(inv.data)
[23]:
damping_cv_obj = inv.optimize_inversion_damping(
damping_limits=(0.001, 1),
n_trials=8,
fname="../tmp/bishop_model_damping_cv",
)
inv.solver_damping
[23]:
0.004113679284993101
[24]:
inv.plot_convergence()
inv.plot_inversion_results(iters_to_plot=2)
_ = ptk.grid_compare(
grid.basement_topo,
inv.model.topography,
grid1_name="True topography",
grid2_name="Inverted topography",
robust=True,
hist=True,
inset=False,
title="difference",
reverse_cpt=True,
cmap="rain",
points=constraint_points,
points_style="x.3c",
)
[25]:
# sample the inverted topography at the constraint points
constraint_points = invert4geom.sample_grids(
constraint_points,
inv.model.topography,
"inverted_topography",
)
rmse = invert4geom.rmse(
constraint_points.true_upward - constraint_points.inverted_topography
)
print(f"RMSE: {rmse:.2f} m")
RMSE: 130.38 m
Density contrast / zref optimization#
Since this optimization uses the inversion error at constraint points, we canโt use the constraint point minimization technique to estimate the regional field since that inherently sets the inversion error at constraints to zero, invalidating the optimization scores.
There are two options for how to get around this issue:
use a different regional estimation technique while finding the optimal density contrast and zref values, then use the found optimal values with the constraint point minimization regional estimation technique afterwards.
separate the constraints into testing and training sets, so that only the training set is used during the regional separation, and only the testing set is used for scoring the density contrast and zref optimization.
Well just use the 1st option below.
Density optimization#
[26]:
inv.reinitialize_inversion()
[27]:
# run the optimization for the density contrast
density_optimization_obj = inv.optimize_inversion_zref_density_contrast(
constraints_df=constraint_points,
density_contrast_limits=(100, 600),
n_trials=10,
regional_grav_kwargs={
"method": "constant",
"constant": inv.data.reg.mean(),
},
starting_topography=starting_topography,
plot_scores=True,
fname="../tmp/bishop_model_density_optimization",
)
Best density_contrast value (600) is at the limit of provided values (600, 100) and thus is likely not a global minimum, expand the range of values tested to ensure the best parameter value is found.
'reg' already a column of `grav_df`, but is being overwritten since calculate_regional_misfit is True
[28]:
# The optimal value has been saved as the Model's density contrast for future inversions
print(inv.model.density_contrast)
600
Zref optimization#
[29]:
inv.reinitialize_inversion()
[30]:
# run the optimization for the zref
zref_optimization_obj = inv.optimize_inversion_zref_density_contrast(
constraints_df=constraint_points,
zref_limits=(-8e3, 0),
n_trials=10,
regional_grav_kwargs={
"method": "constant",
"constant": inv.data.reg.mean(),
},
starting_topography=starting_topography,
plot_scores=True,
fname="../tmp/bishop_model_zref_optimization",
)
'reg' already a column of `grav_df`, but is being overwritten since calculate_regional_misfit is True
[31]:
# The optimal value has been saved as the model's density contrast for future inversions
print(inv.model.zref)
-6090.645590613878
Redo regional separation and inversion with optimal values#
[32]:
# recreate model with optimal zref and density contrast
model = invert4geom.create_model(
zref=inv.model.zref,
density_contrast=inv.model.density_contrast,
topography=starting_topography,
)
# calculate forward gravity of starting prism layer
grav_data.inv.forward_gravity(model)
# calculate regional gravity
grav_data.inv.regional_separation(
**regional_grav_kwargs,
)
grav_data.inv.plot_anomalies()
[33]:
# setup the inversion
optimal_inv = invert4geom.Inversion(
grav_data,
model,
solver_damping=inv.solver_damping,
# set stopping criteria
l2_norm_tolerance=0.5,
delta_l2_norm_tolerance=1.008,
)
optimal_inv.solver_damping, optimal_inv.model.zref, optimal_inv.model.density_contrast
[33]:
(0.004113679284993101, -6090.645590613878, 600)
[34]:
# run the inversion
optimal_inv.invert(
plot_dynamic_convergence=True,
results_fname="../tmp/bishop_model_optimal",
)
[35]:
_ = ptk.grid_compare(
grid.basement_topo,
optimal_inv.model.topography,
region=inversion_region,
grid1_name="True topography",
grid2_name="Inverted topography",
robust=True,
hist=True,
inset=False,
verbose="q",
title="difference",
reverse_cpt=True,
cmap="rain",
points=constraint_points,
points_style="x.3c",
)
[36]:
# sample the inverted topography at the constraint points
constraint_points = ptk.sample_grids(
constraint_points,
optimal_inv.model.topography,
"inverted_topography",
)
rmse = ptk.rmse(constraint_points.true_upward - constraint_points.inverted_topography)
print(f"RMSE: {rmse:.2f} m")
RMSE: 121.55 m
Absolute value of inversion error#
[37]:
inversion_error = np.abs(grid.basement_topo - optimal_inv.model.topography)
fig = ptk.plot_grid(
inversion_error,
hist=True,
cmap="thermal",
title="Absolute value of inversion error",
robust=True,
points=constraint_points,
points_style="x.4c",
# points_pen="1p",
points_fill="white",
)
fig.show()
[38]:
# plotting functions for uncertainty results
def uncert_plots(
results,
inversion_region,
spacing,
true_topography,
constraints_df=None,
weight_by=None,
):
if (weight_by == "constraints") & (constraints_df is None):
msg = "must provide constraints_df if weighting by constraints"
raise ValueError(msg)
stats_ds = invert4geom.merged_stats(
results=results,
plot=True,
constraints_df=constraints_df,
weight_by=weight_by,
region=inversion_region,
)
try:
mean = stats_ds.weighted_mean
stdev = stats_ds.weighted_stdev
except AttributeError:
mean = stats_ds.z_mean
stdev = stats_ds.z_stdev
_ = ptk.grid_compare(
true_topography,
mean,
region=vd.pad_region(inversion_region, -spacing),
grid1_name="True topography",
grid2_name="Inverted topography",
robust=True,
hist=True,
inset=False,
title="difference",
reverse_cpt=True,
cmap="rain",
points=constraints_df,
points_style="x.3c",
)
_ = ptk.grid_compare(
np.abs(true_topography - mean),
stdev,
region=vd.pad_region(inversion_region, -spacing),
grid1_name="True error",
grid2_name="Stochastic uncertainty",
cmap="thermal",
robust=True,
hist=True,
inset=False,
title="difference",
points=constraints_df,
points_style="x.3c",
points_fill="white",
)
return stats_ds
Uncertainty analysis#
[39]:
# get mean spacing between nearest constraints
mean_constraint_spacing = np.mean(
vd.median_distance(
(constraint_points.easting, constraint_points.northing),
k_nearest=1,
)
)
mean_constraint_spacing
[39]:
np.float64(58000.0)
[40]:
# use optimal eq source parameters for regional separation in uncertainty analysis
# re-load the study from the saved pickle file
with pathlib.Path(f"{regional_grav_kwargs.get('cv_kwargs').get('fname')}.pickle").open(
"rb"
) as f:
study = pickle.load(f)
reg_eq_damping = min(study.best_trials, key=lambda t: t.values[0]).params["damping"]
reg_eq_depth = min(study.best_trials, key=lambda t: t.values[0]).params["depth"]
new_regional_grav_kwargs = dict(
method="constraints",
grid_method="eq_sources",
constraints_df=constraint_points,
damping=reg_eq_damping,
depth=reg_eq_depth,
block_size=None,
)
reg_eq_damping, reg_eq_depth
[40]:
(6.1222360462874075e-55, 107860.50329991267)
Damping component#
[41]:
# load study
with pathlib.Path("../tmp/bishop_model_damping_cv_study.pickle").open("rb") as f:
study = pickle.load(f)
study_df = study.trials_dataframe()
study_df = study_df.sort_values("value")
# calculate zscores of values
study_df["value_zscore"] = sp.stats.zscore(study_df["value"])
# drop outliers (values with zscore > |2|)
study_df2 = study_df[(np.abs(study_df.value_zscore) < 2)]
# pick damping standard deviation based on optimization
stdev = np.log10(study_df2.params_damping).std()
print(f"calculated stdev: {stdev}")
# stdev = 0.4
print(f"using stdev: {stdev}")
calculated stdev: 0.6236647974305001
using stdev: 0.6236647974305001
[42]:
fig = invert4geom.plot_scores(
study_df.value,
study_df.params_damping,
param_name="Damping",
logx=True,
logy=True,
)
ax = fig.axes[0]
best = float(study_df2.params_damping.iloc[0])
upper = float(10 ** (np.log10(best) + stdev))
lower = float(10 ** (np.log10(best) - stdev))
y_lims = ax.get_ylim()
ax.vlines(best, ymin=y_lims[0], ymax=y_lims[1], color="r")
ax.vlines(upper, ymin=y_lims[0], ymax=y_lims[1], label="+/- std")
ax.vlines(lower, ymin=y_lims[0], ymax=y_lims[1])
x_lims = ax.get_xlim()
ax.set_xlim(
min(x_lims[0], lower),
max(x_lims[1], upper),
)
ax.legend()
print("best:", best, "\nstd:", stdev, "\n+1std:", upper, "\n-1std:", lower)
best: 0.004113679284993101
std: 0.6236647974305001
+1std: 0.01729399094252044
-1std: 0.0009785108200892278
[43]:
solver_dict = {
"solver_damping": {
"distribution": "normal",
"loc": np.log10(optimal_inv.solver_damping), # mean base 10 exponent
"scale": stdev, # standard deviation of base 10 exponent
"log": True,
},
}
fname = "../tmp/bishop_model_uncertainty_damping"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_damping_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=10,
parameter_dict=solver_dict,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
stats_ds = uncert_plots(
uncert_damping_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
Density component#
[44]:
# load study
with pathlib.Path("../tmp/bishop_model_density_optimization_study.pickle").open(
"rb"
) as f:
study = pickle.load(f)
study_df = study.trials_dataframe()
study_df = study_df.sort_values("value")
# calculate zscores of values
study_df["value_zscore"] = sp.stats.zscore(study_df["value"])
# drop outliers (values with zscore > |2|)
study_df2 = study_df[(np.abs(study_df.value_zscore) < 2)]
density_stdev = study_df2.params_density_contrast.std()
print(f"calculated stdev: {density_stdev}")
# manually pick a stdev
# density_stdev = 20
print(f"using stdev: {density_stdev}")
calculated stdev: 156.12076877995588
using stdev: 156.12076877995588
[45]:
fig = invert4geom.plot_scores(
study.trials_dataframe().value.values,
study.trials_dataframe().params_density_contrast.values,
param_name="Density",
logx=False,
logy=False,
)
ax = fig.axes[0]
best = study_df2.params_density_contrast.iloc[0]
upper = best + density_stdev
lower = best - density_stdev
y_lims = ax.get_ylim()
ax.vlines(best, ymin=y_lims[0], ymax=y_lims[1], color="r")
ax.vlines(upper, ymin=y_lims[0], ymax=y_lims[1], label="+/- std")
ax.vlines(lower, ymin=y_lims[0], ymax=y_lims[1])
x_lims = ax.get_xlim()
ax.set_xlim(
min(x_lims[0], lower),
max(x_lims[1], upper),
)
ax.legend()
print("best:", best, "\nstd:", density_stdev, "\n+1std:", upper, "\n-1std:", lower)
best: 600
std: 156.12076877995588
+1std: 756.1207687799558
-1std: 443.8792312200441
[46]:
density_dict = {
"density_contrast": {
"distribution": "normal",
"loc": optimal_inv.model.density_contrast,
"scale": density_stdev,
},
}
fname = "../tmp/bishop_model_uncertainty_density"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_density_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=10,
parameter_dict=density_dict,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
uncert_plots(
uncert_density_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
[46]:
<xarray.Dataset> Size: 122kB
Dimensions: (northing: 30, easting: 28, runs: 10)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -5.895e+03 ... -6.091e+03
bottom (northing, easting) float64 7kB -6.091e+03 ... -6.731e+03
* runs (runs) <U5 200B 'run_0' 'run_1' 'run_2' ... 'run_8' 'run_9'
Data variables:
run_num (runs, northing, easting) float64 67kB -5.895e+03 ... -6....
z_mean (northing, easting) float64 7kB -5.902e+03 ... -6.735e+03
z_stdev (northing, easting) float64 7kB 15.82 30.49 ... 8.185 9.804
weighted_mean (northing, easting) float64 7kB -5.898e+03 ... -6.732e+03
weighted_stdev (northing, easting) float64 7kB 12.89 25.08 ... 7.283 8.064
z_min (northing, easting) float64 7kB -5.935e+03 ... -6.757e+03
z_max (northing, easting) float64 7kB -5.889e+03 ... -6.722e+03Zref component#
[47]:
# load study
with pathlib.Path("../tmp/bishop_model_zref_optimization_study.pickle").open("rb") as f:
study = pickle.load(f)
study_df = study.trials_dataframe()
study_df = study_df.sort_values("value")
# calculate zscores of values
study_df["value_zscore"] = sp.stats.zscore(study_df["value"])
# drop outliers (values with zscore > |2|)
study_df2 = study_df[(np.abs(study_df.value_zscore) < 2)]
zref_stdev = study_df2.params_zref.std()
print(f"calculated stdev: {zref_stdev}")
# manually pick a stdev
# zref_stdev = 20
print(f"using stdev: {zref_stdev}")
calculated stdev: 1296.6380165743738
using stdev: 1296.6380165743738
[48]:
fig = invert4geom.plot_scores(
study.trials_dataframe().value.values,
study.trials_dataframe().params_zref.values,
param_name="Reference level",
logx=False,
logy=False,
)
ax = fig.axes[0]
best = study_df2.params_zref.iloc[0]
upper = best + zref_stdev
lower = best - zref_stdev
y_lims = ax.get_ylim()
ax.vlines(best, ymin=y_lims[0], ymax=y_lims[1], color="r")
ax.vlines(upper, ymin=y_lims[0], ymax=y_lims[1], label="+/- std")
ax.vlines(lower, ymin=y_lims[0], ymax=y_lims[1])
x_lims = ax.get_xlim()
ax.set_xlim(
min(x_lims[0], lower),
max(x_lims[1], upper),
)
ax.legend()
print("best:", best, "\nstd:", zref_stdev, "\n+1std:", upper, "\n-1std:", lower)
best: -6090.645590613878
std: 1296.6380165743738
+1std: -4794.007574039504
-1std: -7387.283607188252
[49]:
zref_dict = {
"zref": {
"distribution": "normal",
"loc": optimal_inv.model.zref,
"scale": zref_stdev,
},
}
fname = "../tmp/bishop_model_uncertainty_zref"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_zref_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=10,
parameter_dict=zref_dict,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
uncert_plots(
uncert_zref_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
[49]:
<xarray.Dataset> Size: 122kB
Dimensions: (northing: 30, easting: 28, runs: 10)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -5.895e+03 ... -5.928e+03
bottom (northing, easting) float64 7kB -5.928e+03 ... -6.732e+03
* runs (runs) <U5 200B 'run_0' 'run_1' 'run_2' ... 'run_8' 'run_9'
Data variables:
run_num (runs, northing, easting) float64 67kB -5.895e+03 ... -6....
z_mean (northing, easting) float64 7kB -5.894e+03 ... -6.734e+03
z_stdev (northing, easting) float64 7kB 2.942 9.064 ... 1.447 2.443
weighted_mean (northing, easting) float64 7kB -5.894e+03 ... -6.734e+03
weighted_stdev (northing, easting) float64 7kB 2.939 8.992 ... 1.438 2.437
z_min (northing, easting) float64 7kB -5.898e+03 ... -6.739e+03
z_max (northing, easting) float64 7kB -5.889e+03 ... -6.732e+03Constraints component#
[50]:
fname = "../tmp/bishop_model_uncertainty_constraints"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_constraints_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=10,
sample_constraints=True,
constraints_df=constraint_points,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
uncert_plots(
uncert_constraints_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
[50]:
<xarray.Dataset> Size: 122kB
Dimensions: (northing: 30, easting: 28, runs: 10)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -5.855e+03 ... -6.091e+03
bottom (northing, easting) float64 7kB -6.091e+03 ... -6.699e+03
* runs (runs) <U5 200B 'run_0' 'run_1' 'run_2' ... 'run_8' 'run_9'
Data variables:
run_num (runs, northing, easting) float64 67kB -5.855e+03 ... -6....
z_mean (northing, easting) float64 7kB -5.9e+03 ... -6.71e+03
z_stdev (northing, easting) float64 7kB 99.53 106.8 ... 54.62 62.1
weighted_mean (northing, easting) float64 7kB -5.9e+03 ... -6.718e+03
weighted_stdev (northing, easting) float64 7kB 90.1 97.32 ... 53.51 59.33
z_min (northing, easting) float64 7kB -6.076e+03 ... -6.809e+03
z_max (northing, easting) float64 7kB -5.705e+03 ... -6.59e+03Gravity component#
[51]:
fname = "../tmp/bishop_model_uncertainty_grav"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_grav_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=10,
sample_gravity=True,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
uncert_plots(
uncert_grav_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
[51]:
<xarray.Dataset> Size: 122kB
Dimensions: (northing: 30, easting: 28, runs: 10)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -5.919e+03 ... -6.091e+03
bottom (northing, easting) float64 7kB -6.091e+03 ... -6.732e+03
* runs (runs) <U5 200B 'run_0' 'run_1' 'run_2' ... 'run_8' 'run_9'
Data variables:
run_num (runs, northing, easting) float64 67kB -5.919e+03 ... -6....
z_mean (northing, easting) float64 7kB -5.939e+03 ... -6.704e+03
z_stdev (northing, easting) float64 7kB 26.57 74.08 ... 31.12 29.37
weighted_mean (northing, easting) float64 7kB -5.937e+03 ... -6.706e+03
weighted_stdev (northing, easting) float64 7kB 26.57 73.95 ... 31.16 29.47
z_min (northing, easting) float64 7kB -5.983e+03 ... -6.745e+03
z_max (northing, easting) float64 7kB -5.901e+03 ... -6.66e+03Regional field estimation uncertainty#
We will do the same thing for the regional estimation procedure. First we will re-separate the regional to see what equivalent source gridding parameter values were determined optimal. We then estimate an uncertainty distribution for these parameter values, and create an ensemble of regional models which each randomly sample both the parameter values and the gravity data.
[52]:
mean_constraint_spacing, reg_eq_depth, reg_eq_damping
[52]:
(np.float64(58000.0), 107860.50329991267, 6.1222360462874075e-55)
[53]:
np.abs(np.log10(reg_eq_damping))
[53]:
np.float64(54.213089929945504)
[54]:
regional_misfit_parameter_dict = {
"depth": {
"distribution": "uniform",
"loc": 2 * mean_constraint_spacing, # lower bound
"scale": 5 * mean_constraint_spacing, # range, 2+5=7
},
# "depth": {
# "distribution": "normal",
# "loc": reg_eq_depth, # mean base 10 exponent
# "scale": reg_eq_depth / 5, # standard deviation of exponent
# },
"damping": {
"distribution": "uniform",
"loc": np.log10(reg_eq_damping),
"scale": np.abs(np.log10(reg_eq_damping)),
# "scale": 2,
"log": True,
},
}
regional_misfit_stats, _ = invert4geom.regional_misfit_uncertainty(
runs=100,
parameter_dict=regional_misfit_parameter_dict,
grav_ds=optimal_inv.data,
region=optimal_inv.data.inner_region,
**new_regional_grav_kwargs,
)
[55]:
# update grav dataset
optimal_inv.data["reg_uncert"] = regional_misfit_stats.z_stdev
Regional gravity component#
[56]:
fname = "../tmp/bishop_model_uncertainty_regional"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_regional_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=10,
regional_misfit_parameter_dict=regional_misfit_parameter_dict,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
uncert_plots(
uncert_regional_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
[56]:
<xarray.Dataset> Size: 122kB
Dimensions: (northing: 30, easting: 28, runs: 10)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -5.931e+03 ... -6.091e+03
bottom (northing, easting) float64 7kB -6.091e+03 ... -6.249e+03
* runs (runs) <U5 200B 'run_0' 'run_1' 'run_2' ... 'run_8' 'run_9'
Data variables:
run_num (runs, northing, easting) float64 67kB -5.931e+03 ... -6....
z_mean (northing, easting) float64 7kB -5.88e+03 ... -6.504e+03
z_stdev (northing, easting) float64 7kB 97.06 90.44 ... 264.0 278.5
weighted_mean (northing, easting) float64 7kB -5.906e+03 ... -6.451e+03
weighted_stdev (northing, easting) float64 7kB 43.81 46.83 ... 184.2 182.0
z_min (northing, easting) float64 7kB -5.994e+03 ... -7.166e+03
z_max (northing, easting) float64 7kB -5.611e+03 ... -6.09e+03Total Uncertainty#
[57]:
fname = "../tmp/bishop_model_uncertainty_full"
# delete files if already exist
for p in pathlib.Path().glob(f"{fname}*"):
p.unlink(missing_ok=True)
uncert_results = invert4geom.full_workflow_uncertainty_loop(
optimal_inv,
fname=fname,
runs=20,
sample_gravity=True,
sample_constraints=True,
constraints_df=constraint_points,
parameter_dict=density_dict | zref_dict | solver_dict,
regional_grav_kwargs=new_regional_grav_kwargs,
starting_topography_kwargs=starting_topography_kwargs,
)
uncert_plots(
uncert_results,
optimal_inv.data.inner_region,
optimal_inv.data.spacing,
grid.basement_topo,
constraints_df=constraint_points,
weight_by="constraints",
)
[57]:
<xarray.Dataset> Size: 189kB
Dimensions: (northing: 30, easting: 28, runs: 20)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -3.549e+03 ... -3.549e+03
bottom (northing, easting) float64 7kB -3.897e+03 ... -5.121e+03
* runs (runs) <U6 480B 'run_0' 'run_1' ... 'run_18' 'run_19'
Data variables:
run_num (runs, northing, easting) float64 134kB -3.897e+03 ... -6...
z_mean (northing, easting) float64 7kB -4.573e+03 ... -6.099e+03
z_stdev (northing, easting) float64 7kB 439.9 583.7 ... 514.7 616.9
weighted_mean (northing, easting) float64 7kB -4.661e+03 ... -6.156e+03
weighted_stdev (northing, easting) float64 7kB 417.6 427.1 ... 490.6 570.7
z_min (northing, easting) float64 7kB -5.341e+03 ... -7.189e+03
z_max (northing, easting) float64 7kB -3.707e+03 ... -4.971e+03Comparing results#
[58]:
results = [
uncert_results,
uncert_regional_results,
uncert_grav_results,
uncert_constraints_results,
uncert_density_results,
uncert_zref_results,
uncert_damping_results,
]
names = [
"full",
"regional",
"grav",
"constraints",
"density",
"zref",
"damping",
]
# get cell-wise stats for each ensemble
stats = []
for r in results:
ds = invert4geom.merged_stats(
results=r,
plot=False,
constraints_df=constraint_points,
weight_by="constraints",
region=optimal_inv.data.inner_region,
)
stats.append(ds)
[59]:
# get the standard deviation of the ensemble of ensembles
stdevs = []
for i, s in enumerate(stats):
stdevs.append(s.weighted_stdev.rename(f"{names[i]}_stdev"))
merged = xr.merge(stdevs, compat="override")
merged
[59]:
<xarray.Dataset> Size: 61kB
Dimensions: (northing: 30, easting: 28)
Coordinates:
* northing (northing) float64 240B 1.959e+05 2.059e+05 ... 4.859e+05
* easting (easting) float64 224B 5.69e+04 6.69e+04 ... 3.269e+05
top (northing, easting) float64 7kB -3.549e+03 ... -3.549e+03
bottom (northing, easting) float64 7kB -3.897e+03 ... -5.121e+03
Data variables:
full_stdev (northing, easting) float64 7kB 417.6 427.1 ... 570.7
regional_stdev (northing, easting) float64 7kB 43.81 46.83 ... 182.0
grav_stdev (northing, easting) float64 7kB 26.57 73.95 ... 29.47
constraints_stdev (northing, easting) float64 7kB 90.1 97.32 ... 59.33
density_stdev (northing, easting) float64 7kB 12.89 25.08 ... 8.064
zref_stdev (northing, easting) float64 7kB 2.939 8.992 ... 2.437
damping_stdev (northing, easting) float64 7kB 29.11 31.65 ... 19.1[60]:
titles = [
"True error",
"Total uncertainty",
"Uncertainty from regional estimation",
"Uncertainty from gravity",
"Uncertainty from constraints",
"Uncertainty from density",
"Uncertainty from reference level",
"Uncertainty from damping",
]
grids = list(merged.data_vars.values())
# grids.insert(0, np.abs(stats[0].weighted_mean - grid.basement_topo))
grids.insert(0, inversion_error)
grids = [
g.sel(
easting=slice(
optimal_inv.data.inner_region[0], optimal_inv.data.inner_region[1]
),
northing=slice(
optimal_inv.data.inner_region[2], optimal_inv.data.inner_region[3]
),
)
for g in grids
]
cpt_lims = ptk.get_min_max(
grids[0],
robust=True,
)
fig_height = 9
for i, g in enumerate(grids):
xshift_amount = 1
if i == 0:
fig = None
origin_shift = "initialize"
# cpt_lims = ptk.get_min_max(
# g,
# robust=True,
# )
elif i == 4:
origin_shift = "both"
xshift_amount = -3
else:
origin_shift = "x"
fig = ptk.plot_grid(
grid=g,
fig_height=fig_height,
title=titles[i],
title_font="16p,Helvetica,black",
# cmap="thermal",
cpt_lims=cpt_lims,
robust=True,
cbar_label=f"standard deviation (m), mean: {int(np.nanmean(g))}",
hist=True,
fig=fig,
origin_shift=origin_shift,
xshift_amount=xshift_amount,
yshift_amount=-1.25,
points=constraint_points,
points_style="x.2c",
points_pen="1.5p",
points_fill="white",
)
fig.text(
position="TL",
text=f"{string.ascii_lowercase[i]}",
fill="white",
pen=True,
font="16p,Helvetica,black",
offset="j.6/.2",
clearance="+tO",
no_clip=True,
)
if i == 0:
# plot profile location, and endpoints on map
start = [optimal_inv.data.inner_region[1], optimal_inv.data.inner_region[3]]
stop = [optimal_inv.data.inner_region[0], optimal_inv.data.inner_region[2]]
fig.plot(
vd.line_coordinates(start, stop, size=100),
pen="2p,black",
)
fig.text(
x=start[0],
y=start[1],
text="A",
fill="white",
font="12p,Helvetica,black",
justify="CM",
clearance="+tO",
no_clip=True,
)
fig.text(
x=stop[0],
y=stop[1],
text="A' ",
fill="white",
font="12p,Helvetica,black",
justify="CM",
clearance="+tO",
no_clip=True,
)
fig.show()
[61]:
data_dict = ptk.make_data_dict(
names=titles,
grids=grids,
colors=[
"red",
"black",
"blue",
"magenta",
"cyan",
"green",
"purple",
"orange",
],
)
fig, df_data = ptk.plot_data(
"points",
start=[optimal_inv.data.inner_region[1], optimal_inv.data.inner_region[3]],
stop=[optimal_inv.data.inner_region[0], optimal_inv.data.inner_region[2]],
num=10000,
fig_height=4,
fig_width=15,
data_dict=data_dict,
data_legend_loc="jTR+jTL",
data_legend_box="+gwhite",
data_buffer=0.01,
data_frame=["neSW", "xafg+lDistance (m)", "yag+luncertainty (m)"],
# data_pen_style=[None,None,"4_2:2p"],
# data_pen_thickness=[1, 1.5, 1],
share_yaxis=True,
start_label="A",
end_label="A' ",
)
fig.show()
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
grdtrack [WARNING]: Some input points were outside the grid domain(s).
[ ]: