Skip to content

Commit

Permalink
Merge pull request #703 from cbegeman/enhance-drying-slope-with-conve…
Browse files Browse the repository at this point in the history
…rgence

Add drying_slope convergence tests:

This PR adds a convergence test case for both wetting-and-drying methods, `standard` and `ramp`. The RMSE is computed at all locations and times for which an analytic solution is available (the analytic solution was provided prior to this PR). It produces a convergence plot and compares both methods when both have been run. The analytic solution is only comparable to the `default` cases, which use Rayleigh damping.
  • Loading branch information
cbegeman authored Oct 25, 2023
2 parents ab68ad7 + 8e2af4a commit 50395b4
Show file tree
Hide file tree
Showing 20 changed files with 591 additions and 374 deletions.
28 changes: 18 additions & 10 deletions compass/ocean/suites/wetdry.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
ocean/drying_slope/250m/sigma/default
ocean/drying_slope/250m/sigma/ramp
ocean/drying_slope/250m/single_layer/default
ocean/drying_slope/250m/single_layer/ramp
ocean/drying_slope/1km/sigma/default
ocean/drying_slope/1km/sigma/ramp
ocean/drying_slope/1km/sigma/decomp
ocean/drying_slope/1km/single_layer/default
ocean/drying_slope/1km/single_layer/ramp
ocean/drying_slope/1km/single_layer/decomp
ocean/drying_slope/sigma/standard/250m/default
ocean/drying_slope/sigma/standard/1km/default
ocean/drying_slope/sigma/standard/250m/decomp
ocean/drying_slope/sigma/standard/1km/decomp
ocean/drying_slope/single_layer/standard/250m/default
ocean/drying_slope/single_layer/standard/1km/default
ocean/drying_slope/sigma/ramp/250m/default
ocean/drying_slope/sigma/ramp/1km/default
ocean/drying_slope/sigma/ramp/250m/decomp
ocean/drying_slope/sigma/ramp/1km/decomp
ocean/drying_slope/single_layer/ramp/250m/default
ocean/drying_slope/single_layer/ramp/1km/default
ocean/drying_slope/sigma/standard/250m/loglaw
ocean/drying_slope/sigma/standard/1km/loglaw
ocean/drying_slope/single_layer/standard/250m/loglaw
ocean/drying_slope/single_layer/standard/1km/loglaw
ocean/drying_slope/sigma/standard/convergence
ocean/drying_slope/sigma/ramp/convergence
ocean/dam_break/40cm/default
ocean/dam_break/40cm/ramp
ocean/dam_break/120cm/default
Expand Down
27 changes: 16 additions & 11 deletions compass/ocean/tests/drying_slope/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from compass.ocean.tests.drying_slope.convergence import Convergence
from compass.ocean.tests.drying_slope.decomp import Decomp
from compass.ocean.tests.drying_slope.default import Default
from compass.ocean.tests.drying_slope.loglaw import LogLaw
from compass.ocean.tests.drying_slope.ramp import Ramp
from compass.testgroup import TestGroup


Expand All @@ -17,17 +17,22 @@ def __init__(self, mpas_core):
"""
super().__init__(mpas_core=mpas_core, name='drying_slope')

for resolution in [0.25, 1.]:
for method in ['standard', 'ramp']:
for coord_type in ['sigma', 'single_layer']:
for resolution in [0.25, 1.]:
self.add_test_case(
Default(test_group=self, resolution=resolution,
coord_type=coord_type, method=method))
self.add_test_case(
Decomp(test_group=self, resolution=resolution,
coord_type=coord_type, method=method))
for coord_type in ['sigma']:
self.add_test_case(
Default(test_group=self, resolution=resolution,
coord_type=coord_type))
self.add_test_case(
Decomp(test_group=self, resolution=resolution,
coord_type=coord_type))
self.add_test_case(
Ramp(test_group=self, resolution=resolution,
coord_type=coord_type))
Convergence(test_group=self,
coord_type=coord_type,
method=method))
for coord_type in ['sigma', 'single_layer']:
for resolution in [0.25, 1.]:
self.add_test_case(
LogLaw(test_group=self, resolution=resolution,
coord_type=coord_type))
coord_type=coord_type, method='standard'))
168 changes: 168 additions & 0 deletions compass/ocean/tests/drying_slope/analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import xarray as xr
from scipy.interpolate import interp1d

from compass.step import Step


class Analysis(Step):
"""
A step for analyzing the convergence of drying slope results and producing
a convergence plot.
Attributes
----------
damping_coeff : float
The Rayleigh damping coefficient used for the forward runs
resolutions : float
The resolution of the test case
times : list of float
The times at which to compare to the analytical solution
"""
def __init__(self, test_case, resolutions, damping_coeff):
"""
Create a forward step
Parameters
----------
test_case : compass.TestCase
The test case this step belongs to
resolutions : list of floats
The resolution of the test case
damping_coeff: float
the value of the rayleigh damping coefficient
"""

super().__init__(test_case=test_case, name='analysis')

self.damping_coeff = damping_coeff
self.resolutions = resolutions
self.times = ['0.05', '0.15', '0.25', '0.30', '0.40', '0.50']

for resolution in resolutions:
if resolution < 1.:
res_name = f'{int(resolution*1e3)}m'
else:
res_name = f'{int(resolution)}km'
self.add_input_file(filename=f'output_{res_name}.nc',
target=f'../forward_{res_name}/output.nc')
for time in self.times:
filename = f'r{damping_coeff}d{time}-analytical'\
'.csv'
self.add_input_file(filename=filename, target=filename,
database='drying_slope')

self.add_output_file(filename='convergence.png')

def run(self):
"""
Run this step of the test case
"""
self._plot_convergence()

def _compute_rmse(self, ds, t):
"""
Get the time step
Parameters
----------
ds : xarray.Dataset
the MPAS dataset containing output from a forward step
t : float
the time to evaluate the RMSE at
Returns
-------
rmse : float
the root-mean-squared-error of the MPAS output relative to the
analytical solution at time t
"""

x_exact, ssh_exact = self._exact_solution(t)
tidx = int((float(t) / 0.2 + 1e-16) * 24.0)
ds = ds.drop_vars(np.setdiff1d([j for j in ds.variables],
['yCell', 'ssh']))
ds = ds.isel(Time=tidx)
x_mpas = ds.yCell.values / 1000.0
ssh_mpas = ds.ssh.values
# Interpolate mpas solution to the points at which we have an exact
# solution
idx_min = np.argwhere(x_exact - x_mpas[0] >= 0.).item(0)
idx_max = np.argwhere(x_exact - x_mpas[-1] <= 0.).item(-1)
f = interp1d(x_mpas, ssh_mpas)
ssh_mpas_interp = f(x_exact[idx_min:idx_max])
rmse = np.sqrt(np.mean(np.square(ssh_mpas_interp -
ssh_exact[idx_min:idx_max])))
return rmse

def _plot_convergence(self):
"""
Plot convergence curves
"""
comparisons = []
cases = {'standard': '../../../standard/convergence/analysis',
'ramp': '../../../ramp/convergence/analysis'}
for case in cases:
include = True
for resolution in self.resolutions:
if resolution < 1.:
res_name = f'{int(resolution*1e3)}m'
else:
res_name = f'{int(resolution)}km'
if not os.path.exists(f'{cases[case]}/output_{res_name}.nc'):
include = False
if include:
comparisons.append(case)

fig, ax = plt.subplots(nrows=1, ncols=1)

max_rmse = 0
for k, comp in enumerate(comparisons):
rmse = np.zeros((len(self.resolutions), len(self.times)))
for i, resolution in enumerate(self.resolutions):
if resolution < 1.:
res_name = f'{int(resolution*1e3)}m'
else:
res_name = f'{int(resolution)}km'
ds = xr.open_dataset(
f'{cases[comp]}/output_{res_name}.nc')
for j, time in enumerate(self.times):
rmse[i, j] = self._compute_rmse(ds, time)
rmse_tav = np.mean(rmse, axis=1)
if np.max(rmse_tav) > max_rmse:
max_rmse = np.max(rmse_tav)
ax.loglog(self.resolutions, rmse_tav,
linestyle='-', marker='o', label=comp)

rmse_1st_order = np.zeros(len(self.resolutions))
rmse_1st_order[0] = max_rmse
for i in range(len(self.resolutions) - 1):
rmse_1st_order[i + 1] = rmse_1st_order[i] / 2.0

ax.loglog(self.resolutions, np.flip(rmse_1st_order),
linestyle='-', color='k', alpha=.25, label='1st order')

ax.set_xlabel('Cell size (km)')
ax.set_ylabel('RMSE (m)')

ax.legend(loc='lower right')
ax.set_title('SSH convergence')
fig.tight_layout()
fig.savefig('convergence.png')

def _exact_solution(self, time):
"""
Returns distance, ssh of the analytic solution
"""
datafile = f'./r{self.damping_coeff}d{time}-'\
f'analytical.csv'
data = pd.read_csv(datafile, header=None)
return data[0], -data[1]
126 changes: 126 additions & 0 deletions compass/ocean/tests/drying_slope/convergence/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from numpy import ceil

from compass.config import CompassConfigParser
from compass.ocean.tests.drying_slope.analysis import Analysis
from compass.ocean.tests.drying_slope.forward import Forward
from compass.ocean.tests.drying_slope.initial_state import InitialState
from compass.ocean.tests.drying_slope.viz import Viz
from compass.testcase import TestCase
from compass.validate import compare_variables


class Convergence(TestCase):
"""
The convergence drying_slope test case
Attributes
----------
resolution : float
The resolution of the test case in km
coord_type : str
The type of vertical coordinate (``sigma``, ``single_layer``, etc.)
damping_coeffs: list of float
The damping coefficients at which to evaluate convergence. Must be of
length 1.
"""

def __init__(self, test_group, coord_type, method):
"""
Create the test case
Parameters
----------
test_group : compass.ocean.tests.drying_slope.DryingSlope
The test group that this test case belongs to
coord_type : str
The type of vertical coordinate (``sigma``, ``single_layer``)
method: str
The wetting-and-drying method (``standard``, ``ramp``)
"""
name = 'convergence'

self.coord_type = coord_type
damping_coeffs = [0.01]
self.damping_coeffs = damping_coeffs
subdir = f'{coord_type}/{method}/{name}'
super().__init__(test_group=test_group, name=name,
subdir=subdir)
self.resolutions = None
# add the steps with default resolutions so they can be listed
config = CompassConfigParser()
config.add_from_package('compass.ocean.tests.drying_slope',
'drying_slope.cfg')
self._setup_steps(config, subdir=subdir, method=method)

def _setup_steps(self, config, subdir, method):
""" setup steps given resolutions """

default_resolutions = '0.25, 0.5, 1, 2'

# set the default values that a user may change before setup
config.set('drying_slope_convergence', 'resolutions',
default_resolutions,
comment='a list of resolutions (km) to test')

# get the resolutions back, perhaps with values set in the user's
# config file
resolutions = config.getlist('drying_slope_convergence',
'resolutions', dtype=float)

if self.resolutions is not None and self.resolutions == resolutions:
return

# start fresh with no steps
self.steps = dict()
self.steps_to_run = list()

self.resolutions = resolutions
section = config['drying_slope']
ntasks_baseline = section.getint('ntasks_baseline')
min_tasks = section.getint('min_tasks')

for resolution in self.resolutions:

if resolution < 1.:
res_name = f'{int(resolution*1e3)}m'
else:
res_name = f'{int(resolution)}km'
init_name = f'initial_state_{res_name}'
self.add_step(InitialState(test_case=self,
name=init_name,
resolution=resolution,
coord_type=self.coord_type))
ntasks = max(min_tasks,
int(ceil(ntasks_baseline / resolution**2.)))
forward_step = Forward(test_case=self, resolution=resolution,
name=f'forward_{res_name}',
input_path=f'../{init_name}',
ntasks=ntasks, min_tasks=min_tasks,
openmp_threads=1,
damping_coeff=self.damping_coeffs[0],
coord_type=self.coord_type)
if method == 'ramp':
forward_step.add_namelist_options(
{'config_zero_drying_velocity_ramp': ".true."})
self.add_step(forward_step)
self.add_step(Analysis(test_case=self,
resolutions=resolutions,
damping_coeff=self.damping_coeffs[0]))

def validate(self):
"""
Validate variables against a baseline
"""
super().validate()
variables = ['layerThickness', 'normalVelocity']
for resolution in self.resolutions:
if resolution < 1.:
res_name = f'{int(resolution*1e3)}m'
else:
res_name = f'{int(resolution)}km'
compare_variables(test_case=self, variables=variables,
filename1=f'forward_{res_name}/output.nc')
Loading

0 comments on commit 50395b4

Please sign in to comment.