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

Linear boussinesq #491

Merged
merged 4 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
98 changes: 98 additions & 0 deletions examples/boussinesq/skamarock_klemp_linear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
The gravity wave test case of Skamarock and Klemp (1994), solved using the
incompressible Boussinesq equations.

Buoyancy is transported using SUPG.
"""

from gusto import *
from firedrake import (PeriodicIntervalMesh, ExtrudedMesh,
sin, SpatialCoordinate, Function, pi)
import sys

# ---------------------------------------------------------------------------- #
# Test case parameters
# ---------------------------------------------------------------------------- #

dt = 0.5
L = 3.0e5 # Domain length
H = 1.0e4 # Height position of the model top

if '--running-tests' in sys.argv:
tmax = dt
dumpfreq = 1
columns = 30 # number of columns
nlayers = 5 # horizontal layers

else:
tmax = 3600.
dumpfreq = int(tmax / (2*dt))
columns = 300 # number of columns
nlayers = 10 # horizontal layers

# ---------------------------------------------------------------------------- #
# Set up model objects
# ---------------------------------------------------------------------------- #

# Domain
m = PeriodicIntervalMesh(columns, L)
mesh = ExtrudedMesh(m, layers=nlayers, layer_height=H/nlayers)
domain = Domain(mesh, dt, 'CG', 1)

# Equation
parameters = BoussinesqParameters(cs=300)
eqns = LinearBoussinesqEquations(domain, parameters)

# I/O
output = OutputParameters(dirname='skamarock_klemp_linear')
# list of diagnostic fields, each defined in a class in diagnostics.py
diagnostic_fields = [CourantNumber(), Divergence(), Perturbation('b')]
io = IO(domain, output, diagnostic_fields=diagnostic_fields)

# Transport schemes
b_opts = SUPGOptions()
transport_methods = [DGUpwind(eqns, "p"),
DGUpwind(eqns, "b", ibp=b_opts.ibp)]


# Time stepper
stepper = Timestepper(eqns, RK4(domain), io, spatial_methods=transport_methods)

# ---------------------------------------------------------------------------- #
# Initial conditions
# ---------------------------------------------------------------------------- #

b0 = stepper.fields("b")
p0 = stepper.fields("p")

# spaces
Vb = b0.function_space()
Vp = p0.function_space()

x, z = SpatialCoordinate(mesh)

# first setup the background buoyancy profile
# z.grad(bref) = N**2
N = parameters.N
bref = z*(N**2)
# interpolate the expression to the function
b_b = Function(Vb).interpolate(bref)

# setup constants
a = 5.0e3
deltab = 1.0e-2
b_pert = deltab*sin(pi*z/H)/(1 + (x - L/2)**2/a**2)
# interpolate the expression to the function
b0.interpolate(b_b + b_pert)

p_b = Function(Vp)
boussinesq_hydrostatic_balance(eqns, b_b, p_b)
p0.assign(p_b)

# set the background buoyancy
stepper.set_reference_profiles([('p', p_b), ('b', b_b)])

# ---------------------------------------------------------------------------- #
# Run
# ---------------------------------------------------------------------------- #
stepper.run(t=0, tmax=tmax)
102 changes: 97 additions & 5 deletions gusto/equations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1326,7 +1326,7 @@ def __init__(self, domain, parameters,

w, phi, gamma = self.tests[0:3]
u, p, b = split(self.X)
u_trial = split(self.trials)[0]
u_trial, p_trial, _ = split(self.trials)
_, p_bar, b_bar = split(self.X_ref)

# -------------------------------------------------------------------- #
Expand Down Expand Up @@ -1385,16 +1385,21 @@ def __init__(self, domain, parameters,
# -------------------------------------------------------------------- #
if compressible:
cs = parameters.cs
divergence_form = divergence(subject(
prognostic(cs**2 * phi * div(u) * dx, 'p'), self.X))
linear_div_form = divergence(subject(
prognostic(cs**2 * phi * div(u_trial) * dx, 'p'), self.X))
divergence_form = divergence(linearisation(
subject(prognostic(cs**2 * phi * div(u) * dx, 'p'), self.X),
linear_div_form))
else:
# This enforces that div(u) = 0
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the results look odd when running incompressible Boussinesq with RK4 but are fine with SIQN - this comment is about the forcing term which makes me suspicious...

Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be related to issues @atb1995 saw when having a divergence term in the shallow-water equations? And in forcing.py we ensure that the incompressibility term is treated implicitly. So would we expect this to work with an explicit time stepper?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd assume it should work fine for small enough dt - the issue I was having was that the range of stable dt for the IMEX RK schemes was being limited by effectively treating a div(u) term explicitly. You could try using IMEX RK, with the div term labeled first explicitly, then implicitly and see if you see the same issues when it is explicit (but not when it is implicit). Or just use smaller dt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, it's not that - sorry, I should have been clearer. The results don't show instability - they're just wrong. And the reason is that this isn't really an evolution equation for the pressure. What we're trying to do is solve for the pressure that gives us a non-divergent velocity. When we use the SIQN time stepper, the incompressible term is implicit and, as the comment here says, we overwrite the pressure rather than updating it.

# The p features here so that the div(u) evaluated in the
# "forcing" step replaces the whole pressure field, rather than
# merely providing an increment to it.
divergence_form = incompressible(
linear_div_form = incompressible(
subject(prognostic(phi*(p_trial-div(u_trial))*dx, 'p'), self.X))
divergence_form = incompressible(linearisation(
subject(prognostic(phi*(p-div(u))*dx, 'p'), self.X),
)
linear_div_form))

residual = (mass_form + adv_form + divergence_form
+ pressure_gradient_form + gravity_form)
Expand All @@ -1412,3 +1417,90 @@ def __init__(self, domain, parameters,
# -------------------------------------------------------------------- #
# Add linearisations to equations
self.residual = self.generate_linear_terms(residual, self.linearisation_map)


class LinearBoussinesqEquations(BoussinesqEquations):
"""
Class for the Boussinesq equations, which evolve the velocity
'u', the pressure 'p' and the buoyancy 'b'. Can be compressible or
incompressible, depending on the value of the input flag, which defaults
to compressible.

The compressible form of the equations is
∂u/∂t + (u.∇)u + 2Ω×u + ∇p + b*k = 0, \n
∂p/∂t + cs**2 ∇.u = p, \n
∂b/∂t + (u.∇)b = 0, \n
where k is the vertical unit vector, Ω is the planet's rotation vector
and cs is the sound speed.

For the incompressible form of the equations, the pressure features as
a Lagrange multiplier to enforce incompressibility. The equations are \n
∂u/∂t + (u.∇)u + 2Ω×u + ∇p + b*k = 0, \n
∇.u = p, \n
∂b/∂t + (u.∇)b = 0, \n
where k is the vertical unit vector and Ω is the planet's rotation vector.
"""

def __init__(self, domain, parameters,
compressible=True,
Omega=None,
space_names=None,
linearisation_map='default',
u_transport_option="vector_invariant_form",
no_normal_flow_bc_ids=None,
active_tracers=None):
"""
Args:
domain (:class:`Domain`): the model's domain object, containing the
mesh and the compatible function spaces.
parameters (:class:`Configuration`, optional): an object containing
the model's physical parameters.
compressible (bool, optional): flag to indicate whether the
equations are compressible. Defaults to True
Omega (:class:`ufl.Expr`, optional): an expression for the planet's
rotation vector. Defaults to None.
space_names (dict, optional): a dictionary of strings for names of
the function spaces to use for the spatial discretisation. The
keys are the names of the prognostic variables. Defaults to None
in which case the spaces are taken from the de Rham complex.
linearisation_map (func, optional): a function specifying which
terms in the equation set to linearise. If None is specified
then no terms are linearised. Defaults to the string 'default',
in which case the linearisation includes time derivatives and
scalar transport terms.
u_transport_option (str, optional): specifies the transport term
used for the velocity equation. Supported options are:
'vector_invariant_form', 'vector_advection_form' and
'circulation_form'.
Defaults to 'vector_invariant_form'.
no_normal_flow_bc_ids (list, optional): a list of IDs of domain
boundaries at which no normal flow will be enforced. Defaults to
None.
active_tracers (list, optional): a list of `ActiveTracer` objects
that encode the metadata for any active tracers to be included
in the equations.. Defaults to None.

Raises:
NotImplementedError: active tracers are not implemented.
"""

if linearisation_map == 'default':
# Default linearisation is time derivatives, pressure gradient,
# Coriolis and transport term from depth equation
linearisation_map = lambda t: \
(any(t.has_label(time_derivative, pressure_gradient, coriolis,
gravity, divergence, incompressible))
or (t.get(prognostic) in ['p', 'b'] and t.has_label(transport)))
super().__init__(domain=domain,
parameters=parameters,
compressible=compressible,
Omega=Omega,
space_names=space_names,
linearisation_map=linearisation_map,
u_transport_option=u_transport_option,
no_normal_flow_bc_ids=no_normal_flow_bc_ids,
active_tracers=active_tracers)

# Use the underlying routine to do a first linearisation of
# the equations
self.linearise_equation_set()
Loading