Source code for thetis.momentum_eq

r"""
3D momentum equation for hydrostatic Boussinesq flow.

The three dimensional momentum equation reads

.. math::
    \frac{\partial \textbf{u}}{\partial t}
        + \nabla_h \cdot (\textbf{u} \textbf{u})
        + \frac{\partial \left(w\textbf{u} \right)}{\partial z}
        + f\textbf{e}_z\wedge\textbf{u} + g\nabla_h \eta + g\nabla_h r
        = \nabla_h \cdot \left( \nu_h \nabla_h \textbf{u} \right)
        + \frac{\partial }{\partial z}\left( \nu \frac{\partial \textbf{u}}{\partial z}\right)
    :label: mom_eq

where :math:`\textbf{u}` and :math:`w` denote horizontal and vertical velocity,
:math:`\nabla_h` is the horizontal gradient,
:math:`\wedge` denotes the cross product,
:math:`g` is the gravitational acceleration, :math:`f` is the Coriolis
frequency, :math:`\textbf{e}_z` is a vertical unit vector, and
:math:`\nu_h, \nu` stand for horizontal and vertical viscosity.
Water density is given by :math:`\rho = \rho'(T, S, p) + \rho_0`,
where :math:`\rho_0` is a constant reference density.
Above :math:`r` denotes the baroclinic head

.. math::
    r = \frac{1}{\rho_0} \int_{z}^\eta  \rho' d\zeta.
    :label: baroc_head

The internal pressure gradient is computed as a separate diagnostic field:

.. math::
    \mathbf{F}_{pg} = g\nabla_h r.
    :label: int_pg_eq

In the case of purely barotropic problems the :math:`r` and
:math:`\mathbf{F}_{pg}` fields are omitted.

When using mode splitting we split the velocity field into a depth average and
a deviation, :math:`\textbf{u} = \bar{\textbf{u}} + \textbf{u}'`.
Following Higdon and de Szoeke (1997) we write an equation for the deviation
:math:`\textbf{u}'`:

.. math::
    \frac{\partial \textbf{u}'}{\partial t} =
        + \nabla_h \cdot (\textbf{u} \textbf{u})
        + \frac{\partial \left(w\textbf{u} \right)}{\partial z}
        + f\textbf{e}_z\wedge\textbf{u}' + g\nabla_h r
        = \nabla_h \cdot \left( \nu_h \nabla_h \textbf{u} \right)
        + \frac{\partial }{\partial z}\left( \nu  \frac{\partial \textbf{u}}{\partial z}\right)
    :label: mom_eq_split

In :eq:`mom_eq_split` the external pressure gradient :math:`g\nabla_h \eta` vanishes and the
Coriolis term only contains the deviation :math:`\textbf{u}'`.
Advection and diffusion terms are not changed.

Higdon and de Szoeke (1997). Barotropic-Baroclinic Time Splitting for Ocean
Circulation Modeling. Journal of Computational Physics, 135(1):30-53.
http://dx.doi.org/10.1006/jcph.1997.5733
"""
from .utility import *
from .equation import Term, Equation

__all__ = [
    'MomentumEquation',
    'MomentumTerm',
    'HorizontalAdvectionTerm',
    'VerticalAdvectionTerm',
    'HorizontalViscosityTerm',
    'VerticalViscosityTerm',
    'PressureGradientTerm',
    'CoriolisTerm',
    'BottomFrictionTerm',
    'LinearDragTerm',
    'SourceTerm',
    'InternalPressureGradientCalculator',
]

g_grav = physical_constants['g_grav']
rho_0 = physical_constants['rho0']


