Download this Jupyter notebook from github


Convert Geopandas GeoSeries to spherical harmonics#

Author: R. Rietbroek Jan 2025 (r.rietbroek@utwente.nl)*

Often, geographical geometry objects are needed as masks (1 inside,0 outside) expressed in the spherical harmonic domain. This notebook shows how a geopandas Geoseries, containing polygon masks or points can be converted to spherical harmonic coefficients

1. Preparations#

[1]:
#Optionally enable autoreloading for development purposes. Note that this does not automagically reload the binary extensions
%load_ext autoreload
%autoreload 2
[2]:
import xarray as xr
import os
import shxarray
import matplotlib.pyplot as plt
import cartopy.crs as ccrs

import numpy as np
import geopandas as gpd
from shapely import Point
[16]:
#set the maximum desired degree of the output spherical harmonics
nmax=120
/home/roelof/cld_UTwente/Soft/shxarray-git/src/shxarray/core/xr_accessor.py:18: AccessorRegistrationWarning: registration of accessor <class 'shxarray.core.xr_accessor.SHDaAccessor'> under name 'sh' for type <class 'xarray.core.dataarray.DataArray'> is overriding a preexisting attribute with the same name.
  @xr.register_dataarray_accessor("sh")
/home/roelof/cld_UTwente/Soft/shxarray-git/src/shxarray/core/xr_accessor.py:280: AccessorRegistrationWarning: registration of accessor <class 'shxarray.core.xr_accessor.SHDsAccessor'> under name 'sh' for type <class 'xarray.core.dataset.Dataset'> is overriding a preexisting attribute with the same name.
  def synthesis(self,lon=None, lat=None,engine="shlib",**kwargs):

2. Get a set of Antartic grounded drainage basins (as polygons) to use as continental loads#

Note that in this case we load the geometries from a geoslurp database, but the example can be easily modified to load shapes from e.g. geopackage files.

[4]:
# In this example we're loading the drainage basin outlines froma geoslurp database
# The original datasources can be found at https://earth.gsfc.nasa.gov/cryo/data/polar-altimetry/antarctic-and-greenland-drainage-systems
from geoslurp.manager import GeoslurpManager
import geoslurp.tools.pandas
import pandas as pd
gsman=GeoslurpManager(dbalias="tunnelmarge")
tname="cryo.antarc_ddiv_icesat_grnd"

# One can populate the database like this:
#dsant=gsman.dataset(tname)
#dsant.pull()
#dsant.register()

#load the polygons from the databse in a geopandas dataframe NOTE: convert to polar stereographic projection because the polgyon contains the south Pole
#And this projection is needed when converting to spherical harmonics
dfant=pd.DataFrame.gslrp.load(gsman.conn,f"SELECT basinid,ST_transform(geom::geometry,3031) as geometry FROM {tname}")
# quick plot of the drainage basin outlines
ax=dfant.plot(edgecolor="tab:blue",facecolor="grey")
ax.set_title("Antarctic grounded drainage basins")
[4]:
Text(0.5, 1.0, 'Antarctic grounded drainage basins')
../_images/notebooks_Geometry2sphericalHarmonics_6_1.png

3 Convert the drainage outline to spherical harmonics of the desired degrees#

We take the column basinid as an axiliary coordinate to indicate the original geometries present

[11]:
dantsh=xr.DataArray.sh.from_geoseries(dfant.geometry,nmax,auxcoord=dfant.basinid,engine="shtns")
dantsh.name='ant_div'
display(dantsh)

#optionally save to disk
fout=f"data/antarc_ddiv_icesat_grnd_n{nmax}.nc"
# dantsh.reset_index('nm').to_netcdf(fout)
[SHTns 3.7.3] built Jan 17 2025, 23:35:53, id: v3.7-9-gbfb51b8*,avx2,ishioka,openmp,cuda
shxarray-INFO: Masking and gridding polygons
shxarray-INFO: Applying SH analysis
<xarray.DataArray 'ant_div' (basinid: 26, nm: 14641)> Size: 3MB
array([[ 9.40267020e-04, -3.05624425e-04, -1.59546257e-03, ...,
         0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
       [ 1.54626069e-03, -4.34739860e-05, -2.67059892e-03, ...,
         0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
       [ 3.05559875e-03,  1.44490204e-04, -5.21075910e-03, ...,
         0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
       ...,
       [ 2.07448024e-04, -1.00811097e-04, -3.42265171e-04, ...,
         0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
       [ 5.83668038e-05, -3.63363264e-05, -9.28618692e-05, ...,
         0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
       [ 7.85327017e-05, -4.81902117e-05, -1.24477510e-04, ...,
         0.00000000e+00,  0.00000000e+00,  0.00000000e+00]],
      shape=(26, 14641))
Coordinates:
  * nm       (nm) object 117kB MultiIndex
  * n        (nm) int64 117kB 0 1 1 1 2 2 2 2 ... 120 120 120 120 120 120 120
  * m        (nm) int64 117kB 0 -1 0 1 -2 -1 0 1 ... 114 115 116 117 118 119 120
  * basinid  (basinid) float64 208B 2.0 3.0 4.0 5.0 6.0 ... 24.0 25.0 26.0 27.0
Attributes:
    Conventions:  CF-1.8
    source:       shxarray-1.2.1.dev17+ged5d445.d20250123 <https://github.com...
    contact:      Roelof Rietbroek <r.rietbroek@utwente.nl>
    institution:  ITC Faculty of Geo-Information Science and Earth Observatio...
    comments:     Used backend: SHTns High performance Spherical Harmonic Tra...
    history:      Analysis operation

4 Check if the intermediate drainage basin results made sense#

A spatially discontinuous function cannnot be one-to-one mapped to spectrally truncated function, so some Gibb’s phenomena is to be expected

[12]:
# Sum all spherical harmonic coefficient sets for all basins and grid
import cartopy.crs as ccrs
dres=0.25
basinid=18
# basinid=3
daantgrd=dantsh.sel(basinid=basinid).sh.synthesis(lon=np.arange(-180+dres/2,180,dres),lat=np.arange(-90+dres/2,-60,dres))
daantgrd.name="Mask value"
# daantgrd=daantgrd[daantgrd.lat < -50]
ax = plt.axes(projection=ccrs.SouthPolarStereo())
daantgrd.plot(ax=ax,vmax=1,vmin=0,transform=ccrs.PlateCarree())
ax.add_geometries(dfant[dfant.basinid==basinid].geometry,edgecolor='tab:red',crs=ccrs.SouthPolarStereo(),facecolor="none",lw=2)
plt.title(f"Antarctic drainage basin {basinid} from sh nmax={nmax}")
[12]:
Text(0.5, 1.0, 'Antarctic drainage basin 18 from sh nmax=120')
../_images/notebooks_Geometry2sphericalHarmonics_10_1.png

5 What about the conversion of geometry points?#

Points can be interpreted as unit loads, disks or parabolic loads, before being expanded into spherical harmonic coefficients. Let’s create some randomized points on the sphere first

[13]:
from random import sample
npoints=100
latrange=180
lonrange=360
discr=0.001245
latpnt=sample(sorted(np.arange(-90,90,discr)),npoints)
lonpnt=sample(sorted(np.arange(-180,180,discr)),npoints)
gspnts=gpd.GeoSeries([Point(x,y) for x,y in zip(lonpnt,latpnt)]).set_crs(ccrs.PlateCarree())
[14]:
#convert the points to spherical harmonics of disks with spherical radius of 4 degrees
dadskssh=xr.DataArray.sh.from_geoseries(gspnts,nmax,axialtype="disk",psi=4)

#unitload
#daunitsh=xr.DataArray.sh.from_geoseries(gspnts,nmax)

#Parabolic caps
# daparacsh=xr.DataArray.sh.from_geoseries(gspnts,nmax,axialtype="paraboliccap",psi=4)

display(dadskssh)
<xarray.DataArray 'shdisk' (nlonlat: 100, nm: 14641)> Size: 12MB
array([[-7.98018139e-046,  4.68545312e-045,  6.49876794e-044, ...,
        -4.03003727e-045, -5.58969993e-044,  2.70487821e-045],
       [ 1.35493776e-059,  1.09694956e-060,  1.60178764e-059, ...,
         6.97549517e-059,  1.01857573e-057, -2.01777478e-059],
       [ 1.79981953e-078,  6.08291180e-078, -9.19234058e-077, ...,
         4.83598077e-078, -7.30801034e-077, -5.29770319e-079],
       ...,
       [-1.79150622e-116,  3.16292459e-116, -4.89048466e-115, ...,
        -2.72807484e-115,  4.21812401e-114,  2.66646475e-116],
       [-3.53891728e-067,  6.88935952e-066,  1.02217376e-064, ...,
         1.15351689e-066,  1.71147216e-065,  2.09566999e-066],
       [-9.72981424e-034, -5.81789702e-034, -7.46284052e-033, ...,
        -1.60706017e-033, -2.06143795e-032,  1.97184914e-035]],
      shape=(100, 14641))
Coordinates:
  * nm       (nm) object 117kB MultiIndex
  * n        (nm) int32 59kB 120 119 120 118 119 120 ... 118 119 120 119 120 120
  * m        (nm) int32 59kB -120 -119 -119 -118 -118 ... 118 118 119 119 120
    lon      (nlonlat) float64 800B -147.1 145.2 54.89 ... 152.7 -90.08 71.26
    lat      (nlonlat) float64 800B 62.99 69.71 -76.1 ... -83.32 72.38 55.48
    id       (nlonlat) int64 800B 0 1 2 3 4 5 6 7 8 ... 92 93 94 95 96 97 98 99
Dimensions without coordinates: nlonlat

6 Create a visualization of all the disk loads together#

[15]:

dadsks=dadskssh.sum("nlonlat").sh.synthesis() proj=ccrs.NearsidePerspective(central_longitude=0.0, central_latitude=50, satellite_height=35785831) fig, ax = plt.subplots(subplot_kw={'projection': proj},figsize=(12,8.5)) ax.coastlines() dadsks.plot(ax=ax,transform=ccrs.PlateCarree()) gspnts.plot(ax=ax,transform=ccrs.PlateCarree()) ax.set_title("100 randomly positioned spherical harmonic disk loads") plt.show()
../_images/notebooks_Geometry2sphericalHarmonics_15_0.png