Skip to content
Draft
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
155 changes: 154 additions & 1 deletion gusto/core/coord_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
__all__ = ["xyz_from_lonlatr", "lonlatr_from_xyz", "xyz_vector_from_lonlatr",
"lonlatr_components_from_xyz", "rodrigues_rotation", "pole_rotation",
"rotated_lonlatr_vectors", "rotated_lonlatr_coords",
"periodic_distance", "great_arc_angle"]
"periodic_distance", "great_arc_angle", "xy_from_rtheta",
"rtheta_from_xy", "rtheta_from_lonlat", "lonlat_from_rtheta"]


def firedrake_or_numpy(variable):
Expand Down Expand Up @@ -531,3 +532,155 @@ def great_arc_angle(lon1, lat1, lon2, lat2, units='rad'):
arc_length *= 180.0 / pi

return arc_length


def xy_from_rtheta(r, theta, angle_units='rad'):
"""
Returns the planar cartsian x, y coordinates from the
r, theta coordinates
Args:
r (:class:`np.ndarray` or :class:`ufl.Expr`): r-coordinate.
theta (:class:`np.ndarray` or :class:`ufl.Expr`): theta-coordinate.
angle_units (str, optional): the units to use for the angle. Valid
options are 'rad' (radians) or 'deg' (degrees). Defaults to 'rad'.
Returns:
tuple of :class`np.ndarray` or tuple of :class:`ufl.Expr`: the tuple
of (x, y) coordinates in the appropriate form given the
provided arguments.
"""

# Import routines
module, _ = firedrake_or_numpy(r)
cos = module.cos
sin = module.sin
pi = module.pi

if angle_units not in ['rad', 'deg']:
raise ValueError(f'angle_units arg {angle_units} not valid')

if angle_units == 'deg':
unit_factor = 180./pi
if angle_units == 'rad':
unit_factor = 1.0

theta = theta/unit_factor

x = r * cos(theta)
y = r * sin(theta)

return x, y


def rtheta_from_xy(x, y, angle_units='rad'):
"""
Returns the r, theta coordinates (where theta is measured anticlockwise
from horizontal) from the planar Cartesian x, y coordinates.
Args:
x (:class:`np.ndarray` or :class:`ufl.Expr`): x-coordinate.
y (:class:`np.ndarray` or :class:`ufl.Expr`): y-coordinate.
angle_units (str, optional): the units to use for the angle. Valid
options are 'rad' (radians) or 'deg' (degrees). Defaults to 'rad'.
Returns:
tuple of :class`np.ndarray` or tuple of :class:`ufl.Expr`: the tuple
of (r, theta) coordinates in the appropriate form given the
provided arguments.
"""

# Import routines
module, _ = firedrake_or_numpy(x)
atan2 = module.atan2 if hasattr(module, "atan2") else module.arctan2
sqrt = module.sqrt
pi = module.pi

if angle_units not in ['rad', 'deg']:
raise ValueError(f'angle_units arg {angle_units} not valid')

if angle_units == 'deg':
unit_factor = 180./pi
if angle_units == 'rad':
unit_factor = 1.0

theta = atan2(y, x)

@12shughes 12shughes Feb 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this requires x,y to be (0,0) on the pole, which is why I previously had x_shift etc although that only works if the pole is in the centre of the mesh. Unless I've missed something, SpatialCoordinate(mesh) always gives x,y as (0,0) in the corner so would then be a different x,y to this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Would it help to add some input arguments to translate? I can't remember what you use this for...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

My original functions had Lx and Ly as input arguments to shift x and y. I use these to define the position of my cyclones, so given it's a polar plane I want to consider the centre of the mesh as (0,0) as that's the pole.

r = sqrt(x**2 + y**2)

return r, theta*unit_factor


def rtheta_from_lonlat(lon, lat, R, angle_units='rad', pole='north'):
"""
Returns the polar r theta coordinates from the lon lat coordinates
Args:
lon (:class:`np.ndarray` or :class:`ufl.Expr`): lon-coordinate.
lat (:class:`np.ndarray` or :class:`ufl.Expr`): lat-coordinate.
R (float): Planetary radius.
angle_units (str, optional): the units to use for the angle. Valid
options are 'rad' (radians) or 'deg' (degrees). Defaults to 'rad'.
pole (str, optional): which pole? Valid options are 'north' or 'south'.
Defaults to 'north'.
Returns:
tuple of :class`np.ndarray` or tuple of :class:`ufl.Expr`: the tuple
of (r, theta) coordinates in the appropriate form given the
provided arguments.
"""

# Import routines
module, _ = firedrake_or_numpy(lon)
pi = module.pi

if angle_units not in ['rad', 'deg']:
raise ValueError(f'angle_units arg {angle_units} not valid')

if angle_units == 'deg':
unit_factor = 180./pi
if angle_units == 'rad':
unit_factor = 1.0

if pole == 'south':
lat *= -1

lon_scaled = lon/unit_factor
lat_scaled = lat/unit_factor

theta = pi/2-lon_scaled
r = R*(pi/2-lat_scaled)

return r, theta*unit_factor


