Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into TBendall/HydrostaticT…
Browse files Browse the repository at this point in the history
…ests
  • Loading branch information
tommbendall committed Dec 18, 2024
2 parents 684e5b1 + 3eaecc5 commit b2f5b3c
Show file tree
Hide file tree
Showing 16 changed files with 614 additions and 150 deletions.
9 changes: 2 additions & 7 deletions examples/compressible_euler/unsaturated_bubble.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 +216,11 @@ def unsaturated_bubble(
R_v = eqns.parameters.R_v
epsilon = R_d / R_v

# make expressions for determining water_v0
exner = thermodynamics.exner_pressure(eqns.parameters, rho_averaged, theta0)
p = thermodynamics.p(eqns.parameters, exner)
T = thermodynamics.T(eqns.parameters, theta0, exner, water_v0)
r_v_expr = thermodynamics.r_v(eqns.parameters, rel_hum, T, p)

# make expressions to evaluate residual
exner_expr = thermodynamics.exner_pressure(eqns.parameters, rho_averaged, theta0)
p_expr = thermodynamics.p(eqns.parameters, exner_expr)
T_expr = thermodynamics.T(eqns.parameters, theta0, exner_expr, water_v0)
water_v_expr = thermodynamics.r_v(eqns.parameters, rel_hum, T_expr, p_expr)
rel_hum_expr = thermodynamics.RH(eqns.parameters, water_v0, T_expr, p_expr)
rel_hum_eval = Function(Vt)

Expand All @@ -247,7 +242,7 @@ def unsaturated_bubble(

# first solve for r_v
for _ in range(max_inner_solve_count):
water_v_eval.interpolate(r_v_expr)
water_v_eval.interpolate(water_v_expr)
water_v0.assign(water_v0 * (1 - delta) + delta * water_v_eval)

# compute theta_vd
Expand Down
6 changes: 5 additions & 1 deletion gusto/core/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ def __setattr__(self, name, value):

# Almost all parameters should be Constants -- but there are some
# specific exceptions which should be kept as integers
if type(value) in [float, int] and name not in ['dumpfreq', 'pddumpfreq', 'chkptfreq']:
non_constants = [
'dumpfreq', 'pddumpfreq', 'chkptfreq',
'fixed_subcycles', 'max_subcycles', 'subcycle_by_courant'
]
if type(value) in [float, int] and name not in non_constants:
object.__setattr__(self, name, Constant(value))
else:
object.__setattr__(self, name, value)
Expand Down
9 changes: 6 additions & 3 deletions gusto/core/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

from gusto.core.coordinates import Coordinates
from gusto.core.function_spaces import Spaces, check_degree_args
from firedrake import (Constant, SpatialCoordinate, sqrt, CellNormal, cross,
inner, grad, VectorFunctionSpace, Function, FunctionSpace,
perp)
from firedrake import (
Constant, SpatialCoordinate, sqrt, CellNormal, cross, inner, grad,
VectorFunctionSpace, Function, FunctionSpace, perp, curl
)
import numpy as np


Expand Down Expand Up @@ -124,12 +125,14 @@ def __init__(self, mesh, dt, family, degree=None,
V = VectorFunctionSpace(mesh, "DG", sphere_degree)
self.outward_normals = Function(V).interpolate(CellNormal(mesh))
self.perp = lambda u: cross(self.outward_normals, u)
self.divperp = lambda u: inner(self.outward_normals, curl(u))
else:
kvec = [0.0]*dim
kvec[dim-1] = 1.0
self.k = Constant(kvec)
if dim == 2:
self.perp = perp
self.divperp = lambda u: -u[0].dx(1) + u[1].dx(0)

# -------------------------------------------------------------------- #
# Construct information relating to height/radius
Expand Down
43 changes: 42 additions & 1 deletion gusto/diagnostics/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"XComponent", "YComponent", "ZComponent", "MeridionalComponent",
"ZonalComponent", "RadialComponent", "Energy", "KineticEnergy",
"Sum", "Difference", "SteadyStateError", "Perturbation",
"Divergence", "TracerDensity", "IterativeDiagnosticField"]
"Divergence", "TracerDensity", "IterativeDiagnosticField",
"CumulativeSum"]


class Diagnostics(object):
Expand Down Expand Up @@ -1040,3 +1041,43 @@ def setup(self, domain, state_fields):

else:
super().setup(domain, state_fields)


class CumulativeSum(DiagnosticField):
"""Base diagnostic for cumulatively summing a field in time."""
def __init__(self, name):
"""
Args:
name (str): name of the field to take the cumulative sum of.
"""
self.field_name = name
self.integral_name = name+"_cumulative"
super().__init__(self, method='assign', required_fields=(self.field_name,))

def setup(self, domain, state_fields):
"""
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.
"""

# Gather the field to be summed
self.integrand = state_fields(self.field_name)
self.space = self.integrand.function_space()

# Create a new field to store the cumulative sum
self.field = state_fields(self.integral_name, space=self.space, dump=True, pick_up=True)
# Initialise the new field to zero, if picking up from a checkpoint
# file the original cumulative field will load and not be overwritten.
self.field.assign(0.0)

def compute(self):
"""Increment the cumulative sum diagnostic."""
self.field.assign(self.field + self.integrand)

@property
def name(self):
"""Gives the name of this diagnostic field."""
return self.integral_name
1 change: 1 addition & 0 deletions gusto/spatial_methods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from gusto.spatial_methods.diffusion_methods import * # noqa
from gusto.spatial_methods.transport_methods import * # noqa
from gusto.spatial_methods.limiters import * # noqa
from gusto.spatial_methods.augmentation import * # noqa
238 changes: 238 additions & 0 deletions gusto/spatial_methods/augmentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
"""
A module defining objects for temporarily augmenting an equation with another.
"""


from abc import ABCMeta, abstractmethod
from firedrake import (
MixedFunctionSpace, Function, TestFunctions, split, inner, dx, grad,
LinearVariationalProblem, LinearVariationalSolver, lhs, rhs, dot,
ds_b, ds_v, ds_t, ds, FacetNormal, TestFunction, TrialFunction,
transpose, nabla_grad, outer, dS, dS_h, dS_v, sign, jump, div,
Constant, sqrt, cross, curl, FunctionSpace, assemble, DirichletBC
)
from firedrake.fml import subject
from gusto import (
time_derivative, transport, transporting_velocity, TransportEquationType,
logger
)


class Augmentation(object, metaclass=ABCMeta):
"""
Augments an equation with another equation to be solved simultaneously.
"""

@abstractmethod
def pre_apply(self, x_in):
"""
Steps to take at the beginning of an apply method, for instance to
assign the input field to the internal mixed function.
"""

pass

@abstractmethod
def post_apply(self, x_out):
"""
Steps to take at the end of an apply method, for instance to assign the
internal mixed function to the output field.
"""

pass

@abstractmethod
def update(self, x_in_mixed):
"""
Any intermediate update steps, depending on the current mixed function.
"""

pass


class VorticityTransport(Augmentation):
"""
Solves the transport of a vector field, simultaneously with the vorticity
as a mixed proble, as described in Bendall and Wimmer (2022).
Note that this is most effective with implicit time discretisations. The
residual-based SUPG option provides a dissipation method.
Args:
domain (:class:`Domain`): The domain object.
eqns (:class:`PrognosticEquationSet`): The overarching equation set.
transpose_commutator (bool, optional): Whether to include the commutator
of the transpose gradient terms. This is necessary for solving the
general vector transport equation, but is not necessary when the
transporting and transported fields are the same. Defaults to True.
supg (bool, optional): Whether to include dissipation through a
residual-based SUPG scheme. Defaults to False.
"""

def __init__(
self, domain, eqns, transpose_commutator=True, supg=False
):

V_vel = domain.spaces('HDiv')
V_vort = domain.spaces('H1')

self.fs = MixedFunctionSpace((V_vel, V_vort))
self.X = Function(self.fs)
self.tests = TestFunctions(self.fs)

u = Function(V_vel)
F, Z = split(self.X)
test_F, test_Z = self.tests

if hasattr(domain.mesh, "_base_mesh"):
self.ds = ds_b + ds_t + ds_v
self.dS = dS_v + dS_h
else:
self.ds = ds
self.dS = dS

# Add boundary conditions
self.bcs = []
if 'u' in eqns.bcs.keys():
for bc in eqns.bcs['u']:
self.bcs.append(
DirichletBC(self.fs.sub(0), bc.function_arg, bc.sub_domain)
)

# Set up test function and the vorticity term
n = FacetNormal(domain.mesh)
sign_u = 0.5*(sign(dot(u, n)) + 1)
upw = lambda f: (sign_u('+')*f('+') + sign_u('-')*f('-'))

if domain.mesh.topological_dimension() == 2:
mix_test = test_F - domain.perp(grad(test_Z))
F_cross_u = Z*domain.perp(u)
elif domain.mesh.topological_dimension == 3:
mix_test = test_F - curl(test_Z)
F_cross_u = cross(Z, u)

time_deriv_form = inner(F, test_F)*dx + inner(Z, test_Z)*dx

# Standard vector invariant transport form -----------------------------
transport_form = (
# vorticity term
inner(mix_test, F_cross_u)*dx
+ inner(n, test_Z*Z*u)*self.ds
# 0.5*grad(v . F)
- 0.5 * div(mix_test) * inner(u, F)*dx
+ 0.5 * inner(mix_test, n) * inner(u, F)*self.ds
)

# Communtator of tranpose gradient terms -------------------------------
# This is needed for general vector transport
if transpose_commutator:
u_dot_nabla_F = dot(u, transpose(nabla_grad(F)))
transport_form += (
- inner(n, test_Z*domain.perp(u_dot_nabla_F))*self.ds
# + 0.5*grad(F).v
- 0.5 * dot(F, div(outer(u, mix_test)))*dx
+ 0.5 * inner(mix_test('+'), n('+'))*dot(jump(u), upw(F))*self.dS
# - 0.5*grad(v).F
+ 0.5 * dot(u, div(outer(F, mix_test)))*dx
- 0.5 * inner(mix_test('+'), n('+'))*dot(jump(F), upw(u))*self.dS
)

# SUPG terms -----------------------------------------------------------
# Add the vorticity residual to the transported vorticity,
# which damps enstrophy
if supg:

# Determine SUPG coefficient ---------------------------------------
tau = 0.5*domain.dt

# Find mean grid spacing to determine a Courant number
DG0 = FunctionSpace(domain.mesh, 'DG', 0)
ones = Function(DG0).interpolate(Constant(1.0))
area = assemble(ones*dx)
mean_dx = (area/DG0.dof_count)**(1/domain.mesh.geometric_dimension())

# Divide by approximately (1 + c)
tau /= (1.0 + sqrt(dot(u, u))*domain.dt/Constant(mean_dx))

dxqp = dx(degree=3)

if domain.mesh.topological_dimension() == 2:
time_deriv_form -= inner(mix_test, tau*Z*domain.perp(u)/domain.dt)*dxqp
transport_form -= inner(
mix_test, tau*domain.perp(u)*domain.divperp(Z*domain.perp(u))
)*dxqp
if transpose_commutator:
transport_form -= inner(
mix_test,
tau*domain.perp(u)*domain.divperp(u_dot_nabla_F)
)*dxqp
elif domain.mesh.topological_dimension() == 3:
time_deriv_form -= inner(mix_test, tau*cross(Z, u)/domain.dt)*dxqp
transport_form -= inner(
mix_test, tau*cross(curl(Z*u), u)
)*dxqp
if transpose_commutator:
transport_form -= inner(
mix_test,
tau*cross(curl(u_dot_nabla_F), u)
)*dxqp

# Assemble the residual ------------------------------------------------
residual = (
time_derivative(time_deriv_form)
+ transport(
transport_form, TransportEquationType.vector_invariant
)
)
residual = transporting_velocity(residual, u)

self.residual = subject(residual, self.X)

self.x_in = Function(self.fs)
self.Z_in = Function(V_vort)
self.x_out = Function(self.fs)

vort_test = TestFunction(V_vort)
vort_trial = TrialFunction(V_vort)

F_in, _ = split(self.x_in)

eqn = (
inner(vort_trial, vort_test)*dx
+ inner(domain.perp(grad(vort_test)), F_in)*dx
+ vort_test*inner(n, domain.perp(F_in))*self.ds
)
problem = LinearVariationalProblem(
lhs(eqn), rhs(eqn), self.Z_in, constant_jacobian=True
)
self.solver = LinearVariationalSolver(problem)

def pre_apply(self, x_in):
"""
Sets the velocity field for the local mixed function.
Args:
x_in (:class:`Function`): The input velocity field
"""
self.x_in.subfunctions[0].assign(x_in)

def post_apply(self, x_out):
"""
Sets the output velocity field from the local mixed function.
Args:
x_out (:class:`Function`): the output velocity field.
"""
x_out.assign(self.x_out.subfunctions[0])

def update(self, x_in_mixed):
"""
Performs the solve to determine the vorticity function.
Args:
x_in_mixed (:class:`Function`): The mixed function to update.
"""
self.x_in.subfunctions[0].assign(x_in_mixed.subfunctions[0])
logger.debug('Vorticity solve')
self.solver.solve()
self.x_in.subfunctions[1].assign(self.Z_in)
Loading

0 comments on commit b2f5b3c

Please sign in to comment.