Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compressible vorticity #495

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions examples/compressible/skamarock_klemp_hydrostatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,9 @@
domain = Domain(mesh, dt, "RTCF", 1)

# Equation
parameters = CompressibleParameters()
Omega = as_vector((0., 0., 0.5e-4))
parameters = CompressibleParameters(Omega=0.5e-4)
balanced_pg = as_vector((0., -1.0e-4*20, 0.))
eqns = CompressibleEulerEquations(domain, parameters, Omega=Omega,
extra_terms=[("u", balanced_pg)])
eqns = CompressibleEulerEquations(domain, parameters, extra_terms=[("u", balanced_pg)])

# I/O
dirname = 'skamarock_klemp_hydrostatic'
Expand Down
1 change: 1 addition & 0 deletions gusto/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class CompressibleParameters(Configuration):
w_sat2 = -17.27 # second const. in Teten's formula (no units)
w_sat3 = 35.86 # third const. in Teten's formula (K)
w_sat4 = 610.9 # fourth const. in Teten's formula (Pa)
Omega = None # Rotation rate


class ShallowWaterParameters(Configuration):
Expand Down
162 changes: 140 additions & 22 deletions gusto/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
from firedrake import assemble, dot, dx, Function, sqrt, \
TestFunction, TrialFunction, Constant, grad, inner, curl, \
LinearVariationalProblem, LinearVariationalSolver, FacetNormal, \
ds_b, ds_v, ds_t, dS_h, dS_v, ds, dS, div, avg, jump, pi, \
ds_b, ds_v, ds_t, dS_h, dS_v, ds, dS, div, cross, avg, jump, pi, \
TensorFunctionSpace, SpatialCoordinate, as_vector, \
Projector, Interpolator, FunctionSpace, FiniteElement, \
TensorProductElement
from firedrake.assign import Assigner
from ufl.domain import extract_unique_domain

from abc import ABCMeta, abstractmethod, abstractproperty
from gusto.equations import CompressibleEulerEquations
import gusto.thermodynamics as tde
from gusto.coord_transforms import rotated_lonlatr_vectors
from gusto.recovery import Recoverer, BoundaryMethod
from gusto.equations import CompressibleEulerEquations
from gusto.active_tracers import TracerVariableType, Phases
from gusto.logging import logger
from gusto.kernels import MinKernel, MaxKernel
Expand All @@ -28,7 +28,8 @@
"Perturbation", "Theta_e", "InternalEnergy", "PotentialEnergy",
"ThermodynamicKineticEnergy", "Dewpoint", "Temperature", "Theta_d",
"RelativeHumidity", "Pressure", "Exner_Vt", "HydrostaticImbalance", "Precipitation",
"PotentialVorticity", "RelativeVorticity", "AbsoluteVorticity", "Divergence",
"ShallowWaterPotentialVorticity", "ShallowWaterRelativeVorticity", "ShallowWaterAbsoluteVorticity",
"Vorticity", "RelativeVorticity", "AbsoluteVorticity", "Divergence",
"BruntVaisalaFrequencySquared", "TracerDensity"]


Expand Down Expand Up @@ -798,7 +799,7 @@

class ShallowWaterPotentialEnstrophy(DiagnosticField):
"""Diagnostic (dry) compressible kinetic energy density."""
def __init__(self, base_field_name="PotentialVorticity", space=None,
def __init__(self, base_field_name="ShallowWaterPotentialVorticity", space=None,
method='interpolate'):
"""
Args:
Expand All @@ -811,15 +812,17 @@
for this diagnostic. Valid options are 'interpolate', 'project',
'assign' and 'solve'. Defaults to 'interpolate'.
"""
base_enstrophy_names = ["PotentialVorticity", "RelativeVorticity", "AbsoluteVorticity"]
base_enstrophy_names = ["ShallowWaterPotentialVorticity",
"ShallowWaterRelativeVorticity",
"ShallowWaterAbsoluteVorticity"]
if base_field_name not in base_enstrophy_names:
raise ValueError(
f"Don't know how to compute enstrophy with base_field_name={base_field_name};"
+ f"base_field_name should be one of {base_enstrophy_names}")
# Work out required fields
if base_field_name in ["PotentialVorticity", "AbsoluteVorticity"]:
if base_field_name in ["ShallowWaterPotentialVorticity", "ShallowWaterAbsoluteVorticity"]:
required_fields = (base_field_name, "D")
elif base_field_name == "RelativeVorticity":
elif base_field_name == "ShallowWaterRelativeVorticity":
required_fields = (base_field_name, "D", "coriolis")
else:
raise NotImplementedError(f'Enstrophy with vorticity {base_field_name} not implemented')
Expand All @@ -841,17 +844,17 @@
domain (:class:`Domain`): the model's domain object.
state_fields (:class:`StateFields`): the model's field container.
"""
if self.base_field_name == "PotentialVorticity":
pv = state_fields("PotentialVorticity")
if self.base_field_name == "ShallowWaterPotentialVorticity":
pv = state_fields("ShallowWaterPotentialVorticity")
D = state_fields("D")
self.expr = 0.5*pv**2*D
elif self.base_field_name == "RelativeVorticity":
zeta = state_fields("RelativeVorticity")
elif self.base_field_name == "ShallowWaterRelativeVorticity":
zeta = state_fields("ShallowWaterRelativeVorticity")
D = state_fields("D")
f = state_fields("coriolis")
self.expr = 0.5*(zeta + f)**2/D
elif self.base_field_name == "AbsoluteVorticity":
zeta_abs = state_fields("AbsoluteVorticity")
elif self.base_field_name == "ShallowWaterAbsoluteVorticity":
zeta_abs = state_fields("ShallowWaterAbsoluteVorticity")
D = state_fields("D")
self.expr = 0.5*(zeta_abs)**2/D
else:
Expand Down Expand Up @@ -1549,7 +1552,7 @@
self.field.assign(self.field + self.flux)


class Vorticity(DiagnosticField):
class ShallowWaterVorticity(DiagnosticField):
"""Base diagnostic field class for shallow-water vorticity variables."""

def setup(self, domain, state_fields, vorticity_type=None):
Expand Down Expand Up @@ -1577,9 +1580,9 @@

if self.method != 'solve':
if vorticity_type == "potential":
self.expr = curl(u + f) / D
self.expr = (curl(u) + f) / D
elif vorticity_type == "absolute":
self.expr = curl(u + f)
self.expr = curl(u) + f
elif vorticity_type == "relative":
self.expr = curl(u)

Expand All @@ -1604,9 +1607,9 @@
self.evaluator = LinearVariationalSolver(problem, solver_parameters={"ksp_type": "cg"})


class PotentialVorticity(Vorticity):
class ShallowWaterPotentialVorticity(ShallowWaterVorticity):
u"""Diagnostic field for shallow-water potential vorticity, q=(∇×(u+f))/D"""
name = "PotentialVorticity"
name = "ShallowWaterPotentialVorticity"

def __init__(self, space=None, method='solve'):
"""
Expand All @@ -1633,9 +1636,9 @@
super().setup(domain, state_fields, vorticity_type="potential")


class AbsoluteVorticity(Vorticity):
class ShallowWaterAbsoluteVorticity(ShallowWaterVorticity):
u"""Diagnostic field for absolute vorticity, ζ=∇×(u+f)"""
name = "AbsoluteVorticity"
name = "ShallowWaterAbsoluteVorticity"

def __init__(self, space=None, method='solve'):
"""
Expand All @@ -1661,9 +1664,9 @@
super().setup(domain, state_fields, vorticity_type="absolute")


class RelativeVorticity(Vorticity):
class ShallowWaterRelativeVorticity(ShallowWaterVorticity):
u"""Diagnostic field for relative vorticity, ζ=∇×u"""
name = "RelativeVorticity"
name = "ShallowWaterRelativeVorticity"

def __init__(self, space=None, method='solve'):
"""
Expand All @@ -1689,6 +1692,121 @@
super().setup(domain, state_fields, vorticity_type="relative")


class Vorticity(DiagnosticField):
u""" Base diagnostic Field for three dimensional Vorticity """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this (should this?) also work in a vertical slice?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question, which im not sure on so will have a test around and decide if it does / should it, i think it should work but may require a bit of thinking

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you've put in effort to make this work in a vertical slice, you can change this comment!


def setup(self, domain, state_fields, vorticity_type=None):
"""
Sets up the :class:`Function` for the diagnostic field.

Args:
domain (:class:`Domain`): the model's domain object.
state_fields (:class:`StateFields`): the model's field container.
vorticity_type (str, optional): denotes which type of vorticity to
be computed ('relative', 'absolute'). Defaults to
None.
"""
# TODO Do we eventually want a potential voriticy?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think we will -- you can remove this comment for now though

vorticity_types = ['relative', 'absolute']
if vorticity_type not in vorticity_types:
if vorticity_type == 'potential':
raise ValueError('Potential vorticity has not yet been implemented')
else:
raise ValueError(f'vorticity type must be one of {vorticity_types}, not {vorticity_type}')
if domain.mesh.topological_dimension() == 3:
space = domain.spaces('HCurl')
else:
space = domain.spaces('H1')

u = state_fields('u')
if self.method != 'solve':
if vorticity_type == 'relative':
self.expression = curl(u)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be self.expr?

elif vorticity_type == 'absolute':
Omega = as_vector((0, 0, self.parameters.Omega))
self.expression = curl(u) + 2*Omega
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be self.expr?


super().setup(domain, state_fields, space=space)

if self.method == 'solve':
if domain.mesh.topological_dimension() == 3:
vort = TrialFunction(space)
w = TestFunction(space)
n = FacetNormal(domain.mesh)
a = inner(vort, w) * dx
L = inner(u, curl(w)) * dx - jump(cross(w, u), n) * dS_h
if vorticity_type == 'absolute':
Omega = as_vector((0, 0, self.parameters.Omega))
L += inner(2*Omega, w) * dx
else:
vort = TrialFunction(space)
gamma = TestFunction(space)
n = FacetNormal(domain.mesh)
a = inner(vort, gamma) * dx
L = ( inner(domain.perp(grad(gamma)), u))*dx - jump(inner(domain.perp(n), u)*gamma)*ds_h

Check failure on line 1746 in gusto/diagnostics.py

View workflow job for this annotation

GitHub Actions / Run linter

E201

gusto/diagnostics.py:1746:22: E201 whitespace after '('

Check failure on line 1746 in gusto/diagnostics.py

View workflow job for this annotation

GitHub Actions / Run linter

F821

gusto/diagnostics.py:1746:101: F821 undefined name 'ds_h'
# TODO implement absolute version, unsure atm how to get corioilis in vertical slice smartly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't obvious to me, and it isn't obvious that we'll ever want the absolute vorticity in a vertical slice. I suggest that you put an if check here so that we fail with a NotImplementedError in this situation

problem = LinearVariationalProblem(a, L, self.field)
self.evaluator = LinearVariationalSolver(problem, solver_parameters={'ksp_type': 'cg'})

class RelativeVorticity(Vorticity):

Check failure on line 1751 in gusto/diagnostics.py

View workflow job for this annotation

GitHub Actions / Run linter

E302

gusto/diagnostics.py:1751:1: E302 expected 2 blank lines, found 1
u""" Diagnostic field for compressible relative vorticity """
name = 'RelativeVorticity'

def __init__(self, space=None, method='solve'):
u"""
Args:
space (:class:`FunctionSpace`, optional): the function space to
evaluate the diagnostic field in. Defaults to None, in which
case a default space will be chosen for this diagnostic.
method (str, optional): a string specifying the method of evaluation
for this diagnostic. Valid options are 'interpolate', 'project',
'assign' and 'solve'. Defaults to 'solve'.
"""
self.solve_implemented = True
super().__init__(space=space, method=method, required_fields=('u'))

def setup(self, domain, state_fields):
u"""
Sets up the :class:`Function` for the diagnostic field.

Args:
domain (:class:`Domain`): the model's domain object.
state_fields (:class:`StateFields`): the model's field container.
"""
super().setup(domain, state_fields, vorticity_type='relative')


class AbsoluteVorticity(Vorticity):
u""" Diagnostic field for compressible euler absolute vorticity """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Jemma says, you could remove reference to compressible euler here?

name = 'AbsoluteVorticity'

def __init__(self, parameters, space=None, method='solve'):
u"""
Args:
parameters (:class:`CompressibleEulerParameters`): the configuration
object containing the physical parameters for this equation.
space (:class:`FunctionSpace`, optional): the function space to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please update docstring to include parameters argument

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's just CompressibleParameters - this should also work for the Boussinesq equations so you can remove any mention of Euler... in fact, it also works for incompressible...

evaluate the diagnostic field in. Defaults to None, in which
case a default space will be chosen for this diagnostic.
method (str, optional): a string specifying the method of evaluation
for this diagnostic. Valid options are 'interpolate', 'project',
'assign' and 'solve'. Defaults to 'solve'.
"""
self.solve_implemented = True
self.parameters = parameters
super().__init__(space=space, method=method, required_fields=('u'))

def setup(self, domain, state_fields):
u"""
Sets up the :class:`Function` for the diagnostic field.

Args:
domain (:class:`Domain`): the model's domain object.
state_fields (:class:`StateFields`): the model's field container.
"""
super().setup(domain, state_fields, vorticity_type='absolute')


class TracerDensity(DiagnosticField):
"""Diagnostic for computing the density of a tracer. This is
computed as the product of a mixing ratio and dry density"""
Expand Down
12 changes: 7 additions & 5 deletions gusto/equations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
TestFunction, Function, sin, pi, inner, dx, div, cross,
FunctionSpace, MixedFunctionSpace, TestFunctions, TrialFunction,
FacetNormal, jump, avg, dS_v, dS, DirichletBC, conditional,
SpatialCoordinate, split, Constant, action
SpatialCoordinate, split, Constant, action, as_vector
)
from firedrake.fml import (
Term, all_terms, keep, drop, Label, subject,
Expand Down Expand Up @@ -1074,7 +1074,7 @@ class CompressibleEulerEquations(PrognosticEquationSet):
pressure.
"""

def __init__(self, domain, parameters, Omega=None, sponge_options=None,
def __init__(self, domain, parameters, sponge_options=None,
extra_terms=None, space_names=None,
linearisation_map='default',
u_transport_option="vector_invariant_form",
Expand Down Expand Up @@ -1256,10 +1256,12 @@ def __init__(self, domain, parameters, Omega=None, sponge_options=None,
# -------------------------------------------------------------------- #
# Extra Terms (Coriolis, Sponge, Diffusion and others)
# -------------------------------------------------------------------- #
if Omega is not None:
# TODO: add linearisation
residual += coriolis(subject(prognostic(
if parameters.Omega is not None:
# TODO add linerisation and label for this
Omega = as_vector((0, 0, parameters.Omega))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should call this variable Omega_vec to avoid confusion

coriolis_form = coriolis(subject(prognostic(
inner(w, cross(2*Omega, u))*dx, "u"), self.X))
residual += coriolis_form

if sponge_options is not None:
W_DG = FunctionSpace(domain.mesh, "DG", 2)
Expand Down
Loading