[docs] class MomentumTerm(Term): """ Generic term for momentum equation that provides commonly used members and mapping for boundary functions. """ def __init__(self, function_space, bathymetry=None, v_elem_size=None, h_elem_size=None, use_nonlinear_equations=True, use_lax_friedrichs=True, use_bottom_friction=False, sipg_factor=Constant(1.0), sipg_factor_vertical=Constant(1.0)): """ :arg function_space: :class:`FunctionSpace` where the solution belongs :kwarg bathymetry: bathymetry of the domain :type bathymetry: 3D :class:`Function` or :class:`Constant` :kwarg v_elem_size: scalar :class:`Function` that defines the vertical element size :kwarg h_elem_size: scalar :class:`Function` that defines the horizontal element size :kwarg bool use_nonlinear_equations: If False defines the linear shallow water equations :kwarg bool use_bottom_friction: If True includes bottom friction term :kwarg sipg_factor: :class: `Constant` or :class: `Function` horizontal SIPG penalty scaling factor :kwarg sipg_factor_vertical: :class: `Constant` or :class: `Function` vertical SIPG penalty scaling factor """ super(MomentumTerm, self).__init__(function_space) self.bathymetry = bathymetry self.h_elem_size = h_elem_size self.v_elem_size = v_elem_size continuity = element_continuity(self.function_space.ufl_element()) self.horizontal_continuity = continuity.horizontal self.vertical_continuity = continuity.vertical self.use_nonlinear_equations = use_nonlinear_equations self.use_lax_friedrichs = use_lax_friedrichs self.use_bottom_friction = use_bottom_friction self.sipg_factor = sipg_factor self.sipg_factor_vertical = sipg_factor_vertical # define measures with a reasonable quadrature degree p, q = self.function_space.ufl_element().degree() self.quad_degree = (2*p + 1, 2*q + 1) self.dx = dx(degree=self.quad_degree) self.dS_h = dS_h(degree=self.quad_degree) self.dS_v = dS_v(degree=self.quad_degree) self.ds_surf = ds_surf(degree=self.quad_degree) self.ds_bottom = ds_bottom(degree=self.quad_degree)
# TODO add generic get_bnd_functions?
[docs] class PressureGradientTerm(MomentumTerm): r""" Internal pressure gradient term, :math:`g\nabla_h r` where :math:`r` is the baroclinic head :eq:`baroc_head`. Let :math:`s` denote :math:`r/H`. We can then write .. math:: F_{IPG} = g\nabla_h((s -\bar{s}) H) + g\nabla_h\left(\frac{1}{H}\right) H^2\bar{s} + g s_{bot}\nabla_h h where :math:`\bar{s},s_{bot}` are the depth average and bottom value of :math:`s`. If :math:`s` belongs to a discontinuous function space, the first term is integrated by parts. Its weak form reads .. math:: \int_\Omega g\nabla_h((s -\bar{s}) H) \cdot \boldsymbol{\psi} dx = - \int_\Omega g (s -\bar{s}) H \nabla_h \cdot \boldsymbol{\psi} dx + \int_{\mathcal{I}_h \cup \mathcal{I}_v} g (s -\bar{s}) H \boldsymbol{\psi} \cdot \textbf{n}_h dx """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): int_pg = fields.get('int_pg') if int_pg is None: return 0 f = (int_pg[0]*self.test[0] + int_pg[1]*self.test[1])*self.dx return -f
[docs] class HorizontalAdvectionTerm(MomentumTerm): r""" Horizontal advection term, :math:`\nabla_h \cdot (\textbf{u} \textbf{u})` The weak form reads .. math:: \int_\Omega \nabla_h \cdot (\textbf{u} \textbf{u}) \cdot \boldsymbol{\psi} dx = - \int_\Omega \nabla_h \boldsymbol{\psi} : (\textbf{u} \textbf{u}) dx + \int_{\mathcal{I}_h \cup \mathcal{I}_v} \textbf{u}^{\text{up}} \cdot \text{jump}(\boldsymbol{\psi} \textbf{n}_h) \cdot \text{avg}(\textbf{u}) dS where the right hand side has been integrated by parts; :math:`:` stand for the Frobenius inner product, :math:`\textbf{n}_h` is the horizontal projection of the normal vector, :math:`\textbf{u}^{\text{up}}` is the upwind value, and :math:`\text{jump}` and :math:`\text{avg}` denote the jump and average operators across the interface. """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): if not self.use_nonlinear_equations: return 0 lax_friedrichs_factor = fields_old.get('lax_friedrichs_velocity_scaling_factor') uv_depth_av = fields_old.get('uv_depth_av') if uv_depth_av is not None: uv = solution + uv_depth_av uv_old = solution_old + uv_depth_av else: uv = solution uv_old = solution_old f = -(Dx(self.test[0], 0)*uv[0]*uv_old[0] + Dx(self.test[0], 1)*uv[0]*uv_old[1] + Dx(self.test[1], 0)*uv[1]*uv_old[0] + Dx(self.test[1], 1)*uv[1]*uv_old[1])*self.dx uv_av = avg(uv_old) un_av = (uv_av[0]*self.normal('-')[0] + uv_av[1]*self.normal('-')[1]) s = 0.5*(sign(un_av) + 1.0) uv_up = uv('-')*s + uv('+')*(1-s) if self.horizontal_continuity in ['dg', 'hdiv']: f += (uv_up[0]*uv_av[0]*jump(self.test[0], self.normal[0]) + uv_up[0]*uv_av[1]*jump(self.test[0], self.normal[1]) + uv_up[1]*uv_av[0]*jump(self.test[1], self.normal[0]) + uv_up[1]*uv_av[1]*jump(self.test[1], self.normal[1]))*(self.dS_v + self.dS_h) # Lax-Friedrichs stabilization if self.use_lax_friedrichs: gamma = 0.5*abs(un_av)*lax_friedrichs_factor f += gamma*(jump(self.test[0])*jump(uv[0]) + jump(self.test[1])*jump(uv[1]))*(self.dS_v + self.dS_h) for bnd_marker in self.boundary_markers: funcs = bnd_conditions.get(bnd_marker) ds_bnd = ds_v(int(bnd_marker), degree=self.quad_degree) if funcs is None: un = dot(uv, self.normal) uv_ext = uv - 2*un*self.normal if self.use_lax_friedrichs: gamma = 0.5*abs(un)*lax_friedrichs_factor f += gamma*(self.test[0]*(uv[0] - uv_ext[0]) + self.test[1]*(uv[1] - uv_ext[1]))*ds_bnd else: uv_in = uv if 'symm' in funcs: # use internal normal velocity # NOTE should this be symmetric normal velocity? uv_ext = uv_in elif 'uv' in funcs: # prescribe external velocity uv_ext = funcs['uv'] un_ext = dot(uv_ext, self.normal) elif 'un' in funcs: # prescribe normal velocity un_ext = funcs['un'] uv_ext = self.normal*un_ext elif 'flux' in funcs: # prescribe normal volume flux sect_len = Constant(self.boundary_len[bnd_marker]) eta = fields_old['eta'] total_h = self.bathymetry + eta un_ext = funcs['flux'] / total_h / sect_len uv_ext = self.normal*un_ext else: raise Exception('Unsupported bnd type: {:}'.format(funcs.keys())) if self.use_nonlinear_equations: # add interior flux f += (uv_in[0]*self.test[0]*self.normal[0]*uv_in[0] + uv_in[0]*self.test[0]*self.normal[1]*uv_in[1] + uv_in[1]*self.test[1]*self.normal[0]*uv_in[0] + uv_in[1]*self.test[1]*self.normal[1]*uv_in[1])*ds_bnd # add boundary contribution if inflow uv_av = 0.5*(uv_in + uv_ext) un_av = uv_av[0]*self.normal[0] + uv_av[1]*self.normal[1] s = 0.5*(sign(un_av) + 1.0) f += (1-s)*((uv_ext - uv_in)[0]*self.test[0]*self.normal[0]*uv_av[0] + (uv_ext - uv_in)[0]*self.test[0]*self.normal[1]*uv_av[1] + (uv_ext - uv_in)[1]*self.test[1]*self.normal[0]*uv_av[0] + (uv_ext - uv_in)[1]*self.test[1]*self.normal[1]*uv_av[1])*ds_bnd # surf/bottom boundary conditions: closed at bed, symmetric at surf f += (uv_old[0]*uv[0]*self.test[0]*self.normal[0] + uv_old[0]*uv[1]*self.test[0]*self.normal[1] + uv_old[1]*uv[0]*self.test[1]*self.normal[0] + uv_old[1]*uv[1]*self.test[1]*self.normal[1])*(self.ds_surf) return -f
[docs] class VerticalAdvectionTerm(MomentumTerm): r""" Vertical advection term, :math:`\partial \left(w\textbf{u} \right)/(\partial z)` The weak form reads .. math:: \int_\Omega \frac{\partial \left(w\textbf{u} \right)}{\partial z} \cdot \boldsymbol{\psi} dx = - \int_\Omega \left( w \textbf{u} \right) \cdot \frac{\partial \boldsymbol{\psi}}{\partial z} dx + \int_{\mathcal{I}_{h}} \textbf{u}^{\text{up}} \cdot \text{jump}(\boldsymbol{\psi} n_z) \text{avg}(w) dS """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): w = fields_old.get('w') w_mesh = fields_old.get('w_mesh') lax_friedrichs_factor = fields_old.get('lax_friedrichs_velocity_scaling_factor') if w is None or not self.use_nonlinear_equations: return 0 f = 0 uv_depth_av = fields_old.get('uv_depth_av') if uv_depth_av is not None: uv = solution + uv_depth_av else: uv = solution vertvelo = w[2] if w_mesh is not None: vertvelo = w[2]-w_mesh adv_v = -(Dx(self.test[0], 2)*uv[0]*vertvelo + Dx(self.test[1], 2)*uv[1]*vertvelo) f += adv_v * self.dx if self.vertical_continuity in ['dg', 'hdiv']: w_av = avg(vertvelo) s = 0.5*(sign(w_av*self.normal[2]('-')) + 1.0) uv_up = uv('-')*s + uv('+')*(1-s) f += (uv_up[0]*w_av*jump(self.test[0], self.normal[2]) + uv_up[1]*w_av*jump(self.test[1], self.normal[2]))*self.dS_h if self.use_lax_friedrichs: # Lax-Friedrichs gamma = 0.5*abs(w_av*self.normal('-')[2])*lax_friedrichs_factor f += gamma*(jump(self.test[0])*jump(uv[0]) + jump(self.test[1])*jump(uv[1]))*self.dS_h f += (uv[0]*vertvelo*self.test[0]*self.normal[2] + uv[1]*vertvelo*self.test[1]*self.normal[2])*(self.ds_surf) # NOTE bottom impermeability condition is naturally satisfied by the defition of w return -f
[docs] class HorizontalViscosityTerm(MomentumTerm): r""" Horizontal viscosity term, :math:`- \nabla_h \cdot \left( \nu_h \nabla_h \textbf{u} \right)` Using the symmetric interior penalty method the weak form becomes .. math:: \int_\Omega \nabla_h \cdot \left( \nu_h \nabla_h \textbf{u} \right) \cdot \boldsymbol{\psi} dx =& -\int_\Omega \nu_h (\nabla_h \boldsymbol{\psi}) : (\nabla_h \textbf{u})^T dx \\ &+ \int_{\mathcal{I}_h \cup \mathcal{I}_v} \text{jump}(\boldsymbol{\psi} \textbf{n}_h) \cdot \text{avg}( \nu_h \nabla_h \textbf{u}) dS + \int_{\mathcal{I}_h \cup \mathcal{I}_v} \text{jump}(\textbf{u} \textbf{n}_h) \cdot \text{avg}( \nu_h \nabla_h \boldsymbol{\psi}) dS \\ &- \int_{\mathcal{I}_h \cup \mathcal{I}_v} \sigma \text{avg}(\nu_h) \text{jump}(\textbf{u} \textbf{n}_h) \cdot \text{jump}(\boldsymbol{\psi} \textbf{n}_h) dS where :math:`\sigma` is a penalty parameter, see Hillewaert (2013). Hillewaert, Koen (2013). Development of the discontinuous Galerkin method for high-resolution, large scale CFD and acoustics in industrial geometries. PhD Thesis. Université catholique de Louvain. https://dial.uclouvain.be/pr/boreal/object/boreal:128254/ """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): viscosity_h = fields_old.get('viscosity_h') sipg_factor = self.sipg_factor if viscosity_h is None: return 0 f = 0 uv_depth_av = fields_old.get('uv_depth_av') if uv_depth_av is not None: uv = solution + uv_depth_av else: uv = solution def grad_h(a): return as_matrix([[Dx(a[0], 0), Dx(a[0], 1), 0], [Dx(a[1], 0), Dx(a[1], 1), 0], [0, 0, 0]]) visc_tensor = as_matrix([[viscosity_h, 0, 0], [0, viscosity_h, 0], [0, 0, 0]]) grad_uv = grad_h(uv) grad_test = grad_h(self.test) stress = dot(visc_tensor, grad_uv) f += inner(grad_test, stress)*self.dx if self.horizontal_continuity in ['dg', 'hdiv']: h_cell = self.mesh.ufl_cell().sub_cells()[0] p, q = self.function_space.ufl_element().degree() cp = (p + 1) * (p + 2) / 2 if h_cell == triangle else (p + 1)**2 # by default the factor is multiplied by 2 to ensure convergence sigma = cp * FacetArea(self.mesh) / CellVolume(self.mesh) sp = sigma('+') sm = sigma('-') sigma_max = sipg_factor * conditional(sp > sm, sp, sm) ds_interior = (self.dS_h + self.dS_v) f += sigma_max*inner( tensor_jump(self.normal, self.test), dot(avg(visc_tensor), tensor_jump(self.normal, uv)) )*ds_interior f += -inner(avg(dot(visc_tensor, nabla_grad(self.test))), tensor_jump(self.normal, uv))*ds_interior f += -inner(tensor_jump(self.normal, self.test), avg(dot(visc_tensor, nabla_grad(uv))))*ds_interior # symmetric bottom boundary condition f += -inner(stress, outer(self.test, self.normal))*self.ds_surf f += -inner(stress, outer(self.test, self.normal))*self.ds_bottom # TODO boundary conditions # TODO impermeability condition at bottom # TODO implement as separate function return -f
[docs] class VerticalViscosityTerm(MomentumTerm): r""" Vertical viscosity term, :math:`- \frac{\partial }{\partial z}\left( \nu \frac{\partial \textbf{u}}{\partial z}\right)` Using the symmetric interior penalty method the weak form becomes .. math:: \int_\Omega \frac{\partial }{\partial z}\left( \nu \frac{\partial \textbf{u}}{\partial z}\right) \cdot \boldsymbol{\psi} dx =& -\int_\Omega \nu \frac{\partial \boldsymbol{\psi}}{\partial z} \cdot \frac{\partial \textbf{u}}{\partial z} dx \\ &+ \int_{\mathcal{I}_h} \text{jump}(\boldsymbol{\psi} n_z) \cdot \text{avg}\left(\nu \frac{\partial \textbf{u}}{\partial z}\right) dS + \int_{\mathcal{I}_h} \text{jump}(\textbf{u} n_z) \cdot \text{avg}\left(\nu \frac{\partial \boldsymbol{\psi}}{\partial z}\right) dS \\ &- \int_{\mathcal{I}_h} \sigma \text{avg}(\nu) \text{jump}(\textbf{u} n_z) \cdot \text{jump}(\boldsymbol{\psi} n_z) dS where :math:`\sigma` is a penalty parameter, see Hillewaert (2013). Hillewaert, Koen (2013). Development of the discontinuous Galerkin method for high-resolution, large scale CFD and acoustics in industrial geometries. PhD Thesis. Université catholique de Louvain. https://dial.uclouvain.be/pr/boreal/object/boreal:128254/ """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): viscosity_v = fields_old.get('viscosity_v') sipg_factor = self.sipg_factor_vertical if viscosity_v is None: return 0 f = 0 grad_test = Dx(self.test, 2) diff_flux = viscosity_v*Dx(solution, 2) f += inner(grad_test, diff_flux)*self.dx if self.vertical_continuity in ['dg', 'hdiv']: p, q = self.function_space.ufl_element().degree() cp = (q + 1)**2 l_normal = CellVolume(self.mesh) / FacetArea(self.mesh) # by default the factor is multiplied by 2 to ensure convergence sigma = sipg_factor * cp / l_normal sp = sigma('+') sm = sigma('-') sigma_max = conditional(sp > sm, sp, sm) ds_interior = (self.dS_h) f += sigma_max*inner( tensor_jump(self.normal[2], self.test), avg(viscosity_v)*tensor_jump(self.normal[2], solution) )*ds_interior f += -inner(avg(viscosity_v*Dx(self.test, 2)), tensor_jump(self.normal[2], solution))*ds_interior f += -inner(tensor_jump(self.normal[2], self.test), avg(viscosity_v*Dx(solution, 2)))*ds_interior return -f
[docs] class BottomFrictionTerm(MomentumTerm): r""" Quadratic bottom friction term, :math:`\tau_b = C_D \| \textbf{u}_b \| \textbf{u}_b` The weak formulation reads .. math:: \int_{\Gamma_{bot}} \tau_b \cdot \boldsymbol{\psi} dx = \int_{\Gamma_{bot}} C_D \| \textbf{u}_b \| \textbf{u}_b \cdot \boldsymbol{\psi} dx where :math:`\textbf{u}_b` is reconstructed velocity in the middle of the bottom element: .. math:: \textbf{u}_b = \textbf{u}\Big|_{\Gamma_{bot}} + \frac{\partial \textbf{u}}{\partial z}\Big|_{\Gamma_{bot}} h_b, :math:`h_b` being half of the element height. For implicit solvers we linearize the stress as :math:`\tau_b = C_D \| \textbf{u}_b^{n} \| \textbf{u}_b^{n+1}` The drag is computed from the law-of-the wall .. math:: C_D = \left( \frac{\kappa}{\ln (h_b + z_0)/z_0} \right)^2 where :math:`z_0` is the bottom roughness length field. The user can override the :math:`C_D` value by providing ``quadratic_drag_coefficient`` field. """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): f = 0 if self.use_bottom_friction: uv_depth_av = fields_old.get('uv_depth_av') if uv_depth_av is not None: uv = solution + uv_depth_av uv_old = solution_old + uv_depth_av else: uv = solution uv_old = solution_old drag = fields_old.get('quadratic_drag_coefficient') if drag is None: z0 = fields_old.get('bottom_roughness') assert z0 is not None, \ 'if use_bottom_friction=True, either bottom_roughness or quadratic_drag_coefficient must be defined' assert self.v_elem_size is not None kappa = physical_constants['von_karman'] h = self.v_elem_size # compute drag coefficient from an analytical p1dg fit to the # logarithmic velocity profile in the botton element b = -7./4*h - 3./2*z0 + (h + 5./2*z0)*ln((h + z0)/z0) drag = (kappa*h/b)**2 uv_old_mag = sqrt(uv_old[0]**2 + uv_old[1]**2) bfr = drag * uv_old_mag f += bfr * (self.test[0]*uv[0] + self.test[1]*uv[1])*self.ds_bottom return -f
[docs] class LinearDragTerm(MomentumTerm): r""" Linear drag term, :math:`\tau_b = D \textbf{u}_b` where :math:`D` is the drag coefficient, read from ``linear_drag_coefficient`` field. """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): linear_drag_coefficient = fields_old.get('linear_drag_coefficient') f = 0 # Linear drag (consistent with drag in 2D mode) if linear_drag_coefficient is not None: uv_depth_av = fields_old.get('uv_depth_av') if uv_depth_av is not None: uv = solution + uv_depth_av else: uv = solution bottom_fri = linear_drag_coefficient*inner(self.test, uv)*self.dx f += bottom_fri return -f
[docs] class CoriolisTerm(MomentumTerm): r""" Coriolis term, :math:`f\textbf{e}_z\wedge \bar{\textbf{u}}` """
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): coriolis = fields_old.get('coriolis') f = 0 if coriolis is not None: f += coriolis*(-solution[1]*self.test[0] + solution[0]*self.test[1])*self.dx return -f
[docs] class SourceTerm(MomentumTerm): r""" Generic momentum source term The weak form reads .. math:: F_s = \int_\Omega \sigma \cdot \boldsymbol{\psi} dx where :math:`\sigma` is a user defined vector valued :class:`Function`. This term also implements the wind stress, :math:`-\tau_w/(H \rho_0)`. :math:`\tau_w` is a user-defined wind stress :class:`Function` ``wind_stress``. The weak form is .. math:: F_w = \int_{\Gamma_s} \frac{1}{\rho_0} \tau_w \cdot \boldsymbol{\psi} dx Wind stress is only included if vertical viscosity is provided. """ # TODO implement wind stress as a separate term
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): f = 0 source = fields_old.get('source') viscosity_v = fields_old.get('viscosity_v') wind_stress = fields_old.get('wind_stress') if wind_stress is not None and viscosity_v is None: warning('Wind stress is prescribed but vertical viscosity is not:\n Wind stress will be ignored.') if viscosity_v is not None: # wind stress if wind_stress is not None: f -= (wind_stress[0]*self.test[0] + wind_stress[1]*self.test[1])/rho_0*self.ds_surf if source is not None: f += - inner(source, self.test)*self.dx return -f
[docs] class MomentumEquation(Equation): """ Hydrostatic 3D momentum equation :eq:`mom_eq_split` for mode split models """ def __init__(self, function_space, bathymetry=None, v_elem_size=None, h_elem_size=None, use_nonlinear_equations=True, use_lax_friedrichs=True, use_bottom_friction=False, sipg_factor=Constant(1.0), sipg_factor_vertical=Constant(1.0)): """ :arg function_space: :class:`FunctionSpace` where the solution belongs :kwarg bathymetry: bathymetry of the domain :type bathymetry: 3D :class:`Function` or :class:`Constant` :kwarg v_elem_size: scalar :class:`Function` that defines the vertical element size :kwarg h_elem_size: scalar :class:`Function` that defines the horizontal element size :kwarg bool use_nonlinear_equations: If False defines the linear shallow water equations :kwarg bool use_bottom_friction: If True includes bottom friction term :kwarg sipg_factor: :class: `Constant` or :class: `Function` horizontal SIPG penalty scaling factor :kwarg sipg_factor_vertical: :class: `Constant` or :class: `Function` vertical SIPG penalty scaling factor """ # TODO rename for reflect the fact that this is eq for the split eqns super(MomentumEquation, self).__init__(function_space) args = (function_space, bathymetry, v_elem_size, h_elem_size, use_nonlinear_equations, use_lax_friedrichs, use_bottom_friction, sipg_factor, sipg_factor_vertical) self.add_term(PressureGradientTerm(*args), 'source') self.add_term(HorizontalAdvectionTerm(*args), 'explicit') self.add_term(VerticalAdvectionTerm(*args), 'explicit') self.add_term(HorizontalViscosityTerm(*args), 'explicit') self.add_term(VerticalViscosityTerm(*args), 'explicit') self.add_term(BottomFrictionTerm(*args), 'explicit') self.add_term(LinearDragTerm(*args), 'explicit') self.add_term(CoriolisTerm(*args), 'explicit') self.add_term(SourceTerm(*args), 'source')
[docs] class InternalPressureGradientCalculator(MomentumTerm): r""" Computes the internal pressure gradient term, :math:`g\nabla_h r` where :math:`r` is the baroclinic head :eq:`baroc_head`. If :math:`r` belongs to a discontinuous function space, the term is integrated by parts: .. math:: \int_\Omega g \nabla_h r \cdot \boldsymbol{\psi} dx = - \int_\Omega g r \nabla_h \cdot \boldsymbol{\psi} dx + \int_{\mathcal{I}_h \cup \mathcal{I}_v} g \text{avg}(r) \text{jump}(\boldsymbol{\psi} \cdot \textbf{n}_h) dx .. note :: Due to the :class:`Term` sign convention this term is assembled on the right-hand-side. """ def __init__(self, fields, bathymetry, bnd_functions, internal_pg_scalar=None, solver_parameters=None): """ :arg solver: `class`FlowSolver` object :kwarg dict solver_parameters: PETSc solver options """ if solver_parameters is None: solver_parameters = {} self.fields = fields self.internal_pg_scalar = internal_pg_scalar function_space = self.fields.int_pg_3d.function_space() super(InternalPressureGradientCalculator, self).__init__( function_space, bathymetry=bathymetry) solution = self.fields.int_pg_3d fields = { 'baroc_head': self.fields.baroc_head_3d, } l = -self.residual(solution, solution, fields, fields, bnd_conditions=bnd_functions) trial = TrialFunction(self.function_space) a = inner(trial, self.test) * self.dx prob = LinearVariationalProblem(a, l, solution) self.lin_solver = LinearVariationalSolver(prob, solver_parameters=solver_parameters)
[docs] def solve(self): """ Computes internal pressure gradient and stores it in int_pg_3d field """ self.lin_solver.solve()
[docs] def residual(self, solution, solution_old, fields, fields_old, bnd_conditions): bhead = fields_old.get('baroc_head') if bhead is None: return 0 by_parts = element_continuity(bhead.function_space().ufl_element()).horizontal == 'dg' if by_parts: div_test = (Dx(self.test[0], 0) + Dx(self.test[1], 1)) f = -g_grav*bhead*div_test*self.dx head_star = avg(bhead) jump_n_dot_test = (jump(self.test[0], self.normal[0]) + jump(self.test[1], self.normal[1])) f += g_grav*head_star*jump_n_dot_test*(self.dS_v + self.dS_h) n_dot_test = (self.normal[0]*self.test[0] + self.normal[1]*self.test[1]) f += g_grav*bhead*n_dot_test*(self.ds_bottom + self.ds_surf) for bnd_marker in self.boundary_markers: funcs = bnd_conditions.get(bnd_marker) ds_bnd = ds_v(int(bnd_marker), degree=self.quad_degree) if bhead is not None: if funcs is not None and 'baroc_head' in funcs: r_ext = funcs['baroc_head'] head_ext = r_ext head_in = bhead head_star = 0.5*(head_ext + head_in) else: head_star = bhead f += g_grav*head_star*n_dot_test*ds_bnd else: grad_head_dot_test = (Dx(bhead, 0)*self.test[0] + Dx(bhead, 1)*self.test[1]) f = g_grav * grad_head_dot_test * self.dx if self.internal_pg_scalar is not None: f = self.internal_pg_scalar*f return -f