diff --git a/docs/developers_guide/ocean/api.md b/docs/developers_guide/ocean/api.md index 675973bf7..43d26c0ef 100644 --- a/docs/developers_guide/ocean/api.md +++ b/docs/developers_guide/ocean/api.md @@ -82,6 +82,40 @@ viz.Viz.run ``` +### sphere_transport + +```{eval-rst} +.. currentmodule:: polaris.ocean.tasks.sphere_transport + +.. autosummary:: + :toctree: generated/ + + add_sphere_transport_tasks + + SphereTransport + SphereTransport.configure + + init.Init + init.Init.run + + forward.Forward + + analysis.Analysis + analysis.Analysis.convergence_parameters + + mixing_analysis.MixingAnalysis + mixing_analysis.MixingAnalysis.run + + filament_analysis.FilamentAnalysis + filament_analysis.FilamentAnalysis.run + + viz.VizMap + viz.VizMap.runtime_setup + + viz.Viz + viz.Viz.run +``` + ### cosine_bell ```{eval-rst} diff --git a/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md b/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md new file mode 100644 index 000000000..af67f1fcd --- /dev/null +++ b/docs/developers_guide/ocean/tasks/correlated_tracers_2d.md @@ -0,0 +1,108 @@ +(dev-ocean-correlated-tracers-2d)= + +# correlated_tracers_2d + +The {py:class}`polaris.ocean.tasks.sphere_transport.SphereTransport` +`correlated_tracers_2d` test performs a 12-day run on the sphere that has a periodic +deforming flow which affects tracer distributions. The resolution of the +sphere varies (by default, between 60 and 240 km). After one period, the +tracer distributions are compared the initial condition to evaluate numerical +errors associated with the horizontal advection scheme and determine the rate +of convergence. + +## framework + +The config options for the `correlated_tracers_2d` test is described in +{ref}`ocean-correlated_tracers-2d` in the User's Guide. + +Additionally, the test uses a `forward.yaml` file with a few common +model config options related to drag and default horizontal and +vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, +`restart`, and `output` streams. This file has Jinja templating that is +used to update model config options based on Polaris config options, see +{ref}`dev-ocean-spherical-convergence`. + +### base_mesh + +Sphere transport tasks use shared `base_mesh` steps for creating +{ref}`dev-ocean-spherical-meshes` at a sequence of resolutions. + +### init + +The class {py:class}`polaris.ocean.tasks.sphere_transport.init.Init` +defines a step for setting up the initial state at each resolution. The +initial state is differentiated between `correlated_tracers_2d` and the other tests +set up by the class. + +### forward + +The class {py:class}`polaris.ocean.tasks.sphere_transport.forward.Forward` +descends from {py:class}`polaris.ocean.convergence.spherical.SphericalConvergenceForward`, +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]` +section. Other model config options are taken from `forward.yaml`. + +### analysis + +The class {py:class}`polaris.ocean.tasks.sphere_transport.analysis.Analysis` +descends from +{py:class}`polaris.ocean.convergence.ConvergenceAnalysis`, +and defines a step for computing the error norm (L2) for the results +at each resolution for tracers and layer thickness, saving them in +`convergence_*.csv` and plotting them in `convergence_*.png`. + +### mixing_analysis + +The class {py:class}`polaris.ocean.tasks.sphere_transport.mixing_analysis.MixingAnalysis` +plots the relationship between two correlated tracers for each resolution in +`triplots.png`. + +### viz + +Visualization steps are available only in the `correlated_tracers_2d/with_viz` +tasks. They are not included in the `correlated_tracers_2d` in order to keep regression +as fast as possible when visualization isn't needed. + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.VizMap` +defines a step for creating a mapping file from the MPAS mesh at a given +resolution to a lon-lat grid at a resolution and interpolation method +determined by config options. + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve +``` + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.Viz` +is a step for plotting the initial and final states of the advection test for +each resolution, mapped to the common lat-lon grid. The colormap is controlled +by these options: + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz_*] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) +``` + +See {ref}`dev-visualization-global` for more details. diff --git a/docs/developers_guide/ocean/tasks/divergent_2d.md b/docs/developers_guide/ocean/tasks/divergent_2d.md new file mode 100644 index 000000000..65a27c1ee --- /dev/null +++ b/docs/developers_guide/ocean/tasks/divergent_2d.md @@ -0,0 +1,102 @@ +(dev-ocean-divergent-2d)= + +# divergent_2d + +The {py:class}`polaris.ocean.tasks.sphere_transport.SphereTransport` +`divergent_2d` test performs a 12-day run on the sphere that has a periodic +divergent flow which affects tracer distributions. The resolution of the +sphere varies (by default, between 60 and 240 km). After one period, the +tracer distributions are compared the initial condition to evaluate numerical +errors associated with the horizontal advection scheme and determine the rate +of convergence. + +## framework + +The config options for the `divergent_2d` test is described in +{ref}`ocean-divergent-2d` in the User's Guide. + +Additionally, the test uses a `forward.yaml` file with a few common +model config options related to drag and default horizontal and +vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, +`restart`, and `output` streams. This file has Jinja templating that is +used to update model config options based on Polaris config options, see +{ref}`dev-ocean-spherical-convergence`. + +### base_mesh + +Sphere transport tasks use shared `base_mesh` steps for creating +{ref}`dev-ocean-spherical-meshes` at a sequence of resolutions. + +### init + +The class {py:class}`polaris.ocean.tasks.sphere_transport.init.Init` +defines a step for setting up the initial state at each resolution. The +initial state is differentiated between `divergent_2d` and the other tests +set up by the class. + +### forward + +The class {py:class}`polaris.ocean.tasks.sphere_transport.forward.Forward` +descends from {py:class}`polaris.ocean.convergence.spherical.SphericalConvergenceForward`, +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]` +section. Other model config options are taken from `forward.yaml`. + +### analysis + +The class {py:class}`polaris.ocean.tasks.sphere_transport.analysis.Analysis` +descends from +{py:class}`polaris.ocean.convergence.ConvergenceAnalysis`, +and defines a step for computing the error norm (L2) for the results +at each resolution for tracers and layer thickness, saving them in +`convergence_*.csv` and plotting them in `convergence_*.png`. + +### viz + +Visualization steps are available only in the `divergent_2d/with_viz` +tasks. They are not included in the `divergent_2d` in order to keep regression +as fast as possible when visualization isn't needed. + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.VizMap` +defines a step for creating a mapping file from the MPAS mesh at a given +resolution to a lon-lat grid at a resolution and interpolation method +determined by config options. + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve +``` + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.Viz` +is a step for plotting the initial and final states of the advection test for +each resolution, mapped to the common lat-lon grid. The colormap is controlled +by these options: + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz_*] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) +``` + +See {ref}`dev-visualization-global` for more details. diff --git a/docs/developers_guide/ocean/tasks/index.md b/docs/developers_guide/ocean/tasks/index.md index 268e21c92..e2c45b4aa 100644 --- a/docs/developers_guide/ocean/tasks/index.md +++ b/docs/developers_guide/ocean/tasks/index.md @@ -6,9 +6,13 @@ :titlesonly: true baroclinic_channel +correlated_tracers_2d cosine_bell geostrophic +divergent_2d inertial_gravity_wave manufactured_solution +nondivergent_2d +rotation_2d single_column ``` diff --git a/docs/developers_guide/ocean/tasks/nondivergent_2d.md b/docs/developers_guide/ocean/tasks/nondivergent_2d.md new file mode 100644 index 000000000..9eb2fbc3f --- /dev/null +++ b/docs/developers_guide/ocean/tasks/nondivergent_2d.md @@ -0,0 +1,108 @@ +(dev-ocean-nondivergent-2d)= + +# nondivergent_2d + +The {py:class}`polaris.ocean.tasks.sphere_transport.SphereTransport` +`nondivergent_2d` test performs a 12-day run on the sphere that has a periodic +deforming flow which affects tracer distributions. The resolution of the +sphere varies (by default, between 60 and 240 km). After one period, the +tracer distributions are compared the initial condition to evaluate numerical +errors associated with the horizontal advection scheme and determine the rate +of convergence. + +## framework + +The config options for the `nondivergent_2d` test is described in +{ref}`ocean-nondivergent-2d` in the User's Guide. + +Additionally, the test uses a `forward.yaml` file with a few common +model config options related to drag and default horizontal and +vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, +`restart`, and `output` streams. This file has Jinja templating that is +used to update model config options based on Polaris config options, see +{ref}`dev-ocean-spherical-convergence`. + +### base_mesh + +Sphere transport tasks use shared `base_mesh` steps for creating +{ref}`dev-ocean-spherical-meshes` at a sequence of resolutions. + +### init + +The class {py:class}`polaris.ocean.tasks.sphere_transport.init.Init` +defines a step for setting up the initial state at each resolution. The +initial state is differentiated between `nondivergent_2d` and the other tests +set up by the class. + +### forward + +The class {py:class}`polaris.ocean.tasks.sphere_transport.forward.Forward` +descends from {py:class}`polaris.ocean.convergence.spherical.SphericalConvergenceForward`, +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]` +section. Other model config options are taken from `forward.yaml`. + +### analysis + +The class {py:class}`polaris.ocean.tasks.sphere_transport.analysis.Analysis` +descends from +{py:class}`polaris.ocean.convergence.ConvergenceAnalysis`, +and defines a step for computing the error norm (L2) for the results +at each resolution for tracers and layer thickness, saving them in +`convergence_*.csv` and plotting them in `convergence_*.png`. + +### filament_analysis + +The class {py:class}`polaris.ocean.tasks.sphere_transport.filament_analysis.FilamentAnalysis` +computes a filament diagnostic for each threshold value and each resolution +and plots that diagnostic in `filament.png`. + +### viz + +Visualization steps are available only in the `nondivergent_2d/with_viz` +tasks. They are not included in the `nondivergent_2d` in order to keep regression +as fast as possible when visualization isn't needed. + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.VizMap` +defines a step for creating a mapping file from the MPAS mesh at a given +resolution to a lon-lat grid at a resolution and interpolation method +determined by config options. + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve +``` + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.Viz` +is a step for plotting the initial and final states of the advection test for +each resolution, mapped to the common lat-lon grid. The colormap is controlled +by these options: + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz_*] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) +``` + +See {ref}`dev-visualization-global` for more details. diff --git a/docs/developers_guide/ocean/tasks/rotation_2d.md b/docs/developers_guide/ocean/tasks/rotation_2d.md new file mode 100644 index 000000000..7be67239e --- /dev/null +++ b/docs/developers_guide/ocean/tasks/rotation_2d.md @@ -0,0 +1,102 @@ +(dev-ocean-rotation-2d)= + +# rotation_2d + +The {py:class}`polaris.ocean.tasks.sphere_transport.SphereTransport` +`rotation_2d` test performs a 12-day run on the sphere that has a flow with +a constant rotation rate which affects tracer distributions. The resolution of +the sphere varies (by default, between 60 and 240 km). After one period, the +tracer distributions are compared the initial condition to evaluate numerical +errors associated with the horizontal advection scheme and determine the rate +of convergence. + +## framework + +The config options for the `rotation_2d` test is described in +{ref}`ocean-rotation-2d` in the User's Guide. + +Additionally, the test uses a `forward.yaml` file with a few common +model config options related to drag and default horizontal and +vertical momentum and tracer diffusion, as well as defining `mesh`, `input`, +`restart`, and `output` streams. This file has Jinja templating that is +used to update model config options based on Polaris config options, see +{ref}`dev-ocean-spherical-convergence`. + +### base_mesh + +Sphere transport tasks use shared `base_mesh` steps for creating +{ref}`dev-ocean-spherical-meshes` at a sequence of resolutions. + +### init + +The class {py:class}`polaris.ocean.tasks.sphere_transport.init.Init` +defines a step for setting up the initial state at each resolution. The +initial state is differentiated between `rotation_2d` and the other tests +set up by the class. + +### forward + +The class {py:class}`polaris.ocean.tasks.sphere_transport.forward.Forward` +descends from {py:class}`polaris.ocean.convergence.spherical.SphericalConvergenceForward`, +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]` +section. Other model config options are taken from `forward.yaml`. + +### analysis + +The class {py:class}`polaris.ocean.tasks.sphere_transport.analysis.Analysis` +descends from +{py:class}`polaris.ocean.convergence.ConvergenceAnalysis`, +and defines a step for computing the error norm (L2) for the results +at each resolution for tracers and layer thickness, saving them in +`convergence_*.csv` and plotting them in `convergence_*.png`. + +### viz + +Visualization steps are available only in the `rotation_2d/with_viz` +tasks. They are not included in the `rotation_2d` in order to keep regression +as fast as possible when visualization isn't needed. + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.VizMap` +defines a step for creating a mapping file from the MPAS mesh at a given +resolution to a lon-lat grid at a resolution and interpolation method +determined by config options. + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve +``` + +The class {py:class}`polaris.ocean.tasks.sphere_transport.viz.Viz` +is a step for plotting the initial and final states of the advection test for +each resolution, mapped to the common lat-lon grid. The colormap is controlled +by these options: + +```cfg +# options for visualization for the cosine bell convergence test case +[sphere_transport_viz_*] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) +``` + +See {ref}`dev-visualization-global` for more details. diff --git a/docs/users_guide/ocean/tasks/correlated_tracers_2d.md b/docs/users_guide/ocean/tasks/correlated_tracers_2d.md new file mode 100644 index 000000000..aaadcab5d --- /dev/null +++ b/docs/users_guide/ocean/tasks/correlated_tracers_2d.md @@ -0,0 +1,336 @@ +(ocean-correlated-tracers-2d)= + +# correlated tracers 2-d + +## description + +The `correlated_tracers_2d` and `correlated_tracers_2d/with_viz` tasks implement the +non-divergent flow field test of (1) numerical order of convergence and (2) +mixing as described in +[Lauritzen et al. 2012](). + +The numerical order of convergence is analyzed in the `analysis` step and +produces a figure similar to the following showing L2 error norm as a function +of horizontal resolution: + +```{image} images/correlated_tracers_2d_convergence.png +:align: center +:width: 500 px +``` + +The ability of the horizontal advection scheme to maintain nonlinear +relationships between tracers is addressed in the `mixing_analysis` step. This +step produces a visualization of the relationship between two debug tracers at +the end of the simulation: + +```{image} images/correlated_tracers_2d_mixing.png +:align: center +:width: 500 px +``` + +## mesh + +Two global mesh variants are tested, quasi-uniform (QU) and icosohydral. Thus, +there are 4 variants of the task: +``` +ocean/spherical/icos/correlated_tracers_2d +ocean/spherical/icos/correlated_tracers_2d/with_viz +ocean/spherical/qu/correlated_tracers_2d +ocean/spherical/qu/correlated_tracers_2d/with_viz +``` +The default resolutions used in the task depends on the mesh type. + +For the `icos` mesh type, the defaults are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of icosahedral mesh resolutions (km) to test +icos_resolutions = 60, 120, 240, 480 +``` + +for the `qu` mesh type, they are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of quasi-uniform mesh resolutions (km) to test +qu_resolutions = 60, 90, 120, 150, 180, 210, 240 +``` + +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 +before setting up `correlated_tracers_2d`, 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` +meshes, make sure you use a resolution close to those listed in +{ref}`dev-spherical-meshes`. Each resolution will be rounded to the nearest +allowed icosahedral resolution. + +The `base_mesh` steps are shared with other tasks so they are not housed in +the `correlated_tracers_2d` work directory. Instead, they are in work directories +like: + +``` +ocean/spherical/icos/base_mesh/60km +ocean/spherical/qu/base_mesh/60km +``` + +For convenience, there are symlinks inside of the `correlated_tracers_2d` and +`correlated_tracers_2d/with_viz` work directories, e.g.: +``` +ocean/spherical/icos/correlated_tracers_2d/base_mesh/60km +ocean/spherical/qu/correlated_tracers_2d/base_mesh/60km +ocean/spherical/icos/correlated_tracers_2d/with_viz/base_mesh/60km +ocean/spherical/qu/correlated_tracers_2d/with_viz/base_mesh/60km +``` + +## vertical grid + +This task only exercises the shallow water dynamics. As such, a single +vertical level may be used. The bottom depth is constant and the +results should be insensitive to the choice of `bottom_depth`. + +```cfg +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 3 + +# Depth of the bottom of the ocean +bottom_depth = 300.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-level + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = None + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 +``` + +## initial conditions + +The initial condition is characterized by three separate tracer distributions +stored in three `debugTracers`: + +- `tracer1`: A c-infinity function used for convergence analysis +- `tracer2`: A pair of c-2 cosine bells +- `tracer3`: A second pair of cosine bells that are nonlinearly correlated with +tracer2. + +```{image} images/correlated_tracers_2d_init_tracer1.png +:align: center +:width: 500 px +``` + +```{image} images/correlated_tracers_2d_init_tracer2.png +:align: center +:width: 500 px +``` + +```{image} images/correlated_tracers_2d_init_tracer3.png +:align: center +:width: 500 px +``` + +The velocity is + +$$ +u(t) = \frac{10 R}{\tau} \sin^2(\lambda - \frac{2 \pi t}{\tau}) \sin(2\theta) \cos(\frac{\pi t}{\tau}) + \frac{2 \pi R}{\tau}\cos(\theta) +$$ + +$$ +v(t) = \frac{10 R}{\tau} \sin(2\lambda - \frac{4 \pi t}{\tau}) \cos(\theta) \cos(\frac{\pi t}{\tau}) +$$ + +Where $\lambda$ is longitude, $\theta$ is latitude and $R$ is the radius of the +sphere. $\tau$ is the time it takes to transit the equator. The default is 12 +days and is given by the cfg option `vel_pd`. + +Temperature and salinity are not evolved in this task and are given +constant values determined by config options `temperature` and `salinity`. + +The Coriolis parameters `fCell`, `fEdge`, and `fVertex` do not need to be +specified for a global mesh and are initialized as zeros. + +## forcing + +This case is forced to follow $u(t)$ and $v(t)$ given above. + +## time step and run duration + +This task uses the Runge-Kutta 4th-order (RK4) time integrator. The time step +for forward integration is determined by multiplying the resolution by a config +option, `rk4_dt_per_km`, so that coarser meshes have longer time steps. You can +alter this before setup (in a user config file) or before running the task (in +the config file in the work directory). + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# time integrator: {'split_explicit', 'RK4'} +time_integrator = RK4 + +# RK4 time step per resolution (s/km), since dt is proportional to resolution +rk4_dt_per_km = 3.0 +``` + +The `convergence_eval_time`, `run_duration` and `output_interval` are the +period for advection to make a full rotation around the globe, 12 days: + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# Run duration in days +run_duration = ${sphere_transport:vel_pd} + +# Output interval in days +output_interval = ${sphere_transport:vel_pd} +``` + +Here, `${sphere_transport:vel_pd}` means that the same value is used as in the +option `vel_pd` in section `[sphere_transport]`, see below. + +## config options + +The `correlated_tracer_2d` config options include: + +```cfg +# options for all sphere transport test cases +[sphere_transport] + +# temperature +temperature = 15. + +# salinity +salinity = 35. + +# time (days) for bell to transit equator once +vel_pd = 12.0 + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz_tracer] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_tracer_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + + +# options for thickness visualization for the sphere transport test case +[sphere_transport_viz_h] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 99., 'vmax': 101.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_h_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + +# options for correlated tracers 2-d test case +[correlated_tracers_2d] + +# velocity amplitude in meters per second +vel_amp = 10. + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.7 +convergence_thresh_tracer2 = 1.66 +convergence_thresh_tracer3 = 1.28 + +# time in days at which to evaluate mixing +mixing_evaluation_time = 6.0 +``` + +The options in section `sphere_transport` are used by all 4 test cases based +on Lauritzen et al. (2012) and control the initial condition. The options in +section `correlated_tracers_2d` control the initial condition of that case +only, the convergence rate threshold, and the time at which to evaluate mixing +diagnostics. + +The options in sections `sphere_transport_viz*` control properties of the `viz` +step of the test case. + +The default options for the convergence analysis step can be changed here: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# Evaluation time for convergence analysis (in days) +convergence_eval_time = ${sphere_transport:vel_pd} + +# Type of error to compute +error_type = l2 +``` + +## cores + +The target and minimum number of cores are determined by `goal_cells_per_core` +and `max_cells_per_core` from the `ocean` section of the config file, +respectively. This ensures that the number of cells per core is roughly +constant across the different resolutions in the convergence study. diff --git a/docs/users_guide/ocean/tasks/divergent_2d.md b/docs/users_guide/ocean/tasks/divergent_2d.md new file mode 100644 index 000000000..024d8c0f3 --- /dev/null +++ b/docs/users_guide/ocean/tasks/divergent_2d.md @@ -0,0 +1,321 @@ +(ocean-divergent-2d)= + +# divergent 2-d + +## description + +The `divergent_2d` and `divergent_2d/with_viz` tasks implement the +divergent flow field test of numerical order of convergence as described in +[Lauritzen et al. 2012](). + +The numerical order of convergence is analyzed in the `analysis` step and +produces a figure similar to the following showing L2 error norm as a function +of horizontal resolution: + +```{image} images/divergent_2d_convergence.png +:align: center +:width: 500 px +``` + +## mesh + +Two global mesh variants are tested, quasi-uniform (QU) and icosohydral. Thus, +there are 4 variants of the task: +``` +ocean/spherical/icos/divergent_2d +ocean/spherical/icos/divergent_2d/with_viz +ocean/spherical/qu/divergent_2d +ocean/spherical/qu/divergent_2d/with_viz +``` +The default resolutions used in the task depends on the mesh type. + +For the `icos` mesh type, the defaults are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of icosahedral mesh resolutions (km) to test +icos_resolutions = 60, 120, 240, 480 +``` + +for the `qu` mesh type, they are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of quasi-uniform mesh resolutions (km) to test +qu_resolutions = 60, 90, 120, 150, 180, 210, 240 +``` + +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 +before setting up `divergent_2d`, 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` +meshes, make sure you use a resolution close to those listed in +{ref}`dev-spherical-meshes`. Each resolution will be rounded to the nearest +allowed icosahedral resolution. + +The `base_mesh` steps are shared with other tasks so they are not housed in +the `divergent_2d` work directory. Instead, they are in work directories +like: + +``` +ocean/spherical/icos/base_mesh/60km +ocean/spherical/qu/base_mesh/60km +``` + +For convenience, there are symlinks inside of the `divergent_2d` and +`divergent_2d/with_viz` work directories, e.g.: +``` +ocean/spherical/icos/divergent_2d/base_mesh/60km +ocean/spherical/qu/divergent_2d/base_mesh/60km +ocean/spherical/icos/divergent_2d/with_viz/base_mesh/60km +ocean/spherical/qu/divergent_2d/with_viz/base_mesh/60km +``` + +## vertical grid + +This task only exercises the shallow water dynamics. As such, a single +vertical level may be used. The bottom depth is constant and the +results should be insensitive to the choice of `bottom_depth`. + +```cfg +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 3 + +# Depth of the bottom of the ocean +bottom_depth = 300.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-level + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = None + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 +``` + +## initial conditions + +The initial condition is characterized by three separate tracer distributions +stored in three `debugTracers`: + +- `tracer1`: A c-infinity function used for convergence analysis +- `tracer2`: A pair of c-2 cosine bells +- `tracer3`: A discontinuous pair of slotted cylinders + +```{image} images/divergent_2d_init_tracer1.png +:align: center +:width: 500 px +``` + +```{image} images/divergent_2d_init_tracer2.png +:align: center +:width: 500 px +``` + +```{image} images/divergent_2d_init_tracer3.png +:align: center +:width: 500 px +``` + +The velocity is + +$$ +u(t) = \frac{u_0 R}{\tau} \sin^2(\frac{\lambda}{2} - \frac{\pi t}{\tau}) \sin(2\theta) \cos^2(\theta) \cos(\frac{\pi t}{\tau}) + \frac{2 \pi R}{\tau}\cos(\theta) +$$ + +$$ +v(t) = \frac{u_0 R}{2 \tau} \sin(\lambda - \frac{2 \pi t}{\tau}) \cos^3(\theta) \cos(\frac{\pi t}{\tau}) +$$ + +Where $\lambda$ is longitude, $\theta$ is latitude and $R$ is the radius of the +sphere. $\tau$ is the time it takes to transit the equator. The default is 12 +days and is given by the cfg option `vel_pd`. The default velocity amplitude, +$u_0$ is 10 m/s and is given by the cfg option `vel_amp`. + +Temperature and salinity are not evolved in this task and are given +constant values determined by config options `temperature` and `salinity`. + +The Coriolis parameters `fCell`, `fEdge`, and `fVertex` do not need to be +specified for a global mesh and are initialized as zeros. + +## forcing + +This case is forced to follow $u(t)$ and $v(t)$ given above. + +## time step and run duration + +This task uses the Runge-Kutta 4th-order (RK4) time integrator. The time step +for forward integration is determined by multiplying the resolution by a config +option, `rk4_dt_per_km`, so that coarser meshes have longer time steps. You can +alter this before setup (in a user config file) or before running the task (in +the config file in the work directory). + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# time integrator: {'split_explicit', 'RK4'} +time_integrator = RK4 + +# RK4 time step per resolution (s/km), since dt is proportional to resolution +rk4_dt_per_km = 3.0 +``` + +The `convergence_eval_time`, `run_duration` and `output_interval` are the +period for advection to make a full rotation around the globe, 12 days: + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# Run duration in days +run_duration = ${sphere_transport:vel_pd} + +# Output interval in days +output_interval = ${sphere_transport:vel_pd} +``` + +Here, `${sphere_transport:vel_pd}` means that the same value is used as in the +option `vel_pd` in section `[sphere_transport]`, see below. + +## config options + +The `divergent_2d` config options include: + +```cfg +# options for all sphere transport test cases +[sphere_transport] + +# temperature +temperature = 15. + +# salinity +salinity = 35. + +# time (days) for bell to transit equator once +vel_pd = 12.0 + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz_tracer] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_tracer_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + + +# options for thickness visualization for the sphere transport test case +[sphere_transport_viz_h] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 99., 'vmax': 101.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_h_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + +# options for divergent 2-d test case +[divergent_2d] + +# velocity amplitude in meters per second +vel_amp = 5. + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.48 +convergence_thresh_tracer2 = 1.86 +convergence_thresh_tracer3 = 0.4 +``` + +The options in section `sphere_transport` are used by all 4 test cases based +on Lauritzen et al. (2012) and control the initial condition. The options in +section `divergent_2d` control the initial condition of that case +only and the convergence rate threshold. + +The options in sections `sphere_transport_viz*` control properties of the `viz` +step of the test case. + +The default options for the convergence analysis step can be changed here: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# Evaluation time for convergence analysis (in days) +convergence_eval_time = ${sphere_transport:vel_pd} + +# Type of error to compute +error_type = l2 +``` + +## cores + +The target and minimum number of cores are determined by `goal_cells_per_core` +and `max_cells_per_core` from the `ocean` section of the config file, +respectively. This ensures that the number of cells per core is roughly +constant across the different resolutions in the convergence study. diff --git a/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_convergence.png b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_convergence.png new file mode 100644 index 000000000..e410687f2 Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_convergence.png differ diff --git a/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer1.png b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer1.png new file mode 100644 index 000000000..acfece6ed Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer1.png differ diff --git a/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer2.png b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer2.png new file mode 100644 index 000000000..e1e8b157c Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer2.png differ diff --git a/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer3.png b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer3.png new file mode 100644 index 000000000..c0c098b84 Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_init_tracer3.png differ diff --git a/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_mixing.png b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_mixing.png new file mode 100644 index 000000000..c2a24582f Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/correlated_tracers_2d_mixing.png differ diff --git a/docs/users_guide/ocean/tasks/images/divergent_2d_convergence.png b/docs/users_guide/ocean/tasks/images/divergent_2d_convergence.png new file mode 100644 index 000000000..9e02fa489 Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/divergent_2d_convergence.png differ diff --git a/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer1.png b/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer1.png new file mode 100644 index 000000000..acfece6ed Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer1.png differ diff --git a/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer2.png b/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer2.png new file mode 100644 index 000000000..e1e8b157c Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer2.png differ diff --git a/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer3.png b/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer3.png new file mode 100644 index 000000000..179f42ede Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/divergent_2d_init_tracer3.png differ diff --git a/docs/users_guide/ocean/tasks/images/nondivergent_2d_convergence.png b/docs/users_guide/ocean/tasks/images/nondivergent_2d_convergence.png new file mode 100644 index 000000000..e410687f2 Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/nondivergent_2d_convergence.png differ diff --git a/docs/users_guide/ocean/tasks/images/nondivergent_2d_filament.png b/docs/users_guide/ocean/tasks/images/nondivergent_2d_filament.png new file mode 100644 index 000000000..b30297f98 Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/nondivergent_2d_filament.png differ diff --git a/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer1.png b/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer1.png new file mode 100644 index 000000000..acfece6ed Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer1.png differ diff --git a/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer2.png b/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer2.png new file mode 100644 index 000000000..e1e8b157c Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer2.png differ diff --git a/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer3.png b/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer3.png new file mode 100644 index 000000000..179f42ede Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/nondivergent_2d_init_tracer3.png differ diff --git a/docs/users_guide/ocean/tasks/images/rotation_2d_convergence.png b/docs/users_guide/ocean/tasks/images/rotation_2d_convergence.png new file mode 100644 index 000000000..6733f7962 Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/rotation_2d_convergence.png differ diff --git a/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer1.png b/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer1.png new file mode 100644 index 000000000..acfece6ed Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer1.png differ diff --git a/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer2.png b/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer2.png new file mode 100644 index 000000000..e1e8b157c Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer2.png differ diff --git a/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer3.png b/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer3.png new file mode 100644 index 000000000..179f42ede Binary files /dev/null and b/docs/users_guide/ocean/tasks/images/rotation_2d_init_tracer3.png differ diff --git a/docs/users_guide/ocean/tasks/index.md b/docs/users_guide/ocean/tasks/index.md index 49c1cac45..ff7993372 100644 --- a/docs/users_guide/ocean/tasks/index.md +++ b/docs/users_guide/ocean/tasks/index.md @@ -6,9 +6,13 @@ :titlesonly: true baroclinic_channel +correlated_tracers_2d cosine_bell geostrophic +divergent_2d inertial_gravity_wave manufactured_solution +nondivergent_2d +rotation_2d single_column ``` diff --git a/docs/users_guide/ocean/tasks/nondivergent_2d.md b/docs/users_guide/ocean/tasks/nondivergent_2d.md new file mode 100644 index 000000000..15d1ef71e --- /dev/null +++ b/docs/users_guide/ocean/tasks/nondivergent_2d.md @@ -0,0 +1,344 @@ +(ocean-nondivergent-2d)= + +# nondivergent 2-d + +## description + +The `nondivergent_2d` and `nondivergent_2d/with_viz` tasks implement the +non-divergent flow field test of (1) numerical order of convergence, (2) +filament preservation and (3) rough distribution as described in +[Lauritzen et al. 2012](). +The reversing deformational flow in this test case explores large-scale to small-scale transport. + +The numerical order of convergence is analyzed in the `analysis` step and +produces a figure similar to the following showing L2 error norm as a function +of horizontal resolution: + +```{image} images/nondivergent_2d_convergence.png +:align: center +:width: 500 px +``` + +The ability of the horizontal advection scheme to preserve filaments is +addressed in the `filament_analysis` step. It produces a figure similar to the +following, which provides a comparison across resolutions, similar to Figure 6 +from Lauritzen et al. 2012: + +```{image} images/nondivergent_2d_filament.png +:align: center +:width: 500 px +``` + +The ability of the horizontal advection scheme to preserve rough distributions +is addressed in the `viz` steps for each resolution of the +`nondivergent_2d/with_viz` test. These steps produce visualizations of three +debug tracers at initial time, thhe mid-point and final time. The rough +distributions are represented as slotted cylinders in the `tracer3` +field. + +## mesh + +Two global mesh variants are tested, quasi-uniform (QU) and icosohydral. Thus, +there are 4 variants of the task: +``` +ocean/spherical/icos/nondivergent_2d +ocean/spherical/icos/nondivergent_2d/with_viz +ocean/spherical/qu/nondivergent_2d +ocean/spherical/qu/nondivergent_2d/with_viz +``` +The default resolutions used in the task depends on the mesh type. + +For the `icos` mesh type, the defaults are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of icosahedral mesh resolutions (km) to test +icos_resolutions = 60, 120, 240, 480 +``` + +for the `qu` mesh type, they are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of quasi-uniform mesh resolutions (km) to test +qu_resolutions = 60, 90, 120, 150, 180, 210, 240 +``` + +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 +before setting up `nondivergent_2d`, 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` +meshes, make sure you use a resolution close to those listed in +{ref}`dev-spherical-meshes`. Each resolution will be rounded to the nearest +allowed icosahedral resolution. + +The `base_mesh` steps are shared with other tasks so they are not housed in +the `nondivergent_2d` work directory. Instead, they are in work directories +like: + +``` +ocean/spherical/icos/base_mesh/60km +ocean/spherical/qu/base_mesh/60km +``` + +For convenience, there are symlinks inside of the `nondivergent_2d` and +`nondivergent_2d/with_viz` work directories, e.g.: +``` +ocean/spherical/icos/nondivergent_2d/base_mesh/60km +ocean/spherical/qu/nondivergent_2d/base_mesh/60km +ocean/spherical/icos/nondivergent_2d/with_viz/base_mesh/60km +ocean/spherical/qu/nondivergent_2d/with_viz/base_mesh/60km +``` + +## vertical grid + +This task only exercises the shallow water dynamics. As such, a single +vertical level may be used. The bottom depth is constant and the +results should be insensitive to the choice of `bottom_depth`. + +```cfg +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 3 + +# Depth of the bottom of the ocean +bottom_depth = 300.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-level + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = None + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 +``` + +## initial conditions + +The initial condition is characterized by three separate tracer distributions +stored in three `debugTracers`: + +- `tracer1`: A c-infinity function used for convergence analysis +- `tracer2`: A pair of c-2 cosine bells +- `tracer3`: A discontinuous pair of slotted cylinders + +```{image} images/nondivergent_2d_init_tracer1.png +:align: center +:width: 500 px +``` + +```{image} images/nondivergent_2d_init_tracer2.png +:align: center +:width: 500 px +``` + +```{image} images/nondivergent_2d_init_tracer3.png +:align: center +:width: 500 px +``` + +The velocity is + +$$ +u(t) = \frac{u_0 R}{\tau} \sin^2(\lambda - \frac{2 \pi t}{\tau}) \sin(2\theta) \cos(\frac{\pi t}{\tau}) + \frac{2 \pi R}{\tau}\cos(\theta) +$$ + +$$ +v(t) = \frac{u_0 R}{\tau} \sin(2\lambda - \frac{4 \pi t}{\tau}) \cos(\theta) \cos(\frac{\pi t}{\tau}) +$$ + +Where $\lambda$ is longitude, $\theta$ is latitude and $R$ is the radius of the +sphere. $\tau$ is the time it takes to transit the equator. The default is 12 +days and is given by the cfg option `vel_pd`. The default velocity amplitude, +$u_0$ is 10 m/s and is given by the cfg option `vel_amp`. + +Temperature and salinity are not evolved in this task and are given +constant values determined by config options `temperature` and `salinity`. + +The Coriolis parameters `fCell`, `fEdge`, and `fVertex` do not need to be +specified for a global mesh and are initialized as zeros. + +## forcing + +This case is forced to follow $u(t)$ and $v(t)$ given above. + +## time step and run duration + +This task uses the Runge-Kutta 4th-order (RK4) time integrator. The time step +for forward integration is determined by multiplying the resolution by a config +option, `rk4_dt_per_km`, so that coarser meshes have longer time steps. You can +alter this before setup (in a user config file) or before running the task (in +the config file in the work directory). + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# time integrator: {'split_explicit', 'RK4'} +time_integrator = RK4 + +# RK4 time step per resolution (s/km), since dt is proportional to resolution +rk4_dt_per_km = 3.0 +``` + +The `convergence_eval_time`, `run_duration` and `output_interval` are the +period for advection to make a full rotation around the globe, 12 days: + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# Run duration in days +run_duration = ${sphere_transport:vel_pd} + +# Output interval in days +output_interval = ${sphere_transport:vel_pd} +``` + +Here, `${sphere_transport:vel_pd}` means that the same value is used as in the +option `vel_pd` in section `[sphere_transport]`, see below. + +## config options + +The `nondivergent_2d` config options include: + +```cfg +# options for all sphere transport test cases +[sphere_transport] + +# temperature +temperature = 15. + +# salinity +salinity = 35. + +# time (days) for bell to transit equator once +vel_pd = 12.0 + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz_tracer] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_tracer_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + + +# options for thickness visualization for the sphere transport test case +[sphere_transport_viz_h] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 99., 'vmax': 101.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_h_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + +# options for nondivergent 2-d test case +[nondivergent_2d] + +# velocity amplitude in meters per second +vel_amp = 10. + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.5 +convergence_thresh_tracer2 = 1.3 +convergence_thresh_tracer3 = 0.3 + +# time in days at which to evaluate filament preservation +filament_evaluation_time = 6.0 +``` + +The options in section `sphere_transport` are used by all 4 test cases based +on Lauritzen et al. (2012) and control the initial condition. The options in +section `nondivergent_2d` control the initial condition of that case +only, the convergence rate threshold, and the time at which to evaluate +filament preservation. + +The options in sections `sphere_transport_viz*` control properties of the `viz` +step of the test case. + +The default options for the convergence analysis step can be changed here: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# Evaluation time for convergence analysis (in days) +convergence_eval_time = ${sphere_transport:vel_pd} + +# Type of error to compute +error_type = l2 +``` + +## cores + +The target and minimum number of cores are determined by `goal_cells_per_core` +and `max_cells_per_core` from the `ocean` section of the config file, +respectively. This ensures that the number of cells per core is roughly +constant across the different resolutions in the convergence study. diff --git a/docs/users_guide/ocean/tasks/rotation_2d.md b/docs/users_guide/ocean/tasks/rotation_2d.md new file mode 100644 index 000000000..4fd90417e --- /dev/null +++ b/docs/users_guide/ocean/tasks/rotation_2d.md @@ -0,0 +1,310 @@ +(ocean-rotation-2d)= + +# rotation 2-d + +## description + +The `rotation_2d` and `rotation_2d/with_viz` tasks implement the +rotational flow field test of numerical order of convergence. +This test is similar to `cosine_bell` except the axis of rotation is a config +option and can be offset from the z-axis. + +The numerical order of convergence is analyzed in the `analysis` step and +produces a figure similar to the following showing L2 error norm as a function +of horizontal resolution: + +```{image} images/rotation_2d_convergence.png +:align: center +:width: 500 px +``` + +## mesh + +Two global mesh variants are tested, quasi-uniform (QU) and icosohydral. Thus, +there are 4 variants of the task: +``` +ocean/spherical/icos/rotation_2d +ocean/spherical/icos/rotation_2d/with_viz +ocean/spherical/qu/rotation_2d +ocean/spherical/qu/rotation_2d/with_viz +``` +The default resolutions used in the task depends on the mesh type. + +For the `icos` mesh type, the defaults are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of icosahedral mesh resolutions (km) to test +icos_resolutions = 60, 120, 240, 480 +``` + +for the `qu` mesh type, they are: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# a list of quasi-uniform mesh resolutions (km) to test +qu_resolutions = 60, 90, 120, 150, 180, 210, 240 +``` + +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 +before setting up `rotation_2d`, 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` +meshes, make sure you use a resolution close to those listed in +{ref}`dev-spherical-meshes`. Each resolution will be rounded to the nearest +allowed icosahedral resolution. + +The `base_mesh` steps are shared with other tasks so they are not housed in +the `rotation_2d` work directory. Instead, they are in work directories +like: + +``` +ocean/spherical/icos/base_mesh/60km +ocean/spherical/qu/base_mesh/60km +``` + +For convenience, there are symlinks inside of the `rotation_2d` and +`rotation_2d/with_viz` work directories, e.g.: +``` +ocean/spherical/icos/rotation_2d/base_mesh/60km +ocean/spherical/qu/rotation_2d/base_mesh/60km +ocean/spherical/icos/rotation_2d/with_viz/base_mesh/60km +ocean/spherical/qu/rotation_2d/with_viz/base_mesh/60km +``` + +## vertical grid + +This task only exercises the shallow water dynamics. As such, a single +vertical level may be used. The bottom depth is constant and the +results should be insensitive to the choice of `bottom_depth`. + +```cfg +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 3 + +# Depth of the bottom of the ocean +bottom_depth = 300.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-level + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = None + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 +``` + +## initial conditions + +The initial condition is characterized by three separate tracer distributions +stored in three `debugTracers`: + +- `tracer1`: A c-infinity function used for convergence analysis +- `tracer2`: A pair of c-2 cosine bells +- `tracer3`: A discontinuous pair of slotted cylinders + +```{image} images/rotation_2d_init_tracer1.png +:align: center +:width: 500 px +``` + +```{image} images/rotation_2d_init_tracer2.png +:align: center +:width: 500 px +``` + +```{image} images/rotation_2d_init_tracer3.png +:align: center +:width: 500 px +``` + +The velocity is that of rigid rotation about an axis offset from the z-axis of +the sphere. It is not given in Lauritzen et al. The axis of rotation is defined by a vector given by the cfg option `rotation_vector`. + +Temperature and salinity are not evolved in this task and are given +constant values determined by config options `temperature` and `salinity`. + +The Coriolis parameters `fCell`, `fEdge`, and `fVertex` do not need to be +specified for a global mesh and are initialized as zeros. + +## forcing + +This flow velocity case is forced to follow the constant rotation rate given +in the config options. + +## time step and run duration + +This task uses the Runge-Kutta 4th-order (RK4) time integrator. The time step +for forward integration is determined by multiplying the resolution by a config +option, `rk4_dt_per_km`, so that coarser meshes have longer time steps. You can +alter this before setup (in a user config file) or before running the task (in +the config file in the work directory). + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# time integrator: {'split_explicit', 'RK4'} +time_integrator = RK4 + +# RK4 time step per resolution (s/km), since dt is proportional to resolution +rk4_dt_per_km = 3.0 +``` + +The `convergence_eval_time`, `run_duration` and `output_interval` are the +period for advection to make a full rotation around the globe, 12 days: + +```cfg +# config options for spherical convergence tests +[spherical_convergence_forward] + +# Run duration in days +run_duration = ${sphere_transport:vel_pd} + +# Output interval in days +output_interval = ${sphere_transport:vel_pd} +``` + +Here, `${sphere_transport:vel_pd}` means that the same value is used as in the +option `vel_pd` in section `[sphere_transport]`, see below. + +## config options + +The `rotation_2d` config options include: + +```cfg +# options for all sphere transport test cases +[sphere_transport] + +# temperature +temperature = 15. + +# salinity +salinity = 35. + +# time (days) for bell to transit equator once +vel_pd = 12.0 + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz_tracer] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_tracer_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + + +# options for thickness visualization for the sphere transport test case +[sphere_transport_viz_h] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 99., 'vmax': 101.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_h_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + +# options for rotation 2-d test case +[rotation_2d] + +# rotation vector in cartesian coordinates +rotation_vector = 0.2, 0.7, 1.0 + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.4 +convergence_thresh_tracer2 = 1.8 +convergence_thresh_tracer3 = 0.4 +``` + +The options in section `sphere_transport` are used by all 4 test cases based +on Lauritzen et al. (2012) and control the initial condition. The options in +section `rotation_2d` control the convergence rate threshold. + +The options in sections `sphere_transport_viz*` control properties of the `viz` +step of the test case. + +The default options for the convergence analysis step can be changed here: + +```cfg +# config options for spherical convergence tests +[spherical_convergence] + +# Evaluation time for convergence analysis (in days) +convergence_eval_time = ${sphere_transport:vel_pd} + +# Type of error to compute +error_type = l2 +``` + +## cores + +The target and minimum number of cores are determined by `goal_cells_per_core` +and `max_cells_per_core` from the `ocean` section of the config file, +respectively. This ensures that the number of cells per core is roughly +constant across the different resolutions in the convergence study. diff --git a/polaris/ocean/__init__.py b/polaris/ocean/__init__.py index 6de0e5811..90d5f07e5 100644 --- a/polaris/ocean/__init__.py +++ b/polaris/ocean/__init__.py @@ -9,6 +9,7 @@ add_manufactured_solution_tasks, ) from polaris.ocean.tasks.single_column import add_single_column_tasks +from polaris.ocean.tasks.sphere_transport import add_sphere_transport_tasks class Ocean(Component): @@ -29,6 +30,7 @@ def __init__(self): # single column add_single_column_tasks(component=self) + add_sphere_transport_tasks(component=self) # spherical: please keep these in alphabetical order add_cosine_bell_tasks(component=self) diff --git a/polaris/ocean/suites/convergence.txt b/polaris/ocean/suites/convergence.txt index 97a07c5e7..56a8e63c7 100644 --- a/polaris/ocean/suites/convergence.txt +++ b/polaris/ocean/suites/convergence.txt @@ -1,5 +1,10 @@ ocean/planar/inertial_gravity_wave ocean/planar/manufactured_solution +ocean/spherical/icos/correlated_tracers_2d + cached: icos_base_mesh_60km icos_base_mesh_120km icos_base_mesh_240km icos_base_mesh_480km +ocean/spherical/qu/correlated_tracers_2d + cached: qu_base_mesh_60km qu_base_mesh_90km qu_base_mesh_120km qu_base_mesh_150km + cached: qu_base_mesh_180km qu_base_mesh_210km qu_base_mesh_240km 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 @@ -16,3 +21,18 @@ ocean/spherical/qu/geostrophic 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 +ocean/spherical/icos/divergent_2d + cached: icos_base_mesh_60km icos_base_mesh_120km icos_base_mesh_240km icos_base_mesh_480km +ocean/spherical/qu/divergent_2d + cached: qu_base_mesh_60km qu_base_mesh_90km qu_base_mesh_120km qu_base_mesh_150km + cached: qu_base_mesh_180km qu_base_mesh_210km qu_base_mesh_240km +ocean/spherical/icos/nondivergent_2d + cached: icos_base_mesh_60km icos_base_mesh_120km icos_base_mesh_240km icos_base_mesh_480km +ocean/spherical/qu/nondivergent_2d + cached: qu_base_mesh_60km qu_base_mesh_90km qu_base_mesh_120km qu_base_mesh_150km + cached: qu_base_mesh_180km qu_base_mesh_210km qu_base_mesh_240km +ocean/spherical/icos/rotation_2d + cached: icos_base_mesh_60km icos_base_mesh_120km icos_base_mesh_240km icos_base_mesh_480km +ocean/spherical/qu/rotation_2d + cached: qu_base_mesh_60km qu_base_mesh_90km qu_base_mesh_120km qu_base_mesh_150km + cached: qu_base_mesh_180km qu_base_mesh_210km qu_base_mesh_240km diff --git a/polaris/ocean/suites/sphere_transport.txt b/polaris/ocean/suites/sphere_transport.txt new file mode 100644 index 000000000..a93d53fb8 --- /dev/null +++ b/polaris/ocean/suites/sphere_transport.txt @@ -0,0 +1,4 @@ +ocean/spherical/icos/rotation_2d +ocean/spherical/icos/nondivergent_2d +ocean/spherical/icos/divergent_2d +ocean/spherical/icos/correlated_tracers_2d diff --git a/polaris/ocean/suites/sphere_transport_with_viz.txt b/polaris/ocean/suites/sphere_transport_with_viz.txt new file mode 100644 index 000000000..b120cc1f5 --- /dev/null +++ b/polaris/ocean/suites/sphere_transport_with_viz.txt @@ -0,0 +1,4 @@ +ocean/spherical/icos/rotation_2d/with_viz +ocean/spherical/icos/nondivergent_2d/with_viz +ocean/spherical/icos/divergent_2d/with_viz +ocean/spherical/icos/correlated_tracers_2d/with_viz diff --git a/polaris/ocean/tasks/cosine_bell/init.py b/polaris/ocean/tasks/cosine_bell/init.py index 4c3b26ef9..788f0b6cf 100644 --- a/polaris/ocean/tasks/cosine_bell/init.py +++ b/polaris/ocean/tasks/cosine_bell/init.py @@ -124,5 +124,10 @@ def cosine_bell(max_value, ri, r): r : float Radius of the cosine bell in meters + + Returns + ------- + f : np.ndarray of type float + Cosine bell tracer values """ return max_value / 2.0 * (1.0 + np.cos(np.pi * np.divide(ri, r))) diff --git a/polaris/ocean/tasks/sphere_transport/__init__.py b/polaris/ocean/tasks/sphere_transport/__init__.py new file mode 100644 index 000000000..893e05ee8 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/__init__.py @@ -0,0 +1,253 @@ +from typing import Dict + +from polaris import Step, Task +from polaris.config import PolarisConfigParser +from polaris.ocean.mesh.spherical import add_spherical_base_mesh_step +from polaris.ocean.tasks.sphere_transport.analysis import Analysis +from polaris.ocean.tasks.sphere_transport.filament_analysis import ( + FilamentAnalysis, +) +from polaris.ocean.tasks.sphere_transport.forward import Forward +from polaris.ocean.tasks.sphere_transport.init import Init +from polaris.ocean.tasks.sphere_transport.mixing_analysis import MixingAnalysis +from polaris.ocean.tasks.sphere_transport.viz import Viz, VizMap + + +def add_sphere_transport_tasks(component): + """ + Add tasks that define variants of sphere transport test cases: + nondivergent_2d, divergent_2d, correlated_tracers_2d, rotation_2d + + component : polaris.ocean.Ocean + the ocean component that the tasks will be added to + """ + + for icosahedral, prefix in [(True, 'icos'), (False, 'qu')]: + for case_name in ['rotation_2d', 'nondivergent_2d', 'divergent_2d', + 'correlated_tracers_2d']: + filepath = f'spherical/{prefix}/{case_name}/{case_name}.cfg' + config = PolarisConfigParser(filepath=filepath) + config.add_from_package('polaris.ocean.convergence', + 'convergence.cfg') + config.add_from_package('polaris.ocean.convergence.spherical', + 'spherical.cfg') + package = 'polaris.ocean.tasks.sphere_transport' + config.add_from_package(package, 'sphere_transport.cfg') + config.add_from_package(package, f'{case_name}.cfg') + for include_viz in [False, True]: + component.add_task(SphereTransport( + component=component, case_name=case_name, + icosahedral=icosahedral, config=config, + include_viz=include_viz)) + + +class SphereTransport(Task): + """ + A test case for testing properties of tracer advection + + Attributes + ---------- + resolutions : list of float + A list of mesh resolutions + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW meshes + + include_viz : bool + Include VizMap and Viz steps for each resolution + """ + def __init__(self, component, config, case_name, icosahedral, include_viz): + """ + Create test case for creating a global MPAS-Ocean mesh + + Parameters + ---------- + component : polaris.ocean.Ocean + The ocean component that this task belongs to + + config : polaris.config.PolarisConfigParser + A shared config parser + + case_name: string + The name of the case which determines what variant of the + configuration to use + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW + meshes + + include_viz : bool + Include VizMap and Viz steps for each resolution + """ + if icosahedral: + prefix = 'icos' + else: + prefix = 'qu' + + subdir = f'spherical/{prefix}/{case_name}' + name = f'{prefix}_{case_name}' + if include_viz: + subdir = f'{subdir}/with_viz' + name = f'{name}_with_viz' + link = f'{case_name}.cfg' + else: + # config options live in the task already so no need for a symlink + link = None + super().__init__(component=component, name=name, subdir=subdir) + self.resolutions = list() + self.icosahedral = icosahedral + self.case_name = case_name + self.include_viz = include_viz + + self.set_shared_config(config, link=link) + + # add the steps with default resolutions so they can be listed + self._setup_steps() + + def configure(self): + """ + Set config options for the test case + """ + super().configure() + + # set up the steps again in case a user has provided new resolutions + self._setup_steps() + + def _setup_steps(self): + """ setup steps given resolutions """ + case_name = self.case_name + icosahedral = self.icosahedral + config = self.config + config_filename = self.config_filename + + if icosahedral: + prefix = 'icos' + else: + prefix = 'qu' + + resolutions = config.getlist('spherical_convergence', + f'{prefix}_resolutions', dtype=float) + + if self.resolutions == resolutions: + return + + # start fresh with no steps + for step in list(self.steps.values()): + self.remove_step(step) + + self.resolutions = resolutions + + component = self.component + + analysis_dependencies: Dict[str, Dict[str, Step]] = ( + dict(mesh=dict(), init=dict(), forward=dict())) + for resolution in resolutions: + base_mesh_step, mesh_name = add_spherical_base_mesh_step( + component, resolution, icosahedral) + self.add_step(base_mesh_step, symlink=f'base_mesh/{mesh_name}') + analysis_dependencies['mesh'][resolution] = base_mesh_step + + sph_trans_dir = f'spherical/{prefix}/{case_name}' + + name = f'{prefix}_init_{mesh_name}' + subdir = f'{sph_trans_dir}/init/{mesh_name}' + if self.include_viz: + symlink = f'init/{mesh_name}' + else: + symlink = None + if subdir in component.steps: + init_step = component.steps[subdir] + else: + init_step = Init(component=component, name=name, subdir=subdir, + base_mesh=base_mesh_step, case_name=case_name) + init_step.set_shared_config(config, link=config_filename) + self.add_step(init_step, symlink=symlink) + analysis_dependencies['init'][resolution] = init_step + + name = f'{prefix}_forward_{mesh_name}' + subdir = f'{sph_trans_dir}/forward/{mesh_name}' + if self.include_viz: + symlink = f'forward/{mesh_name}' + else: + symlink = None + if subdir in component.steps: + forward_step = component.steps[subdir] + else: + forward_step = Forward(component=component, name=name, + subdir=subdir, resolution=resolution, + base_mesh=base_mesh_step, + init=init_step, + case_name=case_name) + forward_step.set_shared_config(config, link=config_filename) + self.add_step(forward_step, symlink=symlink) + analysis_dependencies['forward'][resolution] = forward_step + + if self.include_viz: + with_viz_dir = f'{sph_trans_dir}/with_viz' + + name = f'{prefix}_map_{mesh_name}' + subdir = f'{with_viz_dir}/map/{mesh_name}' + viz_map = VizMap(component=component, name=name, + subdir=subdir, base_mesh=base_mesh_step, + mesh_name=mesh_name) + viz_map.set_shared_config(config, link=config_filename) + self.add_step(viz_map) + + name = f'{prefix}_viz_{mesh_name}' + subdir = f'{with_viz_dir}/viz/{mesh_name}' + step = Viz(component=component, name=name, + subdir=subdir, base_mesh=base_mesh_step, + init=init_step, forward=forward_step, + viz_map=viz_map, mesh_name=mesh_name) + step.set_shared_config(config, link=config_filename) + self.add_step(step) + + subdir = f'{sph_trans_dir}/analysis' + if self.include_viz: + symlink = 'analysis' + else: + symlink = None + if subdir in component.steps: + step = component.steps[subdir] + step.resolutions = resolutions + step.dependencies_dict = analysis_dependencies + else: + step = Analysis(component=component, resolutions=resolutions, + subdir=subdir, case_name=case_name, + dependencies=analysis_dependencies) + step.set_shared_config(config, link=config_filename) + self.add_step(step, symlink=symlink) + + if case_name == 'correlated_tracers_2d': + subdir = f'{sph_trans_dir}/mixing_analysis' + if self.include_viz: + symlink = 'mixing_analysis' + else: + symlink = None + if subdir in component.steps: + step = component.steps[subdir] + else: + step = MixingAnalysis(component=component, + resolutions=resolutions, + icosahedral=icosahedral, subdir=subdir, + case_name=case_name, + dependencies=analysis_dependencies) + step.set_shared_config(config, link=config_filename) + self.add_step(step, symlink=symlink) + + if case_name == 'nondivergent_2d': + subdir = f'{sph_trans_dir}/filament_analysis' + if self.include_viz: + symlink = 'filament_analysis' + else: + symlink = None + if subdir in component.steps: + step = component.steps[subdir] + else: + step = FilamentAnalysis(component=component, + resolutions=resolutions, + icosahedral=icosahedral, subdir=subdir, + case_name=case_name, + dependencies=analysis_dependencies) + step.set_shared_config(config, link=config_filename) + self.add_step(step, symlink=symlink) diff --git a/polaris/ocean/tasks/sphere_transport/analysis.py b/polaris/ocean/tasks/sphere_transport/analysis.py new file mode 100644 index 000000000..620a80651 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/analysis.py @@ -0,0 +1,79 @@ +from polaris.ocean.convergence import ConvergenceAnalysis + + +class Analysis(ConvergenceAnalysis): + """ + A step for analyzing the output from sphere transport test cases + + Attributes + ---------- + resolutions : list of float + The resolutions of the meshes that have been run + + case_name : str + The name of the test case + """ + def __init__(self, component, resolutions, subdir, case_name, + dependencies): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + resolutions : list of float + The resolutions of the meshes that have been run + + subdir : str + The subdirectory that the step resides in + + case_name: str + The name of the test case + + dependencies : dict of dict of polaris.Steps + The dependencies of this step + """ + self.case_name = case_name + convergence_vars = [{'name': 'tracer1', + 'title': 'tracer1', + 'zidx': 1}, + {'name': 'tracer2', + 'title': 'tracer2', + 'zidx': 1}, + {'name': 'tracer3', + 'title': 'tracer3', + 'zidx': 1}] + super().__init__(component=component, subdir=subdir, + resolutions=resolutions, + dependencies=dependencies, + convergence_vars=convergence_vars) + # Note: there is no need to overwrite the default method exact_solution + # which uses the initial condition + + def convergence_parameters(self, field_name=None): + """ + Get convergence parameters + + Parameters + ---------- + field_name : str + The name of the variable of which we evaluate convergence + For cosine_bell, we use the same convergence rate for all fields + Returns + ------- + conv_thresh: float + The minimum convergence rate + + conv_thresh: float + The maximum convergence rate + """ + config = self.config + section = config[self.case_name] + conv_thresh = section.getfloat(f'convergence_thresh_{field_name}') + + section = config['convergence'] + error_type = section.get('error_type') + + return conv_thresh, error_type diff --git a/polaris/ocean/tasks/sphere_transport/correlated_tracers_2d.cfg b/polaris/ocean/tasks/sphere_transport/correlated_tracers_2d.cfg new file mode 100644 index 000000000..35679737a --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/correlated_tracers_2d.cfg @@ -0,0 +1,17 @@ +# options for correlated_tracers_2d test case +[correlated_tracers_2d] + +# velocity amplitude in meters per second +vel_amp = 10. + +# Quadratic coefficients for the correlated tracer function in decreasing +# order of the terms +correlation_coefficients = -0.8, 0.0, 0.9 + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.5 +convergence_thresh_tracer2 = 1.3 +convergence_thresh_tracer3 = 1.0 + +# time in days at which to evaluate mixing +mixing_evaluation_time = 6.0 diff --git a/polaris/ocean/tasks/sphere_transport/divergent_2d.cfg b/polaris/ocean/tasks/sphere_transport/divergent_2d.cfg new file mode 100644 index 000000000..e442e313a --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/divergent_2d.cfg @@ -0,0 +1,10 @@ +# options for divergent_2d test case +[divergent_2d] + +# velocity amplitude in meters per second +vel_amp = 5. + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.8 +convergence_thresh_tracer2 = 1.5 +convergence_thresh_tracer3 = 0.3 diff --git a/polaris/ocean/tasks/sphere_transport/filament_analysis.py b/polaris/ocean/tasks/sphere_transport/filament_analysis.py new file mode 100644 index 000000000..a3f196439 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/filament_analysis.py @@ -0,0 +1,123 @@ +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import xarray as xr + +from polaris import Step +from polaris.mpas import time_index_from_xtime +from polaris.ocean.resolution import resolution_to_subdir +from polaris.viz import use_mplstyle + + +class FilamentAnalysis(Step): + """ + A step for analyzing the output from sphere transport test cases + + Attributes + ---------- + resolutions : list of float + The resolutions of the meshes that have been run + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW + meshes + + case_name : str + The name of the test case + """ + def __init__(self, component, resolutions, icosahedral, subdir, + case_name, dependencies): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + resolutions : list of float + The resolutions of the meshes that have been run + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW + meshes + + subdir : str + The subdirectory that the step resides in + + case_name: str + The name of the test case + + dependencies : dict of dict of polaris.Steps + The dependencies of this step + """ + super().__init__(component=component, name='filament_analysis', + subdir=subdir) + self.resolutions = resolutions + self.case_name = case_name + + for resolution in resolutions: + mesh_name = resolution_to_subdir(resolution) + base_mesh = dependencies['mesh'][resolution] + init = dependencies['init'][resolution] + forward = dependencies['forward'][resolution] + self.add_input_file( + filename=f'{mesh_name}_mesh.nc', + work_dir_target=f'{base_mesh.path}/base_mesh.nc') + self.add_input_file( + filename=f'{mesh_name}_init.nc', + work_dir_target=f'{init.path}/initial_state.nc') + self.add_input_file( + filename=f'{mesh_name}_output.nc', + work_dir_target=f'{forward.path}/output.nc') + self.add_output_file('filament.png') + + def run(self): + """ + Run this step of the test case + """ + plt.switch_backend('Agg') + resolutions = self.resolutions + config = self.config + section = config[self.case_name] + eval_time = section.getfloat('filament_evaluation_time') + s_per_day = 86400.0 + zidx = 1 + variable_name = 'tracer2' + num_tau = 21 + filament_tau = np.linspace(0, 1, num_tau) + filament_norm = np.zeros((len(resolutions), num_tau)) + use_mplstyle() + fig, ax = plt.subplots() + for i, resolution in enumerate(resolutions): + mesh_name = resolution_to_subdir(resolution) + ds = xr.open_dataset(f'{mesh_name}_output.nc') + tidx = time_index_from_xtime(ds.xtime.values, + eval_time * s_per_day) + tracer = ds[variable_name] + area_cell = ds["areaCell"] + for j, tau in enumerate(filament_tau): + cells_above_tau = tracer[tidx, :, zidx] >= tau + cells_above_tau0 = tracer[0, :, zidx] >= tau + if np.sum(cells_above_tau0 * area_cell) == 0.: + filament_norm[i, j] = np.nan + else: + filament_norm[i, j] = np.divide( + np.sum(area_cell * cells_above_tau), + np.sum(cells_above_tau0 * area_cell)) + plt.plot(filament_tau, filament_norm[i, :], '.-', label=mesh_name) + plt.plot([filament_tau[0], filament_tau[-1]], [1., 1.], 'k--') + ax.set_xlim([filament_tau[0], filament_tau[-1]]) + ax.set_xlabel(r'$\tau$') + ax.set_ylabel(r'$l_f$') + plt.title(f'Filament preservation diagnostic for {variable_name}') + plt.legend() + fig.savefig('filament.png', bbox_inches='tight') + + res_array = np.array(resolutions, dtype=float) + data = np.column_stack((res_array, filament_norm)) + col_headers = ['resolution'] + for tau in filament_tau: + col_headers.append(f'{tau:g}') + df = pd.DataFrame(data, columns=col_headers) + df.to_csv('filament.csv', index=False) diff --git a/polaris/ocean/tasks/sphere_transport/forward.py b/polaris/ocean/tasks/sphere_transport/forward.py new file mode 100644 index 000000000..98007a458 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/forward.py @@ -0,0 +1,52 @@ +from polaris.ocean.convergence.spherical import SphericalConvergenceForward + + +class Forward(SphericalConvergenceForward): + """ + A step for performing forward ocean component runs as part of the sphere + transport test case + """ + + def __init__(self, component, name, subdir, resolution, base_mesh, init, + case_name): + """ + Create a new step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + name : str + The name of the step + + subdir : str + The subdirectory for the step + + resolution : float + The resolution of the (uniform) mesh in km + + base_mesh : polaris.Step + The base mesh step + + init : polaris.Step + The init step + + case_name: str + The name of the test case + """ + package = 'polaris.ocean.tasks.sphere_transport' + flow_id = {'rotation_2d': 1, + 'nondivergent_2d': 2, + 'divergent_2d': 3, + 'correlated_tracers_2d': 4} + namelist_options = dict( + config_transport_tests_flow_id=flow_id[case_name]) + validate_vars = ['normalVelocity', 'tracer1', 'tracer2', 'tracer3'] + super().__init__(component=component, name=name, subdir=subdir, + resolution=resolution, mesh=base_mesh, + init=init, package=package, + yaml_filename='forward.yaml', + output_filename='output.nc', + validate_vars=validate_vars, + options=namelist_options) diff --git a/polaris/ocean/tasks/sphere_transport/forward.yaml b/polaris/ocean/tasks/sphere_transport/forward.yaml new file mode 100644 index 000000000..c870f49aa --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/forward.yaml @@ -0,0 +1,56 @@ +omega: + run_modes: + config_ocean_run_mode: forward + time_management: + config_run_duration: {{ run_duration }} + decomposition: + config_block_decomp_file_prefix: graph.info.part. + advection: + config_vert_coord_movement: impermeable_interfaces + time_integration: + config_dt: {{ dt }} + config_time_integrator: {{ time_integrator }} + debug: + config_disable_thick_sflux: true + config_disable_vel_all_tend: true + config_disable_vel_coriolis: true + config_disable_vel_pgrad: true + config_disable_vel_hmix: true + config_disable_vel_surface_stress: true + config_disable_vel_explicit_bottom_drag: true + config_disable_vel_vmix: true + config_disable_vel_vadv: true + config_disable_tr_hmix: true + config_disable_tr_vmix: true + config_disable_tr_sflux: true + config_disable_tr_nonlocalflux: true + config_check_ssh_consistency: false + cvmix: + config_use_cvmix: false + eos: + config_eos_type: linear + tracer_forcing_debugTracers: + config_use_debugTracers: true + streams: + mesh: + filename_template: init.nc + input: + filename_template: init.nc + restart: + output_interval: 0030_00:00:00 + output: + type: output + filename_template: output.nc + output_interval: {{ output_interval }} + clobber_mode: truncate + reference_time: 0001-01-01_00:00:00 + contents: + - tracers + - mesh + - xtime + - normalVelocity + - layerThickness + - refZMid + - refLayerThickness + - kineticEnergyCell + - relativeVorticityCell diff --git a/polaris/ocean/tasks/sphere_transport/init.py b/polaris/ocean/tasks/sphere_transport/init.py new file mode 100644 index 000000000..815921f6c --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/init.py @@ -0,0 +1,159 @@ +import numpy as np +import xarray as xr +from mpas_tools.io import write_netcdf + +from polaris import Step +from polaris.ocean.tasks.sphere_transport.resources.flow_types import ( + flow_divergent, + flow_nondivergent, + flow_rotation, +) +from polaris.ocean.tasks.sphere_transport.resources.tracer_distributions import ( # noqa: E501 + correlation_fn, + cosine_bells, + slotted_cylinders, + xyztrig, +) +from polaris.ocean.vertical import init_vertical_coord + + +class Init(Step): + """ + A step for an initial condition for for the cosine bell test case + """ + def __init__(self, component, name, subdir, base_mesh, case_name): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + name : str + The name of the step + + subdir : str + The subdirectory for the step + + base_mesh : polaris.Step + The base mesh step + + case_name: str + The name of the test case + """ + super().__init__(component=component, name=name, subdir=subdir) + + self.case_name = case_name + self.add_input_file( + filename='mesh.nc', + work_dir_target=f'{base_mesh.path}/base_mesh.nc') + + self.add_input_file( + filename='graph.info', + work_dir_target=f'{base_mesh.path}/graph.info') + + self.add_output_file(filename='initial_state.nc') + + def run(self): + """ + Run this step of the task + """ + config = self.config + case_name = self.case_name + + section = config['sphere_transport'] + temperature = section.getfloat('temperature') + salinity = section.getfloat('salinity') + vel_pd = section.getfloat('vel_pd') + + section = config['vertical_grid'] + bottom_depth = section.getfloat('bottom_depth') + + ds_mesh = xr.open_dataset('mesh.nc') + angleEdge = ds_mesh.angleEdge + latCell = ds_mesh.latCell + latEdge = ds_mesh.latEdge + lonCell = ds_mesh.lonCell + lonEdge = ds_mesh.lonEdge + sphere_radius = ds_mesh.sphere_radius + + ds = ds_mesh.copy() + + ds['bottomDepth'] = bottom_depth * xr.ones_like(latCell) + ds['ssh'] = xr.zeros_like(latCell) + + init_vertical_coord(config, ds) + + temperature_array = temperature * xr.ones_like(latCell) + temperature_array, _ = xr.broadcast(temperature_array, ds.refZMid) + ds['temperature'] = temperature_array.expand_dims(dim='Time', axis=0) + ds['salinity'] = salinity * xr.ones_like(ds.temperature) + + # tracer1 + tracer1 = xyztrig(lonCell, latCell, sphere_radius) + + # tracer2 + section = config['sphere_transport'] + radius = section.getfloat('cosine_bells_radius') + background_value = section.getfloat('cosine_bells_background') + amplitude = section.getfloat('cosine_bells_amplitude') + tracer2 = cosine_bells(lonCell, latCell, radius, background_value, + amplitude, sphere_radius) + + # tracer3 + if case_name == 'correlated_tracers_2d': + coeff = config.getlist(case_name, 'correlation_coefficients', + dtype=float) + tracer3 = correlation_fn(tracer2, coeff[0], coeff[1], coeff[2]) + else: + section = config['sphere_transport'] + radius = section.getfloat('slotted_cylinders_radius') + background_value = section.getfloat('slotted_cylinders_background') + amplitude = section.getfloat('slotted_cylinders_amplitude') + tracer3 = slotted_cylinders(lonCell, latCell, radius, + background_value, amplitude, + sphere_radius) + _, tracer1_array = np.meshgrid(ds.refZMid.values, tracer1) + _, tracer2_array = np.meshgrid(ds.refZMid.values, tracer2) + _, tracer3_array = np.meshgrid(ds.refZMid.values, tracer3) + + ds['tracer1'] = (('nCells', 'nVertLevels',), tracer1_array) + ds['tracer1'] = ds.tracer1.expand_dims(dim='Time', axis=0) + ds['tracer2'] = (('nCells', 'nVertLevels',), tracer2_array) + ds['tracer2'] = ds.tracer2.expand_dims(dim='Time', axis=0) + ds['tracer3'] = (('nCells', 'nVertLevels',), tracer3_array) + ds['tracer3'] = ds.tracer3.expand_dims(dim='Time', axis=0) + + # Initialize velocity + s_per_hour = 3600. + if case_name == 'rotation_2d': + rotation_vector = config.getlist(case_name, 'rotation_vector', + dtype=float) + vector = np.array(rotation_vector) + u, v = flow_rotation(lonEdge, latEdge, vector, + vel_pd * s_per_hour, sphere_radius) + elif case_name == 'divergent_2d': + section = config[case_name] + vel_amp = section.getfloat('vel_amp') + u, v = flow_divergent(0., lonEdge, latEdge, + vel_amp, vel_pd * s_per_hour) + elif (case_name == 'nondivergent_2d' or + case_name == 'correlated_tracers_2d'): + section = config[case_name] + vel_amp = section.getfloat('vel_amp') + u, v = flow_nondivergent(0., lonEdge, latEdge, + vel_amp, vel_pd * s_per_hour) + else: + raise ValueError(f'Unexpected test case name {case_name}') + + normalVelocity = sphere_radius * (u * np.cos(angleEdge) + + v * np.sin(angleEdge)) + normalVelocity, _ = xr.broadcast(normalVelocity, ds.refZMid) + ds['normalVelocity'] = normalVelocity.expand_dims(dim='Time', axis=0) + + ds['fCell'] = xr.zeros_like(ds_mesh.xCell) + ds['fEdge'] = xr.zeros_like(ds_mesh.xEdge) + ds['fVertex'] = xr.zeros_like(ds_mesh.xVertex) + + write_netcdf(ds, 'initial_state.nc') diff --git a/polaris/ocean/tasks/sphere_transport/mixing_analysis.py b/polaris/ocean/tasks/sphere_transport/mixing_analysis.py new file mode 100644 index 000000000..750518a7f --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/mixing_analysis.py @@ -0,0 +1,149 @@ +from math import ceil + +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +from matplotlib.lines import Line2D + +from polaris import Step +from polaris.mpas import time_index_from_xtime +from polaris.ocean.resolution import resolution_to_subdir +from polaris.viz import use_mplstyle + + +class MixingAnalysis(Step): + """ + A step for analyzing the output from sphere transport test cases + + Attributes + ---------- + resolutions : list of float + The resolutions of the meshes that have been run + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW + meshes + + case_name : str + The name of the test case + """ + def __init__(self, component, resolutions, icosahedral, subdir, + case_name, dependencies): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + resolutions : list of float + The resolutions of the meshes that have been run + + icosahedral : bool + Whether to use icosahedral, as opposed to less regular, JIGSAW + meshes + + subdir : str + The subdirectory that the step resides in + + case_name: str + The name of the test case + + dependencies : dict of dict of polaris.Steps + The dependencies of this step + """ + super().__init__(component=component, name='mixing_analysis', + subdir=subdir) + self.resolutions = resolutions + self.case_name = case_name + + for resolution in resolutions: + mesh_name = resolution_to_subdir(resolution) + base_mesh = dependencies['mesh'][resolution] + init = dependencies['init'][resolution] + forward = dependencies['forward'][resolution] + self.add_input_file( + filename=f'{mesh_name}_mesh.nc', + work_dir_target=f'{base_mesh.path}/base_mesh.nc') + self.add_input_file( + filename=f'{mesh_name}_init.nc', + work_dir_target=f'{init.path}/initial_state.nc') + self.add_input_file( + filename=f'{mesh_name}_output.nc', + work_dir_target=f'{forward.path}/output.nc') + self.add_output_file('triplots.png') + + def run(self): + """ + Run this step of the test case + """ + plt.switch_backend('Agg') + resolutions = self.resolutions + config = self.config + section = config[self.case_name] + eval_time = section.getfloat('mixing_evaluation_time') + s_per_day = 86400.0 + zidx = 1 + nrows = int(ceil(len(resolutions) / 2)) + use_mplstyle() + fig, axes = plt.subplots(nrows=nrows, ncols=2, sharex=True, + sharey=True, figsize=(5.5, 7)) + for i, resolution in enumerate(resolutions): + ax = axes[int(i / 2), i % 2] + _init_triplot_axes(ax) + mesh_name = resolution_to_subdir(resolution) + ax.set(title=mesh_name) + ds = xr.open_dataset(f'{mesh_name}_output.nc') + if i % 2 == 0: + ax.set_ylabel("tracer3") + if int(i / 2) == nrows - 1: + ax.set_xlabel("tracer2") + tidx = time_index_from_xtime(ds.xtime.values, + eval_time * s_per_day) + ds = ds.isel(Time=tidx) + ds = ds.isel(nVertLevels=zidx) + tracer2 = ds["tracer2"].values + tracer3 = ds["tracer3"].values + ax.plot(tracer2, tracer3, '.', markersize=1) + ax.set_aspect('equal') + if i % 2 < 1: + ax = axes[int(i / 2), 1] + ax.set_axis_off() + plt.subplots_adjust(wspace=0.1, hspace=0.1) + fig.suptitle('Correlated tracers 2-d') + fig.savefig('triplots.png', bbox_inches='tight') + + +def _init_triplot_axes(ax): + lw = 0.4 + topline = Line2D([0.1, 1.0], [0.9, 0.9], color='k', + linestyle='-', linewidth=lw) + midline = Line2D([0.1, 1.0], [0.9, 0.1], color='k', + linestyle='-', linewidth=lw) + rightline = Line2D([1, 1], [0.1, 0.9], color='k', + linestyle='-', linewidth=lw) + leftline = Line2D([0.1, 0.1], [0.1, 0.9], color='k', + linestyle='-', linewidth=lw) + botline = Line2D([0.1, 1.0], [0.1, 0.1], color='k', + linestyle='-', linewidth=lw) + crvx = np.linspace(0.1, 1) + crvy = -0.8 * np.square(crvx) + 0.9 + ticks = np.array(range(6)) / 5 + ax.plot(crvx, crvy, 'k-', linewidth=1.25 * lw) + ax.set_xticks(ticks) + ax.set_yticks(ticks) + ax.add_artist(topline) + ax.add_artist(midline) + ax.add_artist(botline) + ax.add_artist(rightline) + ax.add_artist(leftline) + ax.set_xlim([0, 1.1]) + ax.set_ylim([0, 1.0]) + ax.text(0.98, 0.87, 'Range-preserving\n unmixing', fontsize=8, + horizontalalignment='right', verticalalignment='top') + ax.text(0.12, 0.12, 'Range-preserving\n unmixing', fontsize=8, + horizontalalignment='left', verticalalignment='bottom') + ax.text(0.5, 0.27, 'Real mixing', rotation=-40., fontsize=8) + ax.text(0.02, 0.1, 'Overshooting', rotation=90., fontsize=8) + ax.grid(color='lightgray') diff --git a/polaris/ocean/tasks/sphere_transport/nondivergent_2d.cfg b/polaris/ocean/tasks/sphere_transport/nondivergent_2d.cfg new file mode 100644 index 000000000..d78be5b13 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/nondivergent_2d.cfg @@ -0,0 +1,13 @@ +# options for nondivergent_2d test case +[nondivergent_2d] + +# velocity amplitude in meters per second +vel_amp = 10. + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.5 +convergence_thresh_tracer2 = 1.1 +convergence_thresh_tracer3 = 0.3 + +# time in days at which to evaluate filament preservation +filament_evaluation_time = 6.0 diff --git a/polaris/ocean/tasks/sphere_transport/resources/__init__.py b/polaris/ocean/tasks/sphere_transport/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/polaris/ocean/tasks/sphere_transport/resources/flow_types.py b/polaris/ocean/tasks/sphere_transport/resources/flow_types.py new file mode 100644 index 000000000..78a9adf95 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/resources/flow_types.py @@ -0,0 +1,129 @@ +import numpy as np +from mpas_tools.transects import lon_lat_to_cartesian +from numpy import cos, pi, sin + + +def flow_nondivergent(t, lon, lat, u_0, tau): + """ + Compute a nondivergent velocity field + + Parameters + ---------- + t : np.ndarray of type float + times in seconds at which to compute the velocity field + + lon : np.ndarray of type float + longitude + + lat : np.ndarray of type float + latitude + + u_0 : float + velocity amplitude in meters per second + + tau : float + time in seconds for the flow to circumnavigate the sphere + + Returns + ------- + u : np.ndarray of type float + zonal velocity + + v : np.ndarray of type float + meridional velocity + """ + lon_p = lon - 2. * pi * t / tau + coslat = cos(lat) + cost = cos(pi * t / tau) + u = (1 / tau) * (u_0 * (sin(lon_p)**2) * sin(2 * lat) * cost + + 2. * pi * coslat) + v = (u_0 / tau) * sin(2 * lon_p) * coslat * cost + return u, v + + +def flow_divergent(t, lon, lat, u_0, tau): + """ + Compute a nondivergent velocity field + + Parameters + ---------- + t : np.ndarray of type float + times in seconds at which to compute the velocity field + + lon : np.ndarray of type float + longitude + + lat : np.ndarray of type float + latitude + + u_0 : float + velocity amplitude in meters per second + + tau : float + time in seconds for the flow to circumnavigate the sphere + + Returns + ------- + u : np.ndarray of type float + zonal velocity + + v : np.ndarray of type float + meridional velocity + """ + lon_p = lon - 2. * pi * t / tau + coslat = cos(lat) + cost = cos(pi * t / tau) + u = (1 / tau) * (-u_0 * (sin(lon_p / 2)**2) * sin(2 * lat) * + (coslat**2) * cost + 2. * pi * coslat) + v = (u_0 / (2 * tau)) * sin(lon_p) * (coslat**3) * cost + return u, v + + +def flow_rotation(lon, lat, omega, tau, sphere_radius): + """ + Compute a nondivergent velocity field + + Parameters + ---------- + lon : np.ndarray of type float + longitude + + lat : np.ndarray of type float + latitude + + omega : np.ndarray of type float + vector defining the axis of rotation of the flow in cartesian + coordinates + + tau : float + time in seconds for the flow to circumnavigate the sphere + + sphere_radius : float + radius of the sphere + + Returns + ------- + u : np.ndarray of type float + zonal velocity + + v : np.ndarray of type float + meridional velocity + """ + omega = (2. * pi / tau) * (omega / np.linalg.norm(omega)) + x, y, z = lon_lat_to_cartesian(lon, lat, sphere_radius, degrees=False) + xyz = np.stack((x, y, z), axis=1) + vel = np.cross(omega, np.divide(np.transpose(xyz), sphere_radius), axis=0) + east, north = calc_local_east_north(x, y, z) + u = np.sum(vel * east, axis=0) + v = np.sum(vel * north, axis=0) + return u, v + + +def calc_local_east_north(x, y, z): + axis = [0, 0, 1] + xyz = np.stack((x, y, z), axis=1) + east = np.cross(axis, np.transpose(xyz), axis=0) + north = np.cross(np.transpose(xyz), east, axis=0) + east = east / np.linalg.norm(east, axis=0) + north = north / np.linalg.norm(north, axis=0) + return east, north diff --git a/polaris/ocean/tasks/sphere_transport/resources/tracer_distributions.py b/polaris/ocean/tasks/sphere_transport/resources/tracer_distributions.py new file mode 100644 index 000000000..6eef52e71 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/resources/tracer_distributions.py @@ -0,0 +1,213 @@ +import numpy as np +from mpas_tools.transects import lon_lat_to_cartesian +from mpas_tools.vector import Vector + + +def slotted_cylinders(lon, lat, r, b, c, sphere_radius): + """ + Compute two slotted cylinders on the sphere + Lauritzen et al. 2012 eqn. (12) + + Parameters + ---------- + lon : np.ndarray of type float + longitude of cells on the sphere + + lat : np.ndarray of type float + latitude of cells on the sphere + + r : float + radius of each slotted cylinder + + b : float + background value of the tracer + + c : float + value of each slotted cylinder + + sphere_radius : float + radius of the sphere + + Returns + ------- + scs : np.ndarray + slotted cylinder tracer values + """ + lon_thr = 1 / (6 * 2) + lat_thr = 5 / (12 * 2) + lon1 = 5 * (np.pi / 6) + lat1 = 0 + lon2 = -5 * (np.pi / 6) + lat2 = 0 + lon0 = np.where(lon > np.pi, + lon - 2 * np.pi, + lon) + x, y, z = lon_lat_to_cartesian(lon, lat, sphere_radius, degrees=False) + x1, y1, z1 = lon_lat_to_cartesian(lon1, lat1, sphere_radius, degrees=False) + x2, y2, z2 = lon_lat_to_cartesian(lon2, lat2, sphere_radius, degrees=False) + xyz = Vector(x, y, z) + xyz1 = Vector(x1, y1, z1) + xyz2 = Vector(x2, y2, z2) + r1 = xyz.angular_distance(xyz1) + r2 = xyz.angular_distance(xyz2) + scs = np.where(r1 <= r, + np.where(np.logical_or((abs(lon0 - lon1) >= lon_thr), + np.logical_and( + abs(lon0 - lon1) < lon_thr, + lat - lat1 < -lat_thr)), + c, + b), + np.where(np.logical_and(r2 <= r, + np.logical_or( + (abs(lon0 - lon2) >= lon_thr), + np.logical_and( + abs(lon0 - lon2) < lon_thr, + lat - lat2 > lat_thr))), + c, + b)) + return scs + + +def cosine_bells(lon, lat, r, b, c, sphere_radius): + """ + Compute two cosine bells on the sphere + Lauritzen et al. 2012 eqn. (11) + + Parameters + ---------- + lon : np.ndarray of type float + longitude of cells on the sphere + + lat : np.ndarray of type float + latitude of cells on the sphere + + r : float + radius of each cosine bell + + b : float + background value of the tracer + + c : float + maximum value of each cosine bell + + sphere_radius : float + radius of the sphere + + Returns + ------- + cbs : np.ndarray + cosine bell tracer values + """ + # Location of the center of the first cosine bell + lon1 = 5 * (np.pi / 6) + lat1 = 0 + + # Location of the center of the second cosine bell + lon2 = -5 * (np.pi / 6) + lat2 = 0 + + x, y, z = lon_lat_to_cartesian(lon, lat, sphere_radius, degrees=False) + x1, y1, z1 = lon_lat_to_cartesian(lon1, lat1, sphere_radius, degrees=False) + x2, y2, z2 = lon_lat_to_cartesian(lon2, lat2, sphere_radius, degrees=False) + xyz = Vector(x, y, z) + xyz1 = Vector(x1, y1, z1) + xyz2 = Vector(x2, y2, z2) + # Distance of each cell from the center of the first cosine bell + r1 = xyz.angular_distance(xyz1) + # Distance of each cell from the center of the second cosine bell + r2 = xyz.angular_distance(xyz2) + + cbs = np.where(r1 < r, + cosine_bell(1.0, r1, r), + np.where(r2 < r, + cosine_bell(1.0, r2, r), + 0.)) + return b + c * cbs + + +def xyztrig(lon, lat, sphere_radius): + """ + Compute C-infinity tracer (not included in Lauritzen et al. 2012) + + Parameters + ---------- + lon : np.ndarray of type float + longitude of cells on the sphere + + lat : np.ndarray of type float + latitude of cells on the sphere + + r : float + radius of each cosine bell + + b : float + background value of the tracer + + c : float + maximum value of each cosine bell + + sphere_radius : float + radius of the sphere + + Returns + ------- + f : np.ndarray + C-infinity tracer values + """ + x, y, z = lon_lat_to_cartesian(lon, lat, sphere_radius, degrees=False) + x = np.divide(x, sphere_radius) + y = np.divide(y, sphere_radius) + z = np.divide(z, sphere_radius) + f = 0.5 * (1 + np.sin(np.pi * x) * np.sin(np.pi * y) * np.sin(np.pi * z)) + return f + + +def cosine_bell(max_value, ri, r): + """ + Compute values according to cosine bell function + Lauritzen et al. 2012 eqn. (10) + + Parameters + ---------- + max_value : float + Maximum value of the cosine bell function + + ri : np.ndarray of type float + Distance from the center of the cosine bell in meters + + r : float + Radius of the cosine bell in meters + + Returns + ------- + f : np.ndarray of type float + Cosine bell tracer values + """ + return max_value / 2.0 * (1.0 + np.cos(np.pi * np.divide(ri, r))) + + +def correlation_fn(q1, a, b, c): + """ + Compute a quadratic function for nonlinear tracer correlation following + Lauritzen et al. 2012 eqns. (14) and (15) + + Parameters + ---------- + q1 : np.ndarray + tracer values + + a : float + quadratic coefficient + + b : float + linear coefficient + + c : float + offset + + Returns + ------- + q2 : np.ndarray + correlated tracer values + """ + return a * q1**2. + b * q1 + c diff --git a/polaris/ocean/tasks/sphere_transport/resources/wh-bl-gr-ye-re.rgb b/polaris/ocean/tasks/sphere_transport/resources/wh-bl-gr-ye-re.rgb new file mode 100644 index 000000000..d3d0950a1 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/resources/wh-bl-gr-ye-re.rgb @@ -0,0 +1,202 @@ +ncolors= 199 + +# r g b + 255 255 255 + 250 250 255 + 245 245 255 + 240 240 255 + 235 235 255 + 230 230 255 + 224 224 255 + 219 219 255 + 214 214 255 + 209 209 255 + 204 204 255 + 199 199 255 + 194 194 255 + 189 189 255 + 184 184 255 + 179 179 255 + 173 173 255 + 168 168 255 + 163 163 255 + 158 158 255 + 153 153 255 + 148 148 255 + 143 143 255 + 138 138 255 + 133 133 255 + 128 128 255 + 122 122 255 + 117 117 255 + 112 112 255 + 107 107 255 + 102 102 255 + 97 97 255 + 92 92 255 + 87 87 255 + 82 82 255 + 77 77 255 + 71 71 255 + 66 66 255 + 61 61 255 + 56 56 255 + 51 51 255 + 46 46 255 + 41 41 255 + 36 36 255 + 31 31 255 + 26 26 255 + 20 20 255 + 15 15 255 + 10 10 255 + 5 5 255 + 0 0 255 + 0 4 250 + 0 7 245 + 0 11 240 + 1 15 235 + 1 18 231 + 1 22 226 + 1 26 221 + 1 29 216 + 1 33 211 + 2 37 206 + 2 40 201 + 2 44 196 + 2 47 191 + 2 51 186 + 2 55 182 + 3 58 177 + 3 62 172 + 3 66 167 + 3 69 162 + 3 73 157 + 3 77 152 + 4 80 147 + 4 84 142 + 4 88 137 + 4 91 133 + 4 95 128 + 4 99 123 + 5 102 118 + 5 106 113 + 5 110 108 + 5 113 103 + 5 117 98 + 5 121 93 + 6 124 88 + 6 128 84 + 6 132 79 + 6 135 74 + 6 139 69 + 6 142 64 + 7 146 59 + 7 150 54 + 7 153 49 + 7 157 44 + 7 161 39 + 7 164 35 + 8 168 30 + 8 172 25 + 8 175 20 + 8 179 15 + 13 181 15 + 18 182 14 + 23 184 14 + 28 185 14 + 33 187 14 + 38 188 13 + 43 190 13 + 48 191 13 + 52 193 12 + 57 194 12 + 62 196 12 + 67 197 11 + 72 199 11 + 77 200 11 + 82 202 11 + 87 203 10 + 92 205 10 + 97 206 10 + 102 208 9 + 107 209 9 + 112 211 9 + 117 212 8 + 122 214 8 + 127 215 8 + 132 217 8 + 136 219 7 + 141 220 7 + 146 222 7 + 151 223 6 + 156 225 6 + 161 226 6 + 166 228 5 + 171 229 5 + 176 231 5 + 181 232 5 + 186 234 4 + 191 235 4 + 196 237 4 + 201 238 3 + 206 240 3 + 211 241 3 + 215 243 2 + 220 244 2 + 225 246 2 + 230 247 2 + 235 249 1 + 240 250 1 + 245 252 1 + 250 253 0 + 255 255 0 + 255 250 0 + 255 245 0 + 255 239 0 + 255 234 0 + 255 229 0 + 255 224 0 + 255 219 0 + 255 213 0 + 255 208 0 + 255 203 0 + 255 198 0 + 255 193 0 + 255 187 0 + 255 182 0 + 255 177 0 + 255 172 0 + 255 167 0 + 255 161 0 + 255 156 0 + 255 151 0 + 255 146 0 + 255 141 0 + 255 135 0 + 255 130 0 + 255 125 0 + 255 120 0 + 255 114 0 + 255 109 0 + 255 104 0 + 255 99 0 + 255 94 0 + 255 88 0 + 255 83 0 + 255 78 0 + 255 73 0 + 255 68 0 + 255 62 0 + 255 57 0 + 255 52 0 + 255 47 0 + 255 42 0 + 255 36 0 + 255 31 0 + 255 26 0 + 255 21 0 + 255 16 0 + 255 10 0 + 255 5 0 + 255 0 0 diff --git a/polaris/ocean/tasks/sphere_transport/rotation_2d.cfg b/polaris/ocean/tasks/sphere_transport/rotation_2d.cfg new file mode 100644 index 000000000..075270de7 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/rotation_2d.cfg @@ -0,0 +1,10 @@ +# options for rotation_2d test case +[rotation_2d] + +# rotation vector in cartesian coordinates +rotation_vector = 0.2, 0.7, 1.0 + +# convergence threshold below which the test fails +convergence_thresh_tracer1 = 1.8 +convergence_thresh_tracer2 = 2.0 +convergence_thresh_tracer3 = 0.4 diff --git a/polaris/ocean/tasks/sphere_transport/sphere_transport.cfg b/polaris/ocean/tasks/sphere_transport/sphere_transport.cfg new file mode 100644 index 000000000..30b425993 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/sphere_transport.cfg @@ -0,0 +1,147 @@ +# Options related to the vertical grid +[vertical_grid] + +# the type of vertical grid +grid_type = uniform + +# Number of vertical levels +vert_levels = 3 + +# Depth of the bottom of the ocean +bottom_depth = 300.0 + +# The type of vertical coordinate (e.g. z-level, z-star) +coord_type = z-level + +# Whether to use "partial" or "full", or "None" to not alter the topography +partial_cell_type = None + +# The minimum fraction of a layer for partial cells +min_pc_fraction = 0.1 + + +# config options for spherical convergence tests +[convergence] + +# Evaluation time for convergence analysis (in hours) +convergence_eval_time = ${sphere_transport:vel_pd} + +# Error type +error_type = l2 + + +# config options for spherical convergence tests +[convergence_forward] + +# time integrator: {'split_explicit', 'RK4'} +time_integrator = RK4 + +# RK4 time step per resolution (s/km), since dt is proportional to resolution +rk4_dt_per_km = 8.0 + +# Run duration in hours +run_duration = ${sphere_transport:vel_pd} + +# Output interval in hours +output_interval = 24.0 + + +# options for all sphere transport test cases +[sphere_transport] + +# temperature +temperature = 15. + +# salinity +salinity = 35. + +# time (hours) for bell to transit equator once +vel_pd = 288.0 + +# radius of cosine bells tracer distributions +cosine_bells_radius = 0.5 + +# background value of cosine bells tracer distribution +cosine_bells_background = 0.1 + +# amplitude of cosine bells tracer distribution +cosine_bells_amplitude = 0.9 + +# radius of slotted cylinders tracer distributions +slotted_cylinders_radius = 0.5 + +# background value of slotted cylinders tracer distribution +slotted_cylinders_background = 0.1 + +# amplitude of slotted cylinders tracer distribution +slotted_cylinders_amplitude = 1.0 + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz] + +# visualization latitude and longitude resolution +dlon = 0.5 +dlat = 0.5 + +# remapping method ('bilinear', 'neareststod', 'conserve') +remap_method = conserve + + +# options for tracer visualization for the sphere transport test case +[sphere_transport_viz_tracer] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 0., 'vmax': 1.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_tracer_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} + + +# options for thickness visualization for the sphere transport test case +[sphere_transport_viz_h] + +# colormap options +# colormap +colormap_name = viridis + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': 99., 'vmax': 101.} + +# We could provide colorbar tick marks but we'll leave the defaults +# colorbar_ticks = np.linspace(0., 1., 9) + +# options for plotting tracer differences from sphere transport tests +[sphere_transport_viz_h_diff] + +# colormap options +# colormap +colormap_name = cmo.balance + +# the type of norm used in the colormap +norm_type = linear + +# A dictionary with keywords for the norm +norm_args = {'vmin': -0.25, 'vmax': 0.25} diff --git a/polaris/ocean/tasks/sphere_transport/viz.py b/polaris/ocean/tasks/sphere_transport/viz.py new file mode 100644 index 000000000..03dee7b55 --- /dev/null +++ b/polaris/ocean/tasks/sphere_transport/viz.py @@ -0,0 +1,192 @@ +import cmocean # noqa: F401 +import xarray as xr + +from polaris import Step +from polaris.mpas import time_index_from_xtime +from polaris.remap import MappingFileStep +from polaris.viz import plot_global_field + + +class VizMap(MappingFileStep): + """ + A step for making a mapping file for cosine bell viz + + Attributes + ---------- + mesh_name : str + The name of the mesh + """ + def __init__(self, component, name, subdir, base_mesh, mesh_name): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + name : str + The name of the step + + subdir : str + The subdirectory for the step + + base_mesh : polaris.Step + The base mesh step + + mesh_name : str + The name of the mesh + """ + super().__init__(component=component, name=name, subdir=subdir, + ntasks=128, min_tasks=1) + self.mesh_name = mesh_name + self.add_input_file( + filename='mesh.nc', + work_dir_target=f'{base_mesh.path}/base_mesh.nc') + + def runtime_setup(self): + """ + Set up the source and destination grids for this step + """ + config = self.config + section = config['sphere_transport_viz'] + dlon = section.getfloat('dlon') + dlat = section.getfloat('dlat') + method = section.get('remap_method') + self.src_from_mpas(filename='mesh.nc', mesh_name=self.mesh_name) + self.dst_global_lon_lat(dlon=dlon, dlat=dlat, lon_min=0.) + self.method = method + + super().runtime_setup() + + +class Viz(Step): + """ + A step for plotting fields from the cosine bell output + + Attributes + ---------- + mesh_name : str + The name of the mesh + """ + def __init__(self, component, name, subdir, base_mesh, init, forward, + viz_map, mesh_name): + """ + Create the step + + Parameters + ---------- + component : polaris.Component + The component the step belongs to + + name : str + The name of the step + + subdir : str + The subdirectory in the test case's work directory for the step + + base_mesh : polaris.Step + The base mesh step + + init : polaris.Step + The init step + + forward : polaris.Step + The init step + + viz_map : polaris.ocean.tasks.sphere_transport.viz.VizMap + The step for creating a mapping files, also used to remap data + from the MPAS mesh to a lon-lat grid + + mesh_name : str + The name of the mesh + """ + super().__init__(component=component, name=name, subdir=subdir) + self.add_input_file( + filename='mesh.nc', + work_dir_target=f'{base_mesh.path}/base_mesh.nc') + self.add_input_file( + filename='initial_state.nc', + work_dir_target=f'{init.path}/initial_state.nc') + self.add_input_file( + filename='output.nc', + work_dir_target=f'{forward.path}/output.nc') + self.add_dependency(viz_map, name='viz_map') + self.mesh_name = mesh_name + variables_to_plot = dict({'tracer1': 'tracer', + 'tracer2': 'tracer', + 'tracer3': 'tracer', + 'layerThickness': 'h'}) + self.variables_to_plot = variables_to_plot + for var in variables_to_plot.keys(): + self.add_output_file(f'{var}_init.png') + self.add_output_file(f'{var}_final.png') + self.add_output_file(f'{var}_diff.png') + + def run(self): + """ + Run this step of the test case + """ + config = self.config + mesh_name = self.mesh_name + run_duration = config.getfloat('convergence_forward', + 'run_duration') + + viz_map = self.dependencies['viz_map'] + + remapper = viz_map.get_remapper() + + variables_to_plot = self.variables_to_plot + ds_init = xr.open_dataset('initial_state.nc') + ds_init = ds_init[variables_to_plot.keys()].isel(Time=0, nVertLevels=0) + ds_init = remapper.remap(ds_init) + ds_init.to_netcdf('remapped_init.nc') + + ds_out = xr.open_dataset('output.nc') + s_per_hour = 3600.0 + + # Visualization at halfway around the globe (provided run duration is + # set to the time needed to circumnavigate the globe) + tidx = time_index_from_xtime(ds_out.xtime.values, + run_duration * s_per_hour / 2.) + ds_mid = ds_out[variables_to_plot.keys()].isel(Time=tidx, + nVertLevels=0) + ds_mid = remapper.remap(ds_mid) + ds_mid.to_netcdf('remapped_mid.nc') + + # Visualization at all the way around the globe + tidx = time_index_from_xtime(ds_out.xtime.values, + run_duration * s_per_hour) + ds_final = ds_out[variables_to_plot.keys()].isel(Time=tidx, + nVertLevels=0) + ds_final = remapper.remap(ds_final) + ds_final.to_netcdf('remapped_final.nc') + + for var, section_name in variables_to_plot.items(): + colormap_section = f'sphere_transport_viz_{section_name}' + plot_global_field(ds_init.lon.values, ds_init.lat.values, + ds_init[var].values, + out_filename=f'{var}_init.png', config=config, + colormap_section=colormap_section, + title=f'{mesh_name} {var} at init', + plot_land=False) + plot_global_field(ds_init.lon.values, ds_init.lat.values, + ds_mid[var].values, + out_filename=f'{var}_mid.png', config=config, + colormap_section=colormap_section, + title=f'{mesh_name} {var} after ' + f'{run_duration / 48.:g} days', + plot_land=False) + plot_global_field(ds_init.lon.values, ds_init.lat.values, + ds_final[var].values, + out_filename=f'{var}_final.png', config=config, + colormap_section=colormap_section, + title=f'{mesh_name} {var} after ' + f'{run_duration / 24.:g} days', plot_land=False) + plot_global_field(ds_init.lon.values, ds_init.lat.values, + ds_final[var].values - ds_init[var].values, + out_filename=f'{var}_diff.png', config=config, + colormap_section=f'{colormap_section}_diff', + title=f'Difference in {mesh_name} {var} from ' + f'initial condition after {run_duration / 24.:g}' + ' days', plot_land=False)