Python: PyVista/GeoVista - 3D - different orographies on a sphere#
Description
In this example, we show the overlaying of two orographies of different resolutions on the sphere. To display the orographies on a 3D sphere, we use the Python packages PyVista and GeoVista.
The global dataset is the MPI-ESM-LR orography with lower resolution on a rectilinear grid. The regional dataset is the CORDEX EUR-11 orography with higher resolution on a rotated curvilinear grid.
Content
Example data
Plotting - create mesh from 1d-coordinates - create mesh from 2d-coordinates - plot the meshes - add coastlines
Software requirements
Python 3
numpy
xarray
pyvista
geovista
Example script#
overlay_different_orography_on_sphere.py
#!/usr/bin/env python
# coding: utf-8
#
#------------------------------------------------------------------------------
#-- DKRZ example: 3D - different orographies on a sphere
#--
#-- In this example, we show the overlaying of two orographies of different
#-- resolutions on the sphere. To display the orographies on a 3D sphere, we
#-- use the Python packages PyVista and GeoVista.
#--
#-- The global dataset is the MPI-ESM-LR orography with lower resolution on a
#-- rectilinear grid. The regional dataset is the CORDEX EUR-11 orography with
#-- higher resolution on a rotated curvilinear grid.
#--
#------------------------------------------------------------------------------
#--
#-- 2026 copyright DKRZ licensed under CC BY-NC-SA 4.0
#-- (https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
#--
#------------------------------------------------------------------------------
import os
import pyvista as pv
from pyvista import examples
import geovista as gv
import numpy as np
import xarray as xr
import cartopy.util as cutil
#------------------------------------------------------------------------------
#-- Preliminary work
#------------------------------------------------------------------------------
#-- set title string
title = 'Overlay: regional on global grid (orography)'
#-- choose colormap
cmap = 'terrain'
#-- set data value range [m]
var_min = -800
var_max = 3000
#-- retrieve the data of the coastlines
coastlines = examples.download_coastlines()
#------------------------------------------------------------------------------
#-- Plot global MPI-ESM-LR orography on a sphere
#------------------------------------------------------------------------------
#-- First, we read the data for the Orography variable and the land-sea mask.
#-- global orography
orog_file1 = os.environ['HOME'] + '/data/CMIP5/atmos/orog_fx_MPI-ESM-LR_rcp26_r0i0p0.nc'
#-- open orography file
ds1_global = xr.open_dataset(orog_file1)
var1 = ds1_global.orog
#-- global land area fraction
laf_file1 = os.environ['HOME'] + '/data/CMIP5/atmos/sftlf_fx_MPI-ESM-LR_rcp26_r0i0p0.nc'
#-- open land-sea-mask file
ds1_mask = xr.open_dataset(laf_file1)
lsm1 = ds1_mask.sftlf.where(ds1_mask.sftlf > 0.5, ds1_mask.sftlf, 0)
land_only1 = np.where(lsm1 > 0.5, var1, -101) #-- min contour value - 1
lat = ds1_global.lat
lon = ds1_global.lon
dlat = lat[1]-lat[0]
dlon = lon[1]-lon[0]
#-- To prevent a gap at Greenwich Meridian, we add a longitude cyclic point for
#-- Greenwich Meridian.
cyclic_data, cyclic_lon = cutil.add_cyclic_point(land_only1, coord=lon)
#------------------------------------------------------------------------------
#-- Create the mesh of the global data.
#------------------------------------------------------------------------------
#-- use the longitudes and latitudes of the input data to generate the mesh
#-- coordinates
xx, yy, zz = np.meshgrid(np.deg2rad(cyclic_lon), np.deg2rad(lat), [0])
#-- transform the input coordinates to spherical coordinates
radius = 1.
x = radius * np.cos(yy) * np.cos(xx)
y = radius * np.cos(yy) * np.sin(xx)
z = radius * np.sin(yy)
#-- Crearte a structured 3d-grid from the longitudes, latitudes, and one
#-- vertical level and add the example data.
#-- create a structured grid object
grid = pv.StructuredGrid(x, y, z)
#-- add data to the structured grid object
grid['orog1'] = cyclic_data.data.ravel(order='F')
#------------------------------------------------------------------------------
#-- Create the plot on the sphere.
#------------------------------------------------------------------------------
#-- create the plotter
plotter = gv.GeoPlotter(window_size=(800,800))
#-- add the data to the plotter
actor = plotter.add_mesh(grid,
cmap=cmap,
interpolate_before_map=False,
lighting=True,
categories=True,
show_scalar_bar=False)
actor.mapper.scalar_range = var_min, var_max
#-- add color bar
sbar_args = dict(interactive=False,
vertical=False,
n_labels=5,
title_font_size=16,
label_font_size=10,
outline=False,
width=0.7,
height=0.07,
position_x= 0.175,
position_y= 0.07,
fmt='{0:5.0f}')
sbar = plotter.add_scalar_bar('Orography [m]', **sbar_args)
#-- add more space between scalar bar title and the color labels
sbar.GetTitleTextProperty().SetLineOffset(-10.)
#-- add coastlines to the plotter
actor = plotter.add_mesh(coastlines, color='black', line_width=0.5)
#-- display the result
plotter.show()
#------------------------------------------------------------------------------
#-- Plot regional CORDEX EUR-11 orography on a sphere
#------------------------------------------------------------------------------
#-- In the following, we will show how the rotated curvilinear data is handled
#-- in PyVista in order to display it on the sphere.
#-- regional orography
orog_file2 = os.environ['HOME'] + '/data/CORDEX/EUR-11/orog_EUR-11_MPI-M-MPI-ESM-LR_historical_r0i0p0_CLMcom-CCLM4-8-17_v1_fx.nc'
#-- open regional file
ds2 = xr.open_dataset(orog_file2)
var2 = ds2.orog
#-- land-sea-mask data
laf_file2 = os.environ['HOME'] + '/data/CORDEX/EUR-11/sftlf_EUR-11_MPI-M-MPI-ESM-LR_historical_r0i0p0_CLMcom-CCLM4-8-17_v1_fx.nc'
#-- open land-sea-mask data file
ds2_mask = xr.open_dataset(laf_file2)
lsm2 = ds2_mask.sftlf
#-- mask ocean
lsm2 = np.where(lsm2 > 0.5, lsm2, 0)
land_only2 = np.where(lsm2 > 0.5, var2, -101) #-- min contour value - 1
land_only2
#------------------------------------------------------------------------------
#-- To create the mesh, we have to proceed slightly differently here, as the
#-- data is located on a rotated curvilinear grid.
#------------------------------------------------------------------------------
mesh = gv.Transform.from_2d(var2.lon,
var2.lat,
data=land_only2,
name=f"{var2.standard_name} / {var2.units}")
#-- remove cells from the mesh with missing values
mesh = mesh.threshold()
#------------------------------------------------------------------------------
#-- Create the plot on the sphere.
#------------------------------------------------------------------------------
plotter = gv.GeoPlotter()
sphere = pv.Sphere(radius=1.0, theta_resolution=100, phi_resolution=100)
plotter.add_mesh(sphere, color='gainsboro')
actor = plotter.add_mesh(mesh, cmap='terrain')
actor.mapper.scalar_range = var_min, var_max
plotter.add_coastlines()
plotter.camera.zoom(1.3)
cpos = plotter.camera_position
plotter.camera_position = 'yz'
plotter.camera.azimuth = 10
plotter.camera.elevation = 45 #-- move camera upward
plotter.camera.roll -= 5 #-- 'rotate' the sphere
plotter.show()
#------------------------------------------------------------------------------
#-- Combine both meshes in one plot
#------------------------------------------------------------------------------
#-- Now we have everything we need to draw both meshes on the sphere in the same
#-- plot. We also add a title string, the cell edges of the global grid, a
#-- camera, and some lighting to create a nice plot.
#----------------------------------------------------------------
#-- create the plotter
#----------------------------------------------------------------
plotter = gv.GeoPlotter(window_size=(700,700))
plotter.set_background('gainsboro')
#----------------------------------------------------------------
#-- add a title string
#----------------------------------------------------------------
plotter.add_text(title,
font='courier',
viewport=True,
position=(0.18, 0.8),
font_size=8)
#----------------------------------------------------------------
#-- add MPI-ESM-LR mesh
#----------------------------------------------------------------
actor = plotter.add_mesh(grid,
cmap=cmap,
lighting=True,
show_scalar_bar=False)
actor.mapper.scalar_range = var_min, var_max
#----------------------------------------------------------------
#-- add color bar
#----------------------------------------------------------------
sbar_args = dict(interactive=False,
vertical=False,
n_labels=5,
title_font_size=16,
label_font_size=10,
outline=False,
width=0.7,
height=0.07,
position_x= 0.16,
position_y= 0.07,
fmt='{0:5.0f}')
sbar = plotter.add_scalar_bar('Orography [m]', **sbar_args)
#-- add more space between scalar bar title and the color labels
sbar.GetTitleTextProperty().SetLineOffset(-10.)
#----------------------------------------------------------------
#-- add CORDEX EUR-11 mesh
#-- - move the regional mesh up higher to make it more visible
#-- by creating shadows
#-- - change the attributes so that the applied mesh contrasts
#-- better from the one underneath
#----------------------------------------------------------------
actor = plotter.add_mesh(mesh,
cmap=cmap,
show_scalar_bar=False,
split_sharp_edges=True,
pbr=True,
metallic=1.0,
roughness=0.5,
zlevel=1000)
actor.mapper.scalar_range = var_min, var_max
plotter.enable_shadows()
#----------------------------------------------------------------
#-- add coastlines to the plotter
#----------------------------------------------------------------
plotter.add_coastlines()
#----------------------------------------------------------------
#-- set camera
#----------------------------------------------------------------
plotter.camera.zoom(1.3)
plotter.camera_position = 'yz'
plotter.camera.azimuth = 0
plotter.camera.elevation = 45 #-- move camera upward
plotter.camera.roll -= 5 #-- 'rotate' the sphere
#----------------------------------------------------------------
#-- light settings
#----------------------------------------------------------------
light = pv.Light()
light.set_direction_angle(0, 0) #-- +x-direction
light.intensity = 0.25 #-- set light intensity to x%
plotter.add_light(light)
#----------------------------------------------------------------
#-- display the result and save output to PNG
#----------------------------------------------------------------
plotter.show(screenshot='plot_pyvista_grid_resolutions_overlay.png')