def lonlat_from_rtheta(r, theta, R, angle_units='rad', pole='north'):
"""
Returns the lon lat coordinates from the polar r theta coordinates
Args:
r (:class:`np.ndarray` or :class:`ufl.Expr`): r-coordinate.
theta (:class:`np.ndarray` or :class:`ufl.Expr`): theta-coordinate.
R (float): Planetary radius.
angle_units (str, optional): the units to use for the angle. Valid
options are 'rad' (radians) or 'deg' (degrees). Defaults to 'rad'.
pole (str, optional): which pole? Valid options are 'north' or 'south'.
Defaults to 'north'.
Returns:
tuple of :class`np.ndarray` or tuple of :class:`ufl.Expr`: the tuple
of (lon, lat) coordinates in the appropriate form given the
provided arguments.
"""

# Import routines
module, _ = firedrake_or_numpy(r)
pi = module.pi

if angle_units not in ['rad', 'deg']:
raise ValueError(f'angle_units arg {angle_units} not valid')

if angle_units == 'deg':
unit_factor = 180./pi
if angle_units == 'rad':
unit_factor = 1.0

theta_scaled = theta/unit_factor

lon = pi/2-theta_scaled
lat = pi/2 - r/R
if pole == 'south':
lat *= -1

return lon*unit_factor, lat*unit_factor
12 changes: 7 additions & 5 deletions gusto/core/equation_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@

class CoriolisOptions(Enum):

nonrotating = 11 # no Coriolis term
fplane = 23 # constant Coriolis term equal to f0
betaplane = 31 # Coriolis term equal to f0 + beta(y-y0)
sphere = 42 # Coriolis term equal to 2 Omega sin(lat)
nonrotating = 11 # no Coriolis term
fplane = 23 # constant Coriolis term f0
betaplane = 31 # Coriolis term f0 + beta(y-y0)
gammaplane = 37 # Coriolis term f0 - gamma r**2 with gamma=Omega/R**2 and f0=2Omega
sphere = 42 # Coriolis term 2 Omega sin(lat)


class EquationParameters(object):
Expand Down Expand Up @@ -108,9 +109,10 @@ class ShallowWaterParameters(EquationParameters):
# Coriolis options: controlled by rotation parameter
rotation = CoriolisOptions.sphere # type of Coriolis term
Omega = 7.292e-5 # rotation rate
f0 = None # f- and beta- plane Coriolis parameter
f0 = None # f-, beta- and gamma-plane Coriolis parameter
beta = None # beta-plane y-variation parameter
y0 = None # beta-plane y-centre
R = None # Radius of planet used to compute gamma in gamma-plane approx
topog_expr = None # topography expression
H = None # mean depth
# Factor that multiplies the vapour in the equivalent buoyancy
Expand Down
21 changes: 20 additions & 1 deletion gusto/equations/shallow_water_equations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
SpatialCoordinate, Constant, TrialFunctions, TestFunctions, dot, grad,
interpolate, assemble)
from firedrake.fml import subject, drop, all_terms
from gusto.core.coord_transforms import rtheta_from_xy
from gusto.core.labels import (
linearisation, pressure_gradient, coriolis, prognostic
)
Expand Down Expand Up @@ -39,6 +40,7 @@ class ShallowWaterEquations(PrognosticEquationSet):
def __init__(self, domain, parameters,
space_names=None, linearisation_map=all_terms,
u_transport_option='vector_invariant_form',
coriolis_trap=None,
no_normal_flow_bc_ids=None, active_tracers=None):
"""
Args:
Expand All @@ -60,6 +62,10 @@ def __init__(self, domain, parameters,
'vector_invariant_form', 'vector_advection_form', and
'circulation_form'.
Defaults to 'vector_invariant_form'.
coriolis_trap (tuple, optional): (r_edge, f_trap) specifies a
distance r_edge and `trap' function f_trap such that the
Coriolis parameter is set to f_trap(r) for r>r_edge. For use
with gamma-plane setup only. Defaults to None.
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.
Expand All @@ -83,6 +89,10 @@ def __init__(self, domain, parameters,
self.domain = domain
self.active_tracers = active_tracers

if coriolis_trap is not None:
assert self.parameters.rotation is CoriolisOptions.gammaplane, "coriolis_trap option is only for use with gamma-plane setup"
self.coriolis_trap = coriolis_trap

self._setup_residual(u_transport_option)

# -------------------------------------------------------------------- #
Expand Down Expand Up @@ -201,7 +211,16 @@ def _setup_residual(self, u_transport_option):
fexpr = self.parameters.f0
elif rotation is CoriolisOptions.betaplane:
xyz = SpatialCoordinate(self.domain.mesh)
fexpr = self.parameters.f0 + self.parameters.beta * xyz[1]
fexpr = self.parameters.f0 + self.parameters.beta * (
xyz[1] - self.parameters.y0)
elif rotation is CoriolisOptions.gammaplane:
x, y = SpatialCoordinate(self.domain.mesh)
r, _ = rtheta_from_xy(x, y)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's also used in here, so currently my gamma plane and trap is centred on a corner of the domain. This will probably be different for different people using it, but I imagine you'd typically want a gamma plane centred in the centre of the mesh?

Rsq = self.parameters.R**2
fexpr = 2*self.parameters.Omega * (1 - 0.5 * r**2 / Rsq)
if self.coriolis_trap is not None:
r_edge, f_trap = self.coriolis_trap
fexpr = conditional(r < r_edge, fexpr, f_trap)
else:
raise NotImplementedError('Coriolis option is not implemented')
self.prescribed_fields('coriolis', CG1).interpolate(fexpr)
Expand Down