3D gravity inversion of prism layers
See original GitHub issueDescription of the desired feature:
I’m working on a 3D gravity inversion for my Ph.D. to model the bathymetry beneath a float ice shelf. I’m using a vertical-prisms approach, and I’m mostly interested in a geometry inversion, where each prism’s density remains unchanged, but the prisms’ tops or bottoms are updated to minimize the observed gravity - forward gravity misfit. While this inversion is in the early stages, I’m already using several harmonica
tools so I think it would be great to include a fully supported inversion module in harmonica.
Below is a description of my specific use case, a list of what I already have coded, and ideas for additional features to add.
Current state of inversion
Model setup
-
input an arbitrary number of layer topographies (i.e. ice surface, ice base, bathymetry, basement, Moho)
xarray.load_dataset()
to load netcdf’s as gridspygmt.grdsample()
to make sure all grids’ extents match
-
create a layer of vertical prisms between each set of grids
harmonica.prism_layer()
- example: for water layer:
surface
= ice base,reference
= bathymetry
- example: for water layer:
- assign a density to each prism (currently using constant density within each layer)
prism_layer(... properties={'density':water_density)
- allow different cell sizes between layers
- if lower grid cell size doesn’t match upper grid’s, use
pygmt.grdtrack()
to sample the lower grids values at the grid’s prism locations
- if lower grid cell size doesn’t match upper grid’s, use
-
calculate forward gravity of each layer
harmonica.prism_layer.gravity()
for each prism layer
-
calculate gravity misfit
- add forward gravities for all layers, and subtract from observed gravity
- add forward gravities for all layers, and subtract from observed gravity
run geometry inversion
- designate an
active_layer
ex:active_layer='bathymetry_prisms'
- each iteration of the inversion will yield a
surface_correction
- set a max surface change per iteration
- applied to the
active_layer
tops, as well as the bottoms of the layer aboveactive_layer
- recalculate the forward gravity of these 2 updated layers.
- recalculte the misfit with these updated forward gravities before starting the 2nd iteration
- currently this inversion works by finding the least-squares solution to the matrix equation Ax=b, where A is the Jacobian matrix of the vertical derivative of gravity acceleration, and b is the initial misfit between observed and forward gravity.
- the least squares solution is found with
scipy.sparse.linalg.lsqr()
- the vertical derivative is approximated with the Hammer approximation of prisms, using annuluses as opposed to prisms.
- the least squares solution is found with
This is just the method I already had implemented, suggest from my advisor, but I’m happy to alter this and would appreciate suggestions.

Features to add:
-
enforce constraint point (points of known bathymetry) by multiplying
surface_correction
by a constraints grid * 0’s at constraints, linearly increasing to 1’s at a specified distance from nearest constraints *scipy.spatial.cKDTree.query()
to get grid of minimum distances to constraints *gmt grdmath
LE and GE to set constraints to 0 and other points to 1 *gmt grdblend
to merge the clipped grids * andrioxarray.interpolate_na()
to linearly interpolate between 0 and 1 -
allow irregular grid spacing within each layer (i.e. grid spacing increase for a buffer zone, outside the main area of interest)
- could use
discretize.TreeMesh().
from SimPEG.
- could use
-
allow depth-dependent density within each prism. Only for the forward modelling
- would be useful for firn/snow compaction into ice, and sedimentary basins.
- similar to https://github.com/fatiando/harmonica/pull/269
-
density inversion
- similar to the above geometry inversion, but updated each prism’s density to minimize the misfit.
- this is useful for the initial setup of the geometry inversion, allow the regional field to be accounted for in a layers density, leaving only the residual field left to invert on.
Discussion
- Do we want to refer to this type of inversion as a geometry inversion, structural inversion, boundary inversion or something else? There doesn’t seem to be much of a consensus on what you call this.
- I’m pretty new to inversion theory, so if anyone has recommendations on where to learn this material please let me know!
- I think some of the packages I have linked above could be replaced with Fatianado packages to keep dependencies minimal.
- All the inputs in my inversion are stored in a nested dictionary
layers
, with a dictionary for each layer, which includes a .nc filename, a resolution, constant density values, and a dataframe and dataset for each layer. This makes it easy to apply functions to an arbitrary number of input layers (example below). If there’s a better method for this let me know.
layers_list =[ 'ice', 'water', 'bathymetry', 'basement',]
spacing_list = [10e3, 10e3, 10e3, 10e3,]
rho_list = [ 920, 1030, 2600, 2800,]
fname_list=[
'../inversion_layers/BedMachine_surface_filled_inv.nc',
'../inversion_layers/BedMachine_icebase_filled_inv.nc',
../inversion_layers/BedMachine_bed_inv.nc',
'../inversion_layers/ROSETTA_basement_BedMachine_bed_inv.nc',
]
# create nested dictionary of layer properties
layers = {j:{'spacing':spacing_list[i],
'fname':fname_list[i],
'rho':rho_list[i]} for i, j in enumerate(layers_list)}
for i, j in enumerate(layers_list):
# load each .nc as a xarray dataset
layers[j]['grid']=xr.load_dataset(layers[j]['fname'])
# create prism layer between each set of input grids
layers[j]['prisms']=hm.prism_layer(
coordinates=(list(layers[j]['grid'].x), list(layers[j]['grid'].y)),
surface=layers[j]['grid'].z,
reference=layers[reversed_layers_list[i-1]]['grid'].z,
properties={'density':layers[j]['grid'].density})
# calculate forward gravity for each prism layer
for k, v in layers.items():
df_grav[f'{k}_forward_grav'] = v['prisms'].prism_layer.gravity(
coordinates = (df_grav.x, df_grav.y, df_grav.z),
field = 'g_z', progressbar=True)
- to remove edge effects, I’m using a buffer, currently 200km in each direction, as shown in the below figures by the black square. You can see the edge effects in the second to last panel, which is the lowest layer of prisms. I am doing all calculations on the full region, but only showing results within the buffer zone. Does this seem like a reasonable approach? It adds a lot of prisms, but this could be mitigated by using
discretize.TreeMesh().
for the buffer region. I’m unsure if I should be calculating misfit for the whole region or just the inside. Also, I’m not sure if I should invert the prisms just within the inside, or for the whole region.
Are you willing to help implement and maintain this feature?
Yes I’m happy and excited to implement this, but I’m in the last year of my Ph.D. so the outcome of this inversion is more pressing than the implementation of the inversion into Harmonica. Hopefully they can happen simultaneously. @santisoler has suggested looking at both the old Fatiando module fatiando.inversion
and some code related to Uieda and Barbosa 2016 for inspiration on how to implement this, which I will do.
Issue Analytics
- State:
- Created a year ago
- Comments:9 (6 by maintainers)
Matt you might find this useful for general inversion theory - from UBC. This used to be a standalone website, but it looks like you now have to download. Follow the instructions on the page. https://gif.eos.ubc.ca/IAG
Hi @mdtanker. I’m glad you are getting into the rabbit hole of inversion, it’s a fun one haha!
The good news here is that you are already using some simple kind of smallness regularization. The
verde.base.least_squares
function relies on thesklearn.linear_model.Ridge
class that minimizes the objective function:$$ \phi(\mathbf{m}) = \lVert \mathbf{d}_\text{obs} - \mathbf{G} \mathbf{m} \rVert^2 + \alpha \lVert \mathbf{m} \rVert^2 $$
The second term that involves the
alpha
(damping parameter) and the l2 norm of the parameters is a Tikhonov zeroth order regularization. SimPEG implements the smallness by defining this regularization term as the l2 norm of the difference between the model parameters and the reference model. In this reference model you can include you constrains:$$ \phi(\mathbf{m}) = \lVert \mathbf{d}\text{obs} - \mathbf{G} \mathbf{m} \rVert^2 + \alpha \lVert \mathbf{m} - \mathbf{m}\text{ref} \rVert^2 $$
Here’s a nice chapter to get into these topics: https://pubs.geoscienceworld.org/books/book/2111/chapter-abstract/114879875/Inversion-for-Applied-Geophysics-A-Tutorial?redirectedFrom=fulltext
The problem is that you won’t be able to use the
Ridge
class for defining a reference model and you’ll probably have to write you own implementation of the regularization.There are also some lecture notes from @leouieda and @birocoles. They have really nice stuff there, particularly if you need to implement this stuff: https://www.semanticscholar.org/paper/Tópicos-de-inversão-em-geofísica-OliveiraVanderlei-Leonardo/fd651157443f37ad603057a10d683322ff123263