From f346de213c85eedd48f65877f8923ae0d9f92d9a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 5 Dec 2024 07:57:41 -0600 Subject: [PATCH 01/11] Improve error message when refinement factor not found --- polaris/ocean/convergence/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/polaris/ocean/convergence/__init__.py b/polaris/ocean/convergence/__init__.py index 0a04e50e7..b976200c7 100644 --- a/polaris/ocean/convergence/__init__.py +++ b/polaris/ocean/convergence/__init__.py @@ -1,6 +1,3 @@ -import numpy as np - - def get_resolution_for_task(config, refinement_factor, refinement='both'): """ @@ -32,7 +29,8 @@ def get_resolution_for_task(config, refinement_factor, if refinement_factor not in refinement_factors: raise ValueError( - 'refinement_factor not found in config option refinement_factors') + f'refinement_factor {refinement_factor} not found in config ' + f'option {option}:\n {refinement_factors}') if refinement == 'time': resolution = base_resolution @@ -74,7 +72,8 @@ def get_timestep_for_task(config, refinement_factor, if refinement_factor not in refinement_factors: raise ValueError( - 'refinement_factor not found in config option refinement_factors') + f'refinement_factor {refinement_factor} not found in config ' + f'option {option}:\n {refinement_factors}') resolution = get_resolution_for_task( config, refinement_factor, refinement=refinement) From 037ec8c26290dbba8db1491fe181887344f7d2a6 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 5 Dec 2024 06:51:48 -0600 Subject: [PATCH 02/11] A bit of cleanup in cosine bell forward --- polaris/ocean/tasks/cosine_bell/forward.py | 4 ++-- polaris/ocean/tasks/cosine_bell/forward.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/polaris/ocean/tasks/cosine_bell/forward.py b/polaris/ocean/tasks/cosine_bell/forward.py index 270f4048f..4bdf26479 100644 --- a/polaris/ocean/tasks/cosine_bell/forward.py +++ b/polaris/ocean/tasks/cosine_bell/forward.py @@ -8,7 +8,7 @@ class Forward(SphericalConvergenceForward): """ def __init__(self, component, name, subdir, mesh, init, - refinement_factor, refinement='both'): + refinement_factor, refinement): """ Create a new step @@ -32,7 +32,7 @@ def __init__(self, component, name, subdir, mesh, init, refinement_factor : float The factor by which to scale space, time or both - refinement : str, optional + refinement : str Refinement type. One of 'space', 'time' or 'both' indicating both space and time """ diff --git a/polaris/ocean/tasks/cosine_bell/forward.yaml b/polaris/ocean/tasks/cosine_bell/forward.yaml index 41bd59b6a..da8ef388a 100644 --- a/polaris/ocean/tasks/cosine_bell/forward.yaml +++ b/polaris/ocean/tasks/cosine_bell/forward.yaml @@ -90,7 +90,7 @@ Omega: History: Filename: output.nc Freq: {{ output_freq }} - FreqUnits: Seconds + FreqUnits: seconds Contents: - Tracers - LayerThickness From 2f23adc7d23b46cc7486e4c0907052096cd535b7 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 5 Dec 2024 08:24:47 -0600 Subject: [PATCH 03/11] Set refinement config options only once --- polaris/ocean/tasks/cosine_bell/__init__.py | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/polaris/ocean/tasks/cosine_bell/__init__.py b/polaris/ocean/tasks/cosine_bell/__init__.py index 07b4e20bc..d0d012779 100644 --- a/polaris/ocean/tasks/cosine_bell/__init__.py +++ b/polaris/ocean/tasks/cosine_bell/__init__.py @@ -32,6 +32,7 @@ def add_cosine_bell_tasks(component): 'spherical.cfg') config.add_from_package('polaris.ocean.tasks.cosine_bell', 'cosine_bell.cfg') + _set_convergence_configs(config, prefix) for refinement in ['space', 'time', 'both']: for include_viz in [False, True]: @@ -133,17 +134,8 @@ def _setup_steps(self, refinement): option = 'refinement_factors_time' else: option = 'refinement_factors_space' - refinement_factors = config.getlist('spherical_convergence', - f'{prefix}_{option}', dtype=str) - refinement_factors = ', '.join(refinement_factors) - config.set('convergence', option, value=refinement_factors) - refinement_factors = config.getlist('convergence', - option, dtype=float) - base_resolution = config.getfloat('spherical_convergence', - f'{prefix}_base_resolution') - config.set('convergence', 'base_resolution', - value=f'{base_resolution:03g}') + refinement_factors = config.getlist('convergence', option, dtype=float) # start fresh with no steps for step in list(self.steps.values()): @@ -233,3 +225,19 @@ def _setup_steps(self, refinement): self.add_step(step, symlink=symlink) else: self.add_step(step) + + +def _set_convergence_configs(config, prefix): + for refinement in ['space', 'time']: + option = f'refinement_factors_{refinement}' + refinement_factors = config.getlist('spherical_convergence', + f'{prefix}_{option}', dtype=str) + refinement_factors = ', '.join(refinement_factors) + config.set('convergence', option, value=refinement_factors) + refinement_factors = config.getlist('convergence', + option, dtype=float) + + base_resolution = config.getfloat('spherical_convergence', + f'{prefix}_base_resolution') + config.set('convergence', 'base_resolution', + value=f'{base_resolution:03g}') From 4107a9befbdd9d11de4dc09118c807f7ff12ab48 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 5 Dec 2024 15:33:55 -0600 Subject: [PATCH 04/11] Update variable comparison to take datasets This is needed if we are translating dimension and variable names between native model names and common Polaris names. --- polaris/validate.py | 57 ++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/polaris/validate.py b/polaris/validate.py index 5a8ce7aaf..e650a4a36 100755 --- a/polaris/validate.py +++ b/polaris/validate.py @@ -5,7 +5,8 @@ def compare_variables(variables, filename1, filename2, logger, l1_norm=0.0, - l2_norm=0.0, linf_norm=0.0, quiet=True): + l2_norm=0.0, linf_norm=0.0, quiet=True, ds1=None, + ds2=None): """ compare variables in the two files @@ -49,6 +50,16 @@ def compare_variables(variables, filename1, filename2, logger, l1_norm=0.0, comparison is made. This is generally desirable when using nonzero norm tolerance values. + ds1 : xarray.Dataset, optional + A dataset loaded from filename1. This may save time if the dataset is + already loaded and allows for calculations to be performed or variables + to be renamed if necessary. + + ds2 : xarray.Dataset, optional + A dataset loaded from filename2. This may save time if the dataset is + already loaded and allows for calculations to be performed or variables + to be renamed if necessary. + Returns ------- all_pass : bool @@ -61,18 +72,16 @@ def compare_variables(variables, filename1, filename2, logger, l1_norm=0.0, logger.error(f'File {filename} does not exist.') return False - ds1 = xr.open_dataset(filename1) - ds2 = xr.open_dataset(filename2) + if ds1 is None: + ds1 = xr.open_dataset(filename1) + + if ds2 is None: + ds2 = xr.open_dataset(filename2) all_pass = True for variable in variables: - all_found = True - for ds, filename in [(ds1, filename1), (ds2, filename2)]: - if variable not in ds: - logger.error(f'Variable {variable} not in {filename}.') - all_found = False - if not all_found: + if not _all_found(ds1, filename1, ds2, filename2, variable, logger): all_pass = False continue @@ -85,13 +94,8 @@ def compare_variables(variables, filename1, filename2, logger, l1_norm=0.0, all_pass = False continue - all_match = True - for dim in da1.sizes: - if da1.sizes[dim] != da2.sizes[dim]: - logger.error(f"Field sizes for variable {variable} don't " - f"match files {filename1} and {filename2}.") - all_match = False - if not all_match: + if not _all_sizes_match(da1, filename1, da2, filename2, variable, + logger): all_pass = False continue @@ -139,6 +143,27 @@ def compare_variables(variables, filename1, filename2, logger, l1_norm=0.0, return all_pass +def _all_found(ds1, filename1, ds2, filename2, variable, logger): + """ Is the variable found in both datasets? """ + all_found = True + for ds, filename in [(ds1, filename1), (ds2, filename2)]: + if variable not in ds: + logger.error(f'Variable {variable} not in {filename}.') + all_found = False + return all_found + + +def _all_sizes_match(da1, filename1, da2, filename2, variable, logger): + """ Do all dimension sizes match between the two variables? """ + all_match = True + for dim in da1.sizes: + if da1.sizes[dim] != da2.sizes[dim]: + logger.error(f"Field sizes for variable {variable} don't " + f"match files {filename1} and {filename2}.") + all_match = False + return all_match + + def _compute_norms(da1, da2, quiet, max_l1_norm, max_l2_norm, max_linf_norm, time_index=None): """ Compute norms between variables in two DataArrays """ From 6e66aa19597b8e46779c9816b582d26dff54041a Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Thu, 5 Dec 2024 06:58:30 -0600 Subject: [PATCH 05/11] Add restart test to cosine bell --- polaris/ocean/tasks/cosine_bell/__init__.py | 10 ++- .../tasks/cosine_bell/restart/__init__.py | 86 +++++++++++++++++++ .../tasks/cosine_bell/restart/forward.yaml | 35 ++++++++ .../tasks/cosine_bell/restart/restart_step.py | 85 ++++++++++++++++++ .../tasks/cosine_bell/restart/validate.py | 68 +++++++++++++++ 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 polaris/ocean/tasks/cosine_bell/restart/__init__.py create mode 100644 polaris/ocean/tasks/cosine_bell/restart/forward.yaml create mode 100644 polaris/ocean/tasks/cosine_bell/restart/restart_step.py create mode 100644 polaris/ocean/tasks/cosine_bell/restart/validate.py diff --git a/polaris/ocean/tasks/cosine_bell/__init__.py b/polaris/ocean/tasks/cosine_bell/__init__.py index d0d012779..5cf365a79 100644 --- a/polaris/ocean/tasks/cosine_bell/__init__.py +++ b/polaris/ocean/tasks/cosine_bell/__init__.py @@ -11,6 +11,7 @@ from polaris.ocean.tasks.cosine_bell.analysis import Analysis from polaris.ocean.tasks.cosine_bell.forward import Forward from polaris.ocean.tasks.cosine_bell.init import Init +from polaris.ocean.tasks.cosine_bell.restart import Restart from polaris.ocean.tasks.cosine_bell.viz import Viz @@ -22,7 +23,8 @@ def add_cosine_bell_tasks(component): the ocean component that the tasks will be added to """ - for icosahedral, prefix in [(True, 'icos'), (False, 'qu')]: + for icosahedral, prefix, restart_refinement in [(True, 'icos', 8.0), + (False, 'qu', 2.0)]: filepath = f'spherical/{prefix}/cosine_bell/cosine_bell.cfg' config = PolarisConfigParser(filepath=filepath) @@ -42,6 +44,12 @@ def add_cosine_bell_tasks(component): include_viz=include_viz, refinement=refinement)) + component.add_task(Restart(component=component, + config=config, + icosahedral=icosahedral, + refinement_factor=restart_refinement, + refinement='both')) + class CosineBell(Task): """ diff --git a/polaris/ocean/tasks/cosine_bell/restart/__init__.py b/polaris/ocean/tasks/cosine_bell/restart/__init__.py new file mode 100644 index 000000000..0fb834e77 --- /dev/null +++ b/polaris/ocean/tasks/cosine_bell/restart/__init__.py @@ -0,0 +1,86 @@ +from polaris import Task +from polaris.ocean.convergence import get_resolution_for_task +from polaris.ocean.mesh.spherical import add_spherical_base_mesh_step +from polaris.ocean.tasks.cosine_bell.init import Init +from polaris.ocean.tasks.cosine_bell.restart.restart_step import RestartStep +from polaris.ocean.tasks.cosine_bell.restart.validate import Validate + + +class Restart(Task): + """ + A cosine bell restart test case, which makes sure the model produces + identical results with one longer run and two shorter runs with a restart + in between. + """ + + def __init__(self, component, config, icosahedral, refinement_factor, + refinement): + """ + Create the convergence test + + Parameters + ---------- + component : polaris.ocean.Ocean + The ocean component that this task belongs to + + config : polaris.config.PolarisConfigParser + A shared config parser + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW + meshes + + refinement_factor : float + The factor by which to scale space, time or both + + refinement : str + Refinement type. One of 'space', 'time' or 'both' indicating both + space and time + """ + + if icosahedral: + prefix = 'icos' + else: + prefix = 'qu' + + task_subdir = f'spherical/{prefix}/cosine_bell/restart' + name = f'{prefix}_cosine_bell_restart' + config_filename = 'cosine_bell.cfg' + + super().__init__(component=component, name=name, subdir=task_subdir) + + self.set_shared_config(config, link=config_filename) + + resolution = get_resolution_for_task( + config, refinement_factor, refinement=refinement) + + base_mesh_step, mesh_name = add_spherical_base_mesh_step( + component, resolution, icosahedral) + + name = f'{prefix}_init_{mesh_name}' + init_subdir = f'spherical/{prefix}/cosine_bell/init/{mesh_name}' + if init_subdir in component.steps: + init_step = component.steps[init_subdir] + else: + init_step = Init(component=component, name=name, + subdir=init_subdir, base_mesh=base_mesh_step) + init_step.set_shared_config(config, link=config_filename) + + self.add_step(base_mesh_step, symlink=f'base_mesh/{mesh_name}') + self.add_step(init_step, symlink=f'init/{mesh_name}') + + step_names = ['full_run', 'restart_run'] + for name in step_names: + subdir = f'{task_subdir}/{name}' + step = RestartStep( + component=component, name=name, subdir=subdir, + mesh=base_mesh_step, init=init_step, + refinement_factor=refinement_factor, + refinement=refinement) + step.set_shared_config( + config, link=config_filename) + self.add_step(step) + + self.add_step(Validate(component=component, + step_subdirs=step_names, + indir=task_subdir)) diff --git a/polaris/ocean/tasks/cosine_bell/restart/forward.yaml b/polaris/ocean/tasks/cosine_bell/restart/forward.yaml new file mode 100644 index 000000000..986d4bb19 --- /dev/null +++ b/polaris/ocean/tasks/cosine_bell/restart/forward.yaml @@ -0,0 +1,35 @@ +ocean: + time_management: + config_start_time: {{ start_time }} + config_stop_time: none + config_run_duration: {{ run_duration }} + +mpas-ocean: + time_management: + config_do_restart: {{ do_restart }} + io: + config_write_output_on_startup: false + streams: + restart: + filename_template: ../restarts/rst.$Y-$M-$D_$h.$m.$s.nc + filename_interval: output_interval + output_interval: 0000-00-00_00:00:01 + output: + output_interval: {{ output_interval }} + +Omega: + IOStreams: + InitialState: + FreqUnits: {{ init_freq_units }} + RestartRead: + UsePointerFile: false + Filename: ../restarts/rst.$Y-$M-$D_$h.$m.$s + StartTime: {{ restart_time }} + RestartWrite: + UsePointerFile: false + Filename: ../restarts/rst.$Y-$M-$D_$h.$m.$s + Freq: 1 + FreqUnits: seconds + History: + Freq: {{ output_freq }} + FreqUnits: seconds diff --git a/polaris/ocean/tasks/cosine_bell/restart/restart_step.py b/polaris/ocean/tasks/cosine_bell/restart/restart_step.py new file mode 100644 index 000000000..4e6547a1c --- /dev/null +++ b/polaris/ocean/tasks/cosine_bell/restart/restart_step.py @@ -0,0 +1,85 @@ +import os +import time + +import numpy as np + +from polaris.ocean.convergence import get_timestep_for_task +from polaris.ocean.model import get_time_interval_string +from polaris.ocean.tasks.cosine_bell.forward import Forward + + +class RestartStep(Forward): + """ + A forward model step in the restart test case + """ + def dynamic_model_config(self, at_setup): + """ + Add model config options, namelist, streams and yaml files using config + options or template replacements that need to be set both during step + setup and at runtime + + Parameters + ---------- + at_setup : bool + Whether this method is being run during setup of the step, as + opposed to at runtime + """ + super().dynamic_model_config(at_setup) + + dt, _ = get_timestep_for_task( + self.config, self.refinement_factor, refinement=self.refinement) + dt = np.ceil(dt) + + if self.name == 'full_run': + # 2 time steps without a restart + do_restart = False + start_time = 0. + run_duration = 2. * dt + output_interval = 2. * dt + elif self.name == 'restart_run': + # 1 time step from the restart at 1 time step + do_restart = True + start_time = dt + run_duration = dt + output_interval = dt + else: + raise ValueError(f'Unexpected step name {self.name}') + + # to keep the time formatting from getting too complicated, we'll + # assume 2 time steps is never more than a day + start_time_str = time.strftime('0001-01-01_%H:%M:%S', + time.gmtime(start_time)) + + run_duration_str = get_time_interval_string(seconds=run_duration) + + output_interval_str = get_time_interval_string(seconds=output_interval) + + # For Omega, we want the output interval as a number of seconds + output_freq = int(output_interval) + + if do_restart: + restart_time_str = start_time_str + init_freq_units = 'never' + else: + # Effectively never + restart_time_str = '99999-12-31_00:00:00' + init_freq_units = 'OnStartup' + + package = 'polaris.ocean.tasks.cosine_bell.restart' + replacements = dict( + do_restart=do_restart, + start_time=start_time_str, + run_duration=run_duration_str, + output_interval=output_interval_str, + restart_time=restart_time_str, + init_freq_units=init_freq_units, + output_freq=f'{output_freq}' + ) + + self.add_yaml_file(package=package, + yaml='forward.yaml', + template_replacements=replacements) + + restart_dir = os.path.abspath(os.path.join( + self.work_dir, '..', 'restarts')) + os.makedirs(restart_dir, exist_ok=True) diff --git a/polaris/ocean/tasks/cosine_bell/restart/validate.py b/polaris/ocean/tasks/cosine_bell/restart/validate.py new file mode 100644 index 000000000..c401d7d79 --- /dev/null +++ b/polaris/ocean/tasks/cosine_bell/restart/validate.py @@ -0,0 +1,68 @@ +import os + +from polaris.ocean.model import OceanIOStep +from polaris.validate import compare_variables + + +class Validate(OceanIOStep): + """ + A step for comparing outputs between steps in a cosine bell run + + Attributes + ---------- + step_subdirs : list of str + The number of processors used in each run + """ + def __init__(self, component, step_subdirs, indir): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + step_subdirs : list of str + The number of processors used in each run + + indir : str + the directory the step is in, to which ``name`` will be appended + """ + super().__init__(component=component, name='validate', indir=indir) + + self.step_subdirs = step_subdirs + + for subdir in step_subdirs: + self.add_input_file(filename=f'output_{subdir}.nc', + target=f'../{subdir}/output.nc') + + def run(self): + """ + Compare ``tracer1``, ``layerThickness`` and ``normalVelocity`` in the + outputs of two previous steps with each other + """ + super().run() + step_subdirs = self.step_subdirs + logger = self.logger + variables = ['tracer1', 'layerThickness', 'normalVelocity'] + + filename1 = self.inputs[0] + filename2 = self.inputs[1] + + all_pass = True + for filename in [filename1, filename2]: + if not os.path.exists(filename): + logger.error(f'File {filename} does not exist.') + all_pass = False + + if all_pass: + ds1 = self.open_model_dataset(filename1) + ds2 = self.open_model_dataset(filename2) + + all_pass = compare_variables(variables=variables, + filename1=filename1, + filename2=filename2, logger=logger, + ds1=ds1, ds2=ds2) + if not all_pass: + raise ValueError(f'Validation failed comparing outputs between ' + f'{step_subdirs[0]} and {step_subdirs[1]}.') From ed335a17e31979f729978ea04849a4574cbf8ab4 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 10 Dec 2024 08:42:05 -0600 Subject: [PATCH 06/11] Remove unneeded RestartRead from Omega iostreams Requires https://github.com/E3SM-Project/Omega/pull/184 --- polaris/ocean/tasks/cosine_bell/forward.yaml | 1 - polaris/ocean/tasks/manufactured_solution/forward.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/polaris/ocean/tasks/cosine_bell/forward.yaml b/polaris/ocean/tasks/cosine_bell/forward.yaml index da8ef388a..a9513d5fa 100644 --- a/polaris/ocean/tasks/cosine_bell/forward.yaml +++ b/polaris/ocean/tasks/cosine_bell/forward.yaml @@ -95,4 +95,3 @@ Omega: - Tracers - LayerThickness - NormalVelocity - RestartRead: {} diff --git a/polaris/ocean/tasks/manufactured_solution/forward.yaml b/polaris/ocean/tasks/manufactured_solution/forward.yaml index 4f0073794..98e2ce8cb 100644 --- a/polaris/ocean/tasks/manufactured_solution/forward.yaml +++ b/polaris/ocean/tasks/manufactured_solution/forward.yaml @@ -55,4 +55,3 @@ Omega: - NormalVelocity - LayerThickness - SshCellDefault - RestartRead: {} From cd9204fdf7cba07506b0f59fb2e7a00f7d9cf118 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 10 Dec 2024 09:02:07 -0600 Subject: [PATCH 07/11] Fix UseStartEnd in restart run. --- polaris/ocean/tasks/cosine_bell/restart/forward.yaml | 1 + polaris/ocean/tasks/cosine_bell/restart/restart_step.py | 1 + 2 files changed, 2 insertions(+) diff --git a/polaris/ocean/tasks/cosine_bell/restart/forward.yaml b/polaris/ocean/tasks/cosine_bell/restart/forward.yaml index 986d4bb19..2547ab6be 100644 --- a/polaris/ocean/tasks/cosine_bell/restart/forward.yaml +++ b/polaris/ocean/tasks/cosine_bell/restart/forward.yaml @@ -23,6 +23,7 @@ Omega: FreqUnits: {{ init_freq_units }} RestartRead: UsePointerFile: false + UseStartEnd: {{ not_restart }} Filename: ../restarts/rst.$Y-$M-$D_$h.$m.$s StartTime: {{ restart_time }} RestartWrite: diff --git a/polaris/ocean/tasks/cosine_bell/restart/restart_step.py b/polaris/ocean/tasks/cosine_bell/restart/restart_step.py index 4e6547a1c..28934c0ac 100644 --- a/polaris/ocean/tasks/cosine_bell/restart/restart_step.py +++ b/polaris/ocean/tasks/cosine_bell/restart/restart_step.py @@ -68,6 +68,7 @@ def dynamic_model_config(self, at_setup): package = 'polaris.ocean.tasks.cosine_bell.restart' replacements = dict( do_restart=do_restart, + not_restart=not do_restart, start_time=start_time_str, run_duration=run_duration_str, output_interval=output_interval_str, From ee6d56d49fbdded9c4876fa4dc7ad43a877d6e32 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 16 Dec 2024 06:40:05 -0600 Subject: [PATCH 08/11] Move validate step and fix docstring --- polaris/ocean/tasks/cosine_bell/restart/__init__.py | 2 +- polaris/ocean/tasks/cosine_bell/{restart => }/validate.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename polaris/ocean/tasks/cosine_bell/{restart => }/validate.py (97%) diff --git a/polaris/ocean/tasks/cosine_bell/restart/__init__.py b/polaris/ocean/tasks/cosine_bell/restart/__init__.py index 0fb834e77..e33f1f8ca 100644 --- a/polaris/ocean/tasks/cosine_bell/restart/__init__.py +++ b/polaris/ocean/tasks/cosine_bell/restart/__init__.py @@ -3,7 +3,7 @@ from polaris.ocean.mesh.spherical import add_spherical_base_mesh_step from polaris.ocean.tasks.cosine_bell.init import Init from polaris.ocean.tasks.cosine_bell.restart.restart_step import RestartStep -from polaris.ocean.tasks.cosine_bell.restart.validate import Validate +from polaris.ocean.tasks.cosine_bell.validate import Validate class Restart(Task): diff --git a/polaris/ocean/tasks/cosine_bell/restart/validate.py b/polaris/ocean/tasks/cosine_bell/validate.py similarity index 97% rename from polaris/ocean/tasks/cosine_bell/restart/validate.py rename to polaris/ocean/tasks/cosine_bell/validate.py index c401d7d79..056218bfe 100644 --- a/polaris/ocean/tasks/cosine_bell/restart/validate.py +++ b/polaris/ocean/tasks/cosine_bell/validate.py @@ -23,7 +23,7 @@ def __init__(self, component, step_subdirs, indir): The component the step belongs to step_subdirs : list of str - The number of processors used in each run + Subdirectories for the steps with outputs to compare indir : str the directory the step is in, to which ``name`` will be appended From b6dcacb13539142b6bbc86e4d339c3b6c199cc10 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 23 Dec 2024 07:48:58 -0600 Subject: [PATCH 09/11] Address reviewer comments Add a `do_restart` attribute to keep track of which runs are restart runs, rather than using the step name. --- polaris/ocean/tasks/cosine_bell/forward.py | 11 ++++++++++- polaris/ocean/tasks/cosine_bell/restart/__init__.py | 3 ++- .../ocean/tasks/cosine_bell/restart/restart_step.py | 10 ++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/polaris/ocean/tasks/cosine_bell/forward.py b/polaris/ocean/tasks/cosine_bell/forward.py index 4bdf26479..5e4da4b84 100644 --- a/polaris/ocean/tasks/cosine_bell/forward.py +++ b/polaris/ocean/tasks/cosine_bell/forward.py @@ -5,10 +5,15 @@ class Forward(SphericalConvergenceForward): """ A step for performing forward ocean component runs as part of the cosine bell test case + + Attributes + ---------- + do_restart : bool + Whether this is a restart run """ def __init__(self, component, name, subdir, mesh, init, - refinement_factor, refinement): + refinement_factor, refinement, do_restart=False): """ Create a new step @@ -35,6 +40,9 @@ def __init__(self, component, name, subdir, mesh, init, refinement : str Refinement type. One of 'space', 'time' or 'both' indicating both space and time + + do_restart : bool, optional + Whether this is a restart run """ package = 'polaris.ocean.tasks.cosine_bell' validate_vars = ['normalVelocity', 'tracer1'] @@ -46,6 +54,7 @@ def __init__(self, component, name, subdir, mesh, init, graph_target=f'{mesh.path}/graph.info', refinement_factor=refinement_factor, refinement=refinement) + self.do_restart = do_restart def setup(self): """ diff --git a/polaris/ocean/tasks/cosine_bell/restart/__init__.py b/polaris/ocean/tasks/cosine_bell/restart/__init__.py index e33f1f8ca..c35a1252e 100644 --- a/polaris/ocean/tasks/cosine_bell/restart/__init__.py +++ b/polaris/ocean/tasks/cosine_bell/restart/__init__.py @@ -72,11 +72,12 @@ def __init__(self, component, config, icosahedral, refinement_factor, step_names = ['full_run', 'restart_run'] for name in step_names: subdir = f'{task_subdir}/{name}' + do_restart = (name == 'restart_run') step = RestartStep( component=component, name=name, subdir=subdir, mesh=base_mesh_step, init=init_step, refinement_factor=refinement_factor, - refinement=refinement) + refinement=refinement, do_restart=do_restart) step.set_shared_config( config, link=config_filename) self.add_step(step) diff --git a/polaris/ocean/tasks/cosine_bell/restart/restart_step.py b/polaris/ocean/tasks/cosine_bell/restart/restart_step.py index 28934c0ac..7f8771e35 100644 --- a/polaris/ocean/tasks/cosine_bell/restart/restart_step.py +++ b/polaris/ocean/tasks/cosine_bell/restart/restart_step.py @@ -26,24 +26,22 @@ def dynamic_model_config(self, at_setup): """ super().dynamic_model_config(at_setup) + do_restart = self.do_restart + dt, _ = get_timestep_for_task( self.config, self.refinement_factor, refinement=self.refinement) dt = np.ceil(dt) - if self.name == 'full_run': + if not do_restart: # 2 time steps without a restart - do_restart = False start_time = 0. run_duration = 2. * dt output_interval = 2. * dt - elif self.name == 'restart_run': + else: # 1 time step from the restart at 1 time step - do_restart = True start_time = dt run_duration = dt output_interval = dt - else: - raise ValueError(f'Unexpected step name {self.name}') # to keep the time formatting from getting too complicated, we'll # assume 2 time steps is never more than a day From 8085c6d3b893fa3939cf52ee0f84b36ce02e687f Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 23 Dec 2024 09:26:33 -0600 Subject: [PATCH 10/11] Update the docs --- docs/developers_guide/framework/validation.md | 30 ++++++---- docs/developers_guide/ocean/api.md | 11 +++- .../ocean/tasks/cosine_bell.md | 30 ++++++++-- docs/users_guide/ocean/tasks/cosine_bell.md | 57 +++++++++++-------- 4 files changed, 88 insertions(+), 40 deletions(-) diff --git a/docs/developers_guide/framework/validation.md b/docs/developers_guide/framework/validation.md index f237bfeef..c4047152a 100644 --- a/docs/developers_guide/framework/validation.md +++ b/docs/developers_guide/framework/validation.md @@ -35,9 +35,9 @@ be compared against a baseline, if one is provided, after the step as run. In addition to baseline validation, it is often useful to compare files between steps of a run. This is done by adding later step to perform the validation. -This validation step will use the function -{py:func}`polaris.validate.compare_variables()` to compare variables in a file -with a given relative path (`filename1`) with the same variables in another +This validation step will use the function +{py:func}`polaris.validate.compare_variables()` to compare variables in a file +with a given relative path (`filename1`) with the same variables in another file (`filename2`). As a compact example of creating a validate step for a restart run: @@ -58,7 +58,7 @@ class Validate(Step): super().run() variables = ['temperature', 'salinity', 'layerThickness', 'normalVelocity'] - all_pass = compare_variables(variables, + all_pass = compare_variables(variables, filename1='output_full_run.nc', filename2='output_restart_run.nc', logger=self.logger) @@ -68,12 +68,12 @@ class Validate(Step): ``` -The 2 files `../full_run/output.nc` and `../restart_run/output.nc` are -symlinked locally and compared to make sure the variables `temperature`, +The 2 files `../full_run/output.nc` and `../restart_run/output.nc` are +symlinked locally and compared to make sure the variables `temperature`, `salinity`, `layerThickness`, and `normalVelocity` are identical between the two. -By default, the output is "quiet". If you set `quiet=False`, typical output +By default, the output is "quiet". If you set `quiet=False`, typical output will look like this: ```none @@ -186,7 +186,7 @@ normalVelocity Time index: 0, 1, 2 ## Norms -In circumstance where you would like to allow comparison to pass with non-zero +In circumstance where you would like to allow comparison to pass with non-zero differences between variables, you can supply keyword arguments `l1_norm`, `l2_norm` and/or `linf_norm` to give the desired maximum values for these norms, above which the comparison will fail, raising a @@ -196,8 +196,18 @@ values for these norms, above which the comparison will fail, raising a If you want different nonzero norm values for different variables, the easiest solution is to call {py:func}`polaris.validate.compare_variables()` separately for each variable and with different norm values specified. -You will need to "and" together the results from calling -{py:func}`polaris.validate.compare_variables()`. When you specify a nonzero +You will need to "and" together the results from calling +{py:func}`polaris.validate.compare_variables()`. When you specify a nonzero norm, you may want polaris to print the norm values it is using for comparison when the results are printed. To do so, use the optional `quiet=False` argument. + +## Datasets + +In some cases, a comparison cannot be made directly between the datasets loaded +from the two files to be compared. Instead, the datasets require manipulation +for some reason. Currently, this is the case for datasets from the Omega model, +which need to have their variables renamed to the MPAS-Ocean names for use in +Polaris. The `ds1` and `ds2` keyword arguments are used to supply datasets +corresponding to `filename1` and `filename2`, respectively, in such +circumstances. diff --git a/docs/developers_guide/ocean/api.md b/docs/developers_guide/ocean/api.md index c1f2fa9fa..e35e5b04d 100644 --- a/docs/developers_guide/ocean/api.md +++ b/docs/developers_guide/ocean/api.md @@ -99,14 +99,21 @@ init.cosine_bell forward.Forward - forward.Forward.compute_cell_count - forward.Forward.dynamic_model_config analysis.Analysis analysis.Analysis.exact_solution + validate.Validate + validate.Validate.run + viz.Viz viz.Viz.run + + restart.Restart + + restart.RestartStep + restart.RestartStep.dynamic_model_config + ``` ### geostrophic diff --git a/docs/developers_guide/ocean/tasks/cosine_bell.md b/docs/developers_guide/ocean/tasks/cosine_bell.md index 50a08c3ad..10e4e653c 100644 --- a/docs/developers_guide/ocean/tasks/cosine_bell.md +++ b/docs/developers_guide/ocean/tasks/cosine_bell.md @@ -2,15 +2,18 @@ # cosine_bell -The {py:class}`polaris.ocean.tasks.cosine_bell.CosineBell` +In most cases, the {py:class}`polaris.ocean.tasks.cosine_bell.CosineBell` test performs a series of 24-day runs that advect a bell-shaped tracer blob around the sphere. The resolution of the sphere varies (by default, between 60 and 240 km). Advected results are compared with a known exact solution to determine the rate of convergence. +There is also a restart test, described below, that performs only two time +steps of the test to verify the exact restart capability. + ## framework -The config options for the `cosine_bell` tests are described in +The config options for the `cosine_bell` tests are described in {ref}`ocean-cosine-bell` in the User's Guide. Additionally, the test uses a `forward.yaml` file with a few common @@ -38,18 +41,24 @@ descends from {py:class}`polaris.ocean.convergence.spherical.SphericalConvergenc and defines a step for running MPAS-Ocean from an initial condition produced in an `init` step. See {ref}`dev-ocean-convergence` for some relevant discussion of the parent class. The time step is determined from the resolution -based on the `dt_per_km` config option in the `[convergence_forward]` +based on the `dt_per_km` config option in the `[convergence_forward]` section. Other model config options are taken from `forward.yaml`. ### analysis The class {py:class}`polaris.ocean.tasks.cosine_bell.analysis.Analysis` descends from -{py:class}`polaris.ocean.convergence.ConvergenceAnalysis`, +{py:class}`polaris.ocean.convergence.analysis.ConvergenceAnalysis`, and defines a step for computing the error norm (L2) for the results at each resolution, saving them in `convergence_tracer1.csv` and plotting them in `convergence_tracer1.png`. +### validate + +The class {py:class}`polaris.ocean.tasks.cosine_bell.validate.Validate` is a +step for validating the results between two cosine-bell runs. It is currently +used to verify bit-for-bit restart in the `restart` test described below. + ### viz The visualization step is available only in the `cosine_bell/with_viz` @@ -76,3 +85,16 @@ colorbar_limits = 0.0, 1.0 ``` See {ref}`dev-visualization-global` for more details. + +## restart + +The {py:class}`polaris.ocean.tasks.cosine_bell.restart.Restart` class defines +a restart check that performs two time steps of the Cosine Bell test at coarse +resolution, then performs reruns the second time step, as a restart run to +verify the bit-for-bit restart capability for tracer advection. + +### restart_step + +The {py:class}`polaris.ocean.tasks.cosine_bell.restart.RestartStep` class +defines both steps of the restart run, the "full" run (2 time steps) and the +"restart" run (repeating the last time step after a restart). diff --git a/docs/users_guide/ocean/tasks/cosine_bell.md b/docs/users_guide/ocean/tasks/cosine_bell.md index 5230fa36b..df9db26b5 100644 --- a/docs/users_guide/ocean/tasks/cosine_bell.md +++ b/docs/users_guide/ocean/tasks/cosine_bell.md @@ -4,52 +4,61 @@ ## description -The `cosine_bell` and `cosine_bell/with_viz` tasks implement the Cosine -Bell test case as first described in +The `cosine_bell/convergence_*` and `cosine_bell/convergence_*/with_viz` tasks +implement the Cosine Bell test case as first described in [Williamson et al. 1992]() but using the variant from Sec. 3a of [Skamarock and Gassmann](https://doi.org/10.1175/MWR-D-10-05056.1). A flow field representing solid-body rotation transports a bell-shaped perturbation in a tracer $\psi$ once around the sphere, returning to its initial location. -The task is a convergence test with time step varying proportionately to grid -size. The result of the `analysis` step of the task is a plot like the -following showing convergence as a function of the number of cells: +The `convergence_both` task is a convergence test with time step varying +proportionately to cell size, while `convergence_time` and `convergence_space` +vary only the time step and the cell size, respectively. The result of the +`analysis` step of each task is a plot like the following showing convergence +as a function of the cell size and/or the time step: ```{image} images/cosine_bell_convergence.png :align: center :width: 500 px ``` -The `cosine_bell/with_viz` variant also includes visualization of the initial +The `with_viz` variant also includes visualization of the initial and final state on a lat-lon grid for each resolution. The visualization is -not included in the `cosine_bell` version of the task in order to not slow down +not included in the other versions of the task in order to not slow down regression testing. +Another task, `cosine_bell/restart`, performs two time steps of the Cosine Bell +test at coarse resolution, then performs reruns the second time step, +as a restart run to verify the bit-for-bit restart capability for tracer +advection. + ## suppported models -These tasks support only MPAS-Ocean. +These tasks support both MPAS-Ocean and Omega. (ocean-cosine-bell-mesh)= ## mesh Two global mesh variants are tested, quasi-uniform (QU) and icosohydral. There -are also variants to test convergence in space, time, or both space and time. -In addition, the tests can be set up with or without the viz step. Thus, there -are 12 variants of the task: +are also variants to test convergence in space, time, or both space and time +as well as the restart test. In addition, the tests can be set up with or +without the viz step. Thus, there are 14 variants of the task: ``` -ocean/spherical/icos/cosine_bell/convergence_both/ -ocean/spherical/icos/cosine_bell/convergence_both/with_viz -ocean/spherical/qu/cosine_bell/convergence_both/ -ocean/spherical/qu/cosine_bell/convergence_both/with_viz -ocean/spherical/icos/cosine_bell/convergence_space/ +ocean/spherical/icos/cosine_bell/convergence_space ocean/spherical/icos/cosine_bell/convergence_space/with_viz -ocean/spherical/qu/cosine_bell/convergence_space/ -ocean/spherical/qu/cosine_bell/convergence_space/with_viz -ocean/spherical/icos/cosine_bell/convergence_time/ +ocean/spherical/icos/cosine_bell/convergence_time ocean/spherical/icos/cosine_bell/convergence_time/with_viz -ocean/spherical/qu/cosine_bell/convergence_time/ +ocean/spherical/icos/cosine_bell/convergence_both +ocean/spherical/icos/cosine_bell/convergence_both/with_viz +ocean/spherical/icos/cosine_bell/restart +ocean/spherical/qu/cosine_bell/convergence_space +ocean/spherical/qu/cosine_bell/convergence_space/with_viz +ocean/spherical/qu/cosine_bell/convergence_time ocean/spherical/qu/cosine_bell/convergence_time/with_viz +ocean/spherical/qu/cosine_bell/convergence_both +ocean/spherical/qu/cosine_bell/convergence_both/with_viz +ocean/spherical/qu/cosine_bell/restart ``` The default resolutions used in the task depends on the mesh type. @@ -85,10 +94,10 @@ qu_base_resolution = 120. qu_refinement_factors = 0.5, 0.75, 1., 1.25, 1.5, 1.75, 2. ``` -To alter the resolutions used in this task, you will need to create your own -config file (or add a `spherical_convergence` section to a config file if -you're already using one). The resolutions are a comma-separated list of the -resolution of the mesh in km. If you specify a different list +To alter the resolutions used in the convergence tasks, you will need to create +your own config file (or add a `spherical_convergence` section to a config file +if you're already using one). The resolutions are a comma-separated list of +the resolution of the mesh in km. If you specify a different list before setting up `cosine_bell`, steps will be generated with the requested resolutions. (If you alter `icos_resolutions` or `qu_resolutions`) in the task's config file in the work directory, nothing will happen.) For `icos` From 2b6571ddcb8a513382a92e95c7c7397be8728a64 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Mon, 23 Dec 2024 09:49:29 -0600 Subject: [PATCH 11/11] Add cosine bell restart to pr and nightly suites Update cosine_bell suite to include all tests. Remove cosine_bell_cached_init suite, which has not been well maintained. --- polaris/ocean/suites/cosine_bell.txt | 16 ++++++++++++++-- polaris/ocean/suites/cosine_bell_cached_init.txt | 8 -------- polaris/ocean/suites/nightly.txt | 1 + polaris/ocean/suites/pr.txt | 1 + 4 files changed, 16 insertions(+), 10 deletions(-) delete mode 100644 polaris/ocean/suites/cosine_bell_cached_init.txt diff --git a/polaris/ocean/suites/cosine_bell.txt b/polaris/ocean/suites/cosine_bell.txt index 7ef38e541..81ea683d1 100644 --- a/polaris/ocean/suites/cosine_bell.txt +++ b/polaris/ocean/suites/cosine_bell.txt @@ -1,2 +1,14 @@ -ocean/spherical/icos/cosine_bell -ocean/spherical/qu/cosine_bell +ocean/spherical/icos/cosine_bell/convergence_space +ocean/spherical/icos/cosine_bell/convergence_space/with_viz +ocean/spherical/icos/cosine_bell/convergence_time +ocean/spherical/icos/cosine_bell/convergence_time/with_viz +ocean/spherical/icos/cosine_bell/convergence_both +ocean/spherical/icos/cosine_bell/convergence_both/with_viz +ocean/spherical/icos/cosine_bell/restart +ocean/spherical/qu/cosine_bell/convergence_space +ocean/spherical/qu/cosine_bell/convergence_space/with_viz +ocean/spherical/qu/cosine_bell/convergence_time +ocean/spherical/qu/cosine_bell/convergence_time/with_viz +ocean/spherical/qu/cosine_bell/convergence_both +ocean/spherical/qu/cosine_bell/convergence_both/with_viz +ocean/spherical/qu/cosine_bell/restart diff --git a/polaris/ocean/suites/cosine_bell_cached_init.txt b/polaris/ocean/suites/cosine_bell_cached_init.txt deleted file mode 100644 index 021e294c1..000000000 --- a/polaris/ocean/suites/cosine_bell_cached_init.txt +++ /dev/null @@ -1,8 +0,0 @@ -ocean/spherical/icos/cosine_bell - cached: icos_base_mesh_60km icos_init_60km icos_base_mesh_120km icos_init_120km - cached: icos_base_mesh_240km icos_init_240km icos_base_mesh_480km icos_init_480km -ocean/spherical/qu/cosine_bell - cached: qu_base_mesh_60km qu_init_60km qu_base_mesh_90km qu_init_90km - cached: qu_base_mesh_120km qu_init_120km qu_base_mesh_150km qu_init_150km - cached: qu_base_mesh_180km qu_init_180km qu_base_mesh_210km qu_init_210km - cached: qu_base_mesh_240km qu_init_240km diff --git a/polaris/ocean/suites/nightly.txt b/polaris/ocean/suites/nightly.txt index e2b2ef4aa..e1760b595 100644 --- a/polaris/ocean/suites/nightly.txt +++ b/polaris/ocean/suites/nightly.txt @@ -5,3 +5,4 @@ ocean/planar/ice_shelf_2d/5km/z-star/default/with_restart ocean/planar/ice_shelf_2d/5km/z-level/default/with_restart ocean/planar/inertial_gravity_wave/convergence_both # ocean/planar/manufactured_solution +ocean/spherical/icos/cosine_bell/restart diff --git a/polaris/ocean/suites/pr.txt b/polaris/ocean/suites/pr.txt index 4bb3cf52a..c7fc1b175 100644 --- a/polaris/ocean/suites/pr.txt +++ b/polaris/ocean/suites/pr.txt @@ -9,3 +9,4 @@ ocean/planar/internal_wave/vlr/default # ocean/planar/manufactured_solution ocean/single_column/cvmix ocean/single_column/ideal_age +ocean/spherical/icos/cosine_bell/restart