Source code for thetis.solver

"""
Module for three dimensional baroclinic solver
"""
from .utility import *
from .utility3d import *
from . import shallowwater_eq
from . import momentum_eq
from . import tracer_eq
from . import turbulence
from . import coupled_timeintegrator
import thetis.limiter as limiter
import time as time_mod
from mpi4py import MPI
from . import exporter
import weakref
from .field_defs import field_metadata
from .options import ModelOptions3d
from . import callback
from .log import *
from collections import OrderedDict
import numpy
import datetime


[docs] class FlowSolver(FrozenClass): """ Main object for 3D solver **Example** Create 2D mesh .. code-block:: python from thetis import * mesh2d = RectangleMesh(20, 20, 10e3, 10e3) Create bathymetry function, set a constant value .. code-block:: python fs_p1 = FunctionSpace(mesh2d, 'CG', 1) bathymetry_2d = Function(fs_p1, name='Bathymetry').assign(10.0) Create a 3D model with 6 uniform levels, and set some options (see :class:`.ModelOptions3d`) .. code-block:: python solver_obj = solver.FlowSolver(mesh2d, bathymetry_2d, n_layers=6) options = solver_obj.options options.element_family = 'dg-dg' options.polynomial_degree = 1 options.timestepper_type = 'SSPRK22' options.timestepper_options.use_automatic_timestep = False options.solve_salinity = False options.solve_temperature = False options.simulation_export_time = 50.0 options.simulation_end_time = 3600. options.timestep = 25.0 Assign initial condition for water elevation .. code-block:: python solver_obj.create_function_spaces() init_elev = Function(solver_obj.function_spaces.H_2d) coords = SpatialCoordinate(mesh2d) init_elev.project(2.0*exp(-((coords[0] - 4e3)**2 + (coords[1] - 4.5e3)**2)/2.2e3**2)) solver_obj.assign_initial_conditions(elev=init_elev) Run simulation .. code-block:: python solver_obj.iterate() See the manual for more complex examples. """ @unfrozen @PETSc.Log.EventDecorator("thetis.FlowSolver.__init__") def __init__(self, mesh2d, bathymetry_2d, n_layers, options=None, extrude_options=None, keep_log=False): """ :arg mesh2d: :class:`Mesh` object of the 2D mesh :arg bathymetry_2d: Bathymetry of the domain. Bathymetry stands for the mean water depth (positive downwards). :type bathymetry_2d: 2D :class:`Function` :arg int n_layers: Number of layers in the vertical direction. Elements are distributed uniformly over the vertical. :kwarg options: Model options (optional). Model options can also be changed directly via the :attr:`.options` class property. :type options: :class:`.ModelOptions3d` instance :kwarg bool keep_log: append to an existing log file, or overwrite it? """ self._initialized = False self.bathymetry_cg_2d = bathymetry_2d self.mesh2d = mesh2d """2D :class`Mesh`""" if extrude_options is None: extrude_options = {} self.mesh = extrude_mesh_sigma(mesh2d, n_layers, bathymetry_2d, **extrude_options) """3D :class`Mesh`""" self.comm = mesh2d.comm # add boundary length info bnd_len = compute_boundary_length(self.mesh2d) self.mesh2d.boundary_len = bnd_len self.mesh.boundary_len = bnd_len self.dt = None """Time step""" self.dt_2d = None """Time of the 2D solver""" self.M_modesplit = None """Mode split ratio (int)""" # override default options self.options = ModelOptions3d() """ Dictionary of all options. A :class:`.ModelOptions3d` object. """ if options is not None: self.options.update(options) # simulation time step bookkeeping self.simulation_time = 0 self.iteration = 0 self.i_export = 0 self.next_export_t = self.simulation_time + self.options.simulation_export_time self.bnd_functions = {'shallow_water': {}, 'momentum': {}, 'salt': {}, 'temp': {}, } self.callbacks = callback.CallbackManager() """ :class:`.CallbackManager` object that stores all callbacks """ self.fields = FieldDict() """ :class:`.FieldDict` that holds all functions needed by the solver object """ self.function_spaces = AttrDict() """ :class:`.AttrDict` that holds all function spaces needed by the solver object """ self.export_initial_state = True """Do export initial state. False if continuing a simulation""" self._simulation_continued = False self.keep_log = keep_log self._field_preproc_funcs = {}
[docs] def compute_dx_factor(self): """ Computes normalized distance between nodes in the horizontal direction The factor depends on the finite element space and its polynomial degree. It is used to compute maximal stable time steps. """ p = self.options.polynomial_degree if self.options.element_family in ['rt-dg', 'bdm-dg']: # velocity space is essentially p+1 p = self.options.polynomial_degree + 1 # assuming DG basis functions on triangles l_r = p**2/3.0 + 7.0/6.0*p + 1.0 factor = 0.5*0.25/l_r return factor
[docs] def compute_dz_factor(self): """ Computes a normalized distance between nodes in the vertical direction The factor depends on the finite element space and its polynomial degree. It is used to compute maximal stable time steps. """ p = self.options.polynomial_degree # assuming DG basis functions in an interval l_r = 1.0/max(p, 1) factor = 0.5*0.25/l_r return factor
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.compute_dt_2d") def compute_dt_2d(self, u_scale): r""" Computes maximum explicit time step from CFL condition. .. math :: \Delta t = \frac{\Delta x}{U} Assumes velocity scale :math:`U = \sqrt{g H} + U_{scale}` where :math:`U_{scale}` is estimated advective velocity. :arg u_scale: User provided maximum advective velocity scale :type u_scale: float or :class:`Constant` """ csize = self.fields.h_elem_size_2d bath = self.fields.bathymetry_2d fs = bath.function_space() bath_pos = Function(fs, name='bathymetry') bath_pos.assign(bath) min_depth = 0.05 bath_pos.dat.data[bath_pos.dat.data < min_depth] = min_depth test = TestFunction(fs) trial = TrialFunction(fs) solution = Function(fs) g = physical_constants['g_grav'] u = (sqrt(g * bath_pos) + u_scale) a = inner(test, trial) * dx l = inner(test, csize / u) * dx sp = { "snes_type": "ksponly", "ksp_type": "cg", "pc_type": "bjacobi", "sub_pc_type": "ilu", } solve(a == l, solution, solver_parameters=sp) dt = float(solution.dat.data.min()) dt = self.comm.allreduce(dt, op=MPI.MIN) dt *= self.compute_dx_factor() return dt
[docs] def compute_dt_h_advection(self, u_scale): r""" Computes maximum explicit time step for horizontal advection .. math :: \Delta t = \frac{\Delta x}{U_{scale}} where :math:`U_{scale}` is estimated horizontal advective velocity. :arg u_scale: User provided maximum horizontal velocity scale :type u_scale: float or :class:`Constant` """ u = u_scale if isinstance(u_scale, Constant): u = float(u_scale) min_dx = self.fields.h_elem_size_2d.dat.data.min() # alpha = 0.5 if self.options.element_family == 'rt-dg' else 1.0 # dt = alpha*1.0/10.0/(self.options.polynomial_degree + 1)*min_dx/u min_dx *= self.compute_dx_factor() dt = min_dx/u dt = self.comm.allreduce(dt, op=MPI.MIN) return dt
[docs] def compute_dt_v_advection(self, w_scale): r""" Computes maximum explicit time step for vertical advection .. math :: \Delta t = \frac{\Delta z}{W_{scale}} where :math:`W_{scale}` is estimated vertical advective velocity. :arg w_scale: User provided maximum vertical velocity scale :type w_scale: float or :class:`Constant` """ w = w_scale if isinstance(w_scale, Constant): w = float(w_scale) min_dz = self.fields.v_elem_size_2d.dat.data.min() # alpha = 0.5 if self.options.element_family == 'rt-dg' else 1.0 # dt = alpha*1.0/1.5/(self.options.polynomial_degree + 1)*min_dz/w min_dz *= self.compute_dz_factor() dt = min_dz/w dt = self.comm.allreduce(dt, op=MPI.MIN) return dt
[docs] def compute_dt_diffusion(self, nu_scale): r""" Computes maximum explicit time step for horizontal diffusion. .. math :: \Delta t = \alpha \frac{(\Delta x)^2}{\nu_{scale}} where :math:`\nu_{scale}` is estimated diffusivity scale. """ nu = nu_scale if isinstance(nu_scale, Constant): nu = float(nu_scale) min_dx = self.fields.h_elem_size_2d.dat.data.min() factor = 2.0 if self.options.element_family == 'bdm-dg': factor = 1.8 if self.options.timestepper_type == 'LeapFrog': factor *= 0.6 min_dx *= factor*self.compute_dx_factor() dt = (min_dx)**2/nu dt = self.comm.allreduce(dt, op=MPI.MIN) return dt
[docs] def compute_mesh_stats(self): """ Computes number of elements, nodes etc and prints to sdtout """ nnodes = self.function_spaces.P1_2d.dim() P1DG_2d = self.function_spaces.P1DG_2d nelem2d = int(P1DG_2d.dim()/P1DG_2d.ufl_cell().num_vertices()) nlayers = self.mesh.topology.layers - 1 nprisms = nelem2d*nlayers dofs_elev2d = self.function_spaces.H_2d.dim() dofs_u2d = self.function_spaces.U_2d.dim() dofs_u3d = self.function_spaces.U.dim() dofs_w3d = self.function_spaces.W.dim() dofs_t3d = self.function_spaces.H.dim() dofs_t3d_core = int(dofs_t3d/self.comm.size) min_h_size = self.comm.allreduce(self.fields.h_elem_size_2d.dat.data.min(), MPI.MIN) max_h_size = self.comm.allreduce(self.fields.h_elem_size_2d.dat.data.max(), MPI.MAX) min_v_size = self.comm.allreduce(self.fields.v_elem_size_3d.dat.data.min(), MPI.MIN) max_v_size = self.comm.allreduce(self.fields.v_elem_size_3d.dat.data.max(), MPI.MAX) print_output(f'Element family: {self.options.element_family}, degree: {self.options.polynomial_degree}') print_output(f'2D cell type: {self.mesh2d.ufl_cell()}') print_output(f'2D mesh: {nnodes} vertices, {nelem2d} elements') print_output(f'3D mesh: {nlayers} layers, {nprisms} prisms') print_output(f'Horizontal element size: {min_h_size:.2f} ... {max_h_size:.2f} m') print_output(f'Vertical element size: {min_v_size:.3f} ... {max_v_size:.3f} m') print_output(f'Number of 2D elevation DOFs: {dofs_elev2d}') print_output(f'Number of 2D velocity DOFs: {dofs_u2d}') print_output(f'Number of 3D h. velocity DOFs: {dofs_u3d}') print_output(f'Number of 3D v. velocity DOFs: {dofs_w3d}') print_output(f'Number of 3D tracer DOFs: {dofs_t3d}') print_output(f'Number of cores: {self.comm.size}') print_output(f'Tracer DOFs per core: ~{dofs_t3d_core:.1f}')
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.set_time_step") def set_time_step(self): """ Sets the model the model time step If the time integrator supports automatic time step, and :attr:`ModelOptions3d.timestepper_options.use_automatic_timestep` is `True`, we compute the maximum time step allowed by the CFL condition. Otherwise uses :attr:`ModelOptions3d.timestep`. Once the time step is determined, will adjust it to be an integer fraction of export interval ``options.simulation_export_time``. """ automatic_timestep = (hasattr(self.options.timestepper_options, 'use_automatic_timestep') and self.options.timestepper_options.use_automatic_timestep) cfl2d = self.timestepper.cfl_coeff_2d cfl3d = self.timestepper.cfl_coeff_3d max_dt_swe = self.compute_dt_2d(self.options.horizontal_velocity_scale) max_dt_hadv = self.compute_dt_h_advection(self.options.horizontal_velocity_scale) max_dt_vadv = self.compute_dt_v_advection(self.options.vertical_velocity_scale) max_dt_diff = self.compute_dt_diffusion(self.options.horizontal_viscosity_scale) print_output(' - dt 2d swe: {:}'.format(max_dt_swe)) print_output(' - dt h. advection: {:}'.format(max_dt_hadv)) print_output(' - dt v. advection: {:}'.format(max_dt_vadv)) print_output(' - dt viscosity: {:}'.format(max_dt_diff)) max_dt_2d = cfl2d*max_dt_swe max_dt_3d = cfl3d*min(max_dt_hadv, max_dt_vadv, max_dt_diff) print_output(' - CFL adjusted dt: 2D: {:} 3D: {:}'.format(max_dt_2d, max_dt_3d)) if not automatic_timestep: print_output(' - User defined dt: 2D: {:} 3D: {:}'.format(self.options.timestep_2d, self.options.timestep)) self.dt = self.options.timestep self.dt_2d = self.options.timestep_2d if automatic_timestep: assert self.options.timestep is not None assert self.options.timestep > 0.0 assert self.options.timestep_2d is not None assert self.options.timestep_2d > 0.0 if self.dt_mode == 'split': if automatic_timestep: self.dt = max_dt_3d self.dt_2d = max_dt_2d # compute mode split ratio and force it to be integer self.M_modesplit = int(numpy.ceil(self.dt/self.dt_2d)) self.dt_2d = self.dt/self.M_modesplit elif self.dt_mode == '2d': if automatic_timestep: self.dt = min(max_dt_2d, max_dt_3d) self.dt_2d = self.dt self.M_modesplit = 1 elif self.dt_mode == '3d': if automatic_timestep: self.dt = max_dt_3d self.dt_2d = self.dt self.M_modesplit = 1 print_output(' - chosen dt: 2D: {:} 3D: {:}'.format(self.dt_2d, self.dt)) # fit dt to export time m_exp = int(numpy.ceil(self.options.simulation_export_time/self.dt)) self.dt = float(self.options.simulation_export_time)/m_exp if self.dt_mode == 'split': self.M_modesplit = int(numpy.ceil(self.dt/self.dt_2d)) self.dt_2d = self.dt/self.M_modesplit else: self.dt_2d = self.dt print_output(' - adjusted dt: 2D: {:} 3D: {:}'.format(self.dt_2d, self.dt)) print_output('dt = {0:f}'.format(self.dt)) if self.dt_mode == 'split': print_output('2D dt = {0:f} {1:d}'.format(self.dt_2d, self.M_modesplit)) sys.stdout.flush()
[docs] @unfrozen @PETSc.Log.EventDecorator("thetis.FlowSolver.create_function_spaces") def create_function_spaces(self): """ Creates function spaces Function spaces are accessible via :attr:`.function_spaces` object. """ # ----- function spaces: elev in H, uv in U, mixed is W self.function_spaces.P0 = get_functionspace(self.mesh, 'DG', 0, 'DG', 0, name='P0') self.function_spaces.P1 = get_functionspace(self.mesh, 'CG', 1, 'CG', 1, name='P1') self.function_spaces.P1v = get_functionspace(self.mesh, 'CG', 1, 'CG', 1, name='P1v', vector=True) self.function_spaces.P1DG = get_functionspace(self.mesh, 'DG', 1, 'DG', 1, name='P1DG') self.function_spaces.P1DGv = get_functionspace(self.mesh, 'DG', 1, 'DG', 1, name='P1DGv', vector=True) # function spaces for (u,v) and w if self.options.element_family in ['rt-dg', 'bdm-dg']: h_family_prefix = self.options.element_family.split('-')[0].upper() h_family_suffix = {'triangle': 'F', 'quadrilateral': 'CF'} h_cell = self.mesh2d.ufl_cell().cellname() hfam = h_family_prefix + h_family_suffix[h_cell] h_degree = self.options.polynomial_degree + 1 self.function_spaces.U = get_functionspace(self.mesh, hfam, h_degree, 'DG', self.options.polynomial_degree, name='U', hdiv=True) self.function_spaces.W = get_functionspace(self.mesh, 'DG', self.options.polynomial_degree, 'CG', self.options.polynomial_degree+1, name='W', hdiv=True) self.function_spaces.U_2d = get_functionspace(self.mesh2d, hfam, h_degree, name='U_2d') elif self.options.element_family == 'dg-dg': self.function_spaces.U = get_functionspace(self.mesh, 'DG', self.options.polynomial_degree, 'DG', self.options.polynomial_degree, name='U', vector=True) self.function_spaces.W = get_functionspace(self.mesh, 'DG', self.options.polynomial_degree, 'DG', self.options.polynomial_degree, name='W', vector=True) self.function_spaces.U_2d = get_functionspace(self.mesh2d, 'DG', self.options.polynomial_degree, name='U_2d', vector=True) else: raise Exception('Unsupported finite element family {:}'.format(self.options.element_family)) self.function_spaces.Uint = self.function_spaces.U # vertical integral of uv # tracers self.function_spaces.H = get_functionspace(self.mesh, 'DG', self.options.polynomial_degree, 'DG', self.options.polynomial_degree, name='H') self.function_spaces.turb_space = self.function_spaces.P0 # 2D spaces self.function_spaces.P1_2d = get_functionspace(self.mesh2d, 'CG', 1, name='P1_2d') self.function_spaces.P1v_2d = get_functionspace(self.mesh2d, 'CG', 1, name='P1v_2d', vector=True) self.function_spaces.P1DG_2d = get_functionspace(self.mesh2d, 'DG', 1, name='P1DG_2d') self.function_spaces.P1DGv_2d = get_functionspace(self.mesh2d, 'DG', 1, name='P1DGv_2d', vector=True) self.function_spaces.H_2d = get_functionspace(self.mesh2d, 'DG', self.options.polynomial_degree, name='H_2d') self.function_spaces.V_2d = MixedFunctionSpace([self.function_spaces.U_2d, self.function_spaces.H_2d], name='V_2d') # define function spaces for baroclinic head and internal pressure gradient if self.options.use_quadratic_pressure: self.function_spaces.P2DGxP2 = get_functionspace(self.mesh, 'DG', 2, 'CG', 2, name='P2DGxP2') self.function_spaces.P2DG_2d = get_functionspace(self.mesh2d, 'DG', 2, name='P2DG_2d') self.function_spaces.H_bhead = self.function_spaces.P2DGxP2 self.function_spaces.H_bhead_2d = self.function_spaces.P2DG_2d if self.options.element_family == 'dg-dg': self.function_spaces.P2DGxP1DGv = get_functionspace(self.mesh, 'DG', 2, 'DG', 1, name='P2DGxP1DGv', vector=True, dim=2) self.function_spaces.U_int_pg = self.function_spaces.P2DGxP1DGv else: self.function_spaces.U_int_pg = self.function_spaces.U else: self.function_spaces.P1DGxP2 = get_functionspace(self.mesh, 'DG', 1, 'CG', 2, name='P1DGxP2') self.function_spaces.H_bhead = self.function_spaces.P1DGxP2 self.function_spaces.H_bhead_2d = self.function_spaces.P1DG_2d self.function_spaces.U_int_pg = self.function_spaces.U
[docs] @unfrozen @PETSc.Log.EventDecorator("thetis.FlowSolver.create_fields") def create_fields(self): """ Creates all fields """ if not hasattr(self, 'U_2d'): self.create_function_spaces() if self.options.log_output and not self.options.no_exports: mode = "a" if self.keep_log else "w" set_log_directory(self.options.output_directory, mode=mode) # mesh velocity etc fields must be in the same space as 3D coordinates coord_is_dg = element_continuity(self.mesh2d.coordinates.function_space().ufl_element()).horizontal == 'dg' if coord_is_dg: coord_fs = FunctionSpace(self.mesh, 'DG', 1, vfamily='CG', vdegree=1) coord_fs_2d = self.function_spaces.P1DG_2d else: coord_fs = self.function_spaces.P1 coord_fs_2d = self.function_spaces.P1_2d # ----- fields self.fields.solution_2d = Function(self.function_spaces.V_2d) # correct treatment of the split 2d functions uv_2d, eta2d = self.fields.solution_2d.subfunctions self.fields.uv_2d = uv_2d self.fields.elev_2d = eta2d # elevation field seen by the 3D mode, different from elev_2d self.fields.elev_domain_2d = ExtrudedFunction(self.function_spaces.H_2d, mesh_3d=self.mesh) self.fields.elev_cg_2d = ExtrudedFunction(coord_fs_2d, mesh_3d=self.mesh) self.fields.uv_3d = Function(self.function_spaces.U) self.fields.bathymetry_2d = ExtrudedFunction(coord_fs_2d, mesh_3d=self.mesh) # z coordinate in the strecthed mesh self.fields.z_coord_3d = Function(coord_fs) # z coordinate in the reference mesh (eta=0) self.fields.z_coord_ref_3d = Function(coord_fs) self.fields.uv_dav_3d = Function(self.function_spaces.U) self.fields.uv_dav_2d = Function(self.function_spaces.U_2d) self.fields.split_residual_2d = Function(self.function_spaces.U_2d) self.fields.w_3d = Function(self.function_spaces.W) self.fields.hcc_metric_3d = Function(self.function_spaces.P1DG, name='mesh consistency') if self.options.use_ale_moving_mesh: self.fields.w_mesh_3d = Function(coord_fs) if self.options.solve_salinity: self.fields.salt_3d = Function(self.function_spaces.H, name='Salinity') if self.options.solve_temperature: self.fields.temp_3d = Function(self.function_spaces.H, name='Temperature') if self.options.use_baroclinic_formulation: if self.options.use_quadratic_density: self.fields.density_3d = Function(self.function_spaces.P2DGxP2, name='Density') else: self.fields.density_3d = Function(self.function_spaces.H, name='Density') self.fields.baroc_head_3d = Function(self.function_spaces.H_bhead) self.fields.int_pg_3d = Function(self.function_spaces.U_int_pg, name='int_pg_3d') if self.options.coriolis_frequency is not None: if isinstance(self.options.coriolis_frequency, Constant): self.fields.coriolis_3d = self.options.coriolis_frequency else: self.fields.coriolis_3d = extend_function_to_3d(self.options.coriolis_frequency, self.mesh) if self.options.wind_stress is not None: if isinstance(self.options.wind_stress, Function): assert self.options.wind_stress.function_space().mesh().geometric_dimension() == 3, \ 'wind stress field must be a 3D function' self.fields.wind_stress_3d = self.options.wind_stress elif isinstance(self.options.wind_stress, Constant): self.fields.wind_stress_3d = self.options.wind_stress else: raise Exception('Unsupported wind stress type: {:}'.format(type(self.options.wind_stress))) self.fields.v_elem_size_3d = Function(self.function_spaces.P1DG) self.fields.v_elem_size_2d = Function(self.function_spaces.P1DG_2d) self.fields.h_elem_size_3d = Function(self.function_spaces.P1) self.fields.h_elem_size_2d = Function(self.function_spaces.P1_2d) get_horizontal_elem_size_3d(self.fields.h_elem_size_2d, self.fields.h_elem_size_3d) self.fields.max_h_diff = Function(self.function_spaces.P1) if self.options.use_smagorinsky_viscosity: self.fields.smag_visc_3d = Function(self.function_spaces.P1) if self.options.use_limiter_for_tracers and self.options.polynomial_degree > 0: self.tracer_limiter = limiter.VertexBasedP1DGLimiter(self.function_spaces.H) else: self.tracer_limiter = None if (self.options.use_limiter_for_velocity and self.options.polynomial_degree > 0 and self.options.element_family == 'dg-dg'): self.uv_limiter = limiter.VertexBasedP1DGLimiter(self.function_spaces.U) else: self.uv_limiter = None if self.options.use_turbulence: if self.options.turbulence_model_type == 'gls': # NOTE tke and psi should be in H as tracers ?? self.fields.tke_3d = Function(self.function_spaces.turb_space) self.fields.psi_3d = Function(self.function_spaces.turb_space) # NOTE other turb. quantities should share the same nodes ?? self.fields.eps_3d = Function(self.function_spaces.turb_space) self.fields.len_3d = Function(self.function_spaces.turb_space) self.fields.eddy_visc_3d = Function(self.function_spaces.turb_space) self.fields.eddy_diff_3d = Function(self.function_spaces.turb_space) # NOTE M2 and N2 depend on d(.)/dz -> use CG in vertical ? self.fields.shear_freq_3d = Function(self.function_spaces.turb_space) self.fields.buoy_freq_3d = Function(self.function_spaces.turb_space) self.turbulence_model = turbulence.GenericLengthScaleModel( weakref.proxy(self), self.fields.tke_3d, self.fields.psi_3d, self.fields.uv_3d, self.fields.get('density_3d'), self.fields.len_3d, self.fields.eps_3d, self.fields.eddy_diff_3d, self.fields.eddy_visc_3d, self.fields.buoy_freq_3d, self.fields.shear_freq_3d, options=self.options.turbulence_model_options) elif self.options.turbulence_model_type == 'pacanowski': self.fields.eddy_visc_3d = Function(self.function_spaces.turb_space) self.fields.eddy_diff_3d = Function(self.function_spaces.turb_space) self.fields.shear_freq_3d = Function(self.function_spaces.turb_space) self.fields.buoy_freq_3d = Function(self.function_spaces.turb_space) self.turbulence_model = turbulence.PacanowskiPhilanderModel( weakref.proxy(self), self.fields.uv_3d, self.fields.get('density_3d'), self.fields.eddy_diff_3d, self.fields.eddy_visc_3d, self.fields.buoy_freq_3d, self.fields.shear_freq_3d, options=self.options.turbulence_model_options) else: raise Exception('Unsupported turbulence model: {:}'.format(self.options.turbulence_model)) else: self.turbulence_model = None # copute total viscosity/diffusivity self.tot_h_visc = SumFunction() self.tot_h_visc.add(self.options.horizontal_viscosity) self.tot_h_visc.add(self.fields.get('smag_visc_3d')) self.tot_v_visc = SumFunction() self.tot_v_visc.add(self.options.vertical_viscosity) self.tot_v_visc.add(self.fields.get('eddy_visc_3d')) self.tot_h_diff = SumFunction() self.tot_h_diff.add(self.options.horizontal_diffusivity) self.tot_v_diff = SumFunction() self.tot_v_diff.add(self.options.vertical_diffusivity) self.tot_v_diff.add(self.fields.get('eddy_diff_3d'))
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.add_new_field") def add_new_field(self, function, label, name, filename, shortname=None, unit='-', preproc_func=None): """ Add a field to :attr:`fields`. :arg function: representation of the field as a :class:`Function` :arg label: field label used internally by Thetis, e.g. 'tracer_3d' :arg name: human readable name for the tracer field, e.g. 'Tracer concentration' :arg filename: file name for outputs, e.g. 'Tracer3d' :kwarg shortname: short version of name, e.g. 'Tracer' :kwarg unit: units for field, e.g. '-' :kwarg preproc_func: optional pre-processor function which will be called before exporting """ assert isinstance(function, Function) assert isinstance(label, str) assert isinstance(name, str) assert isinstance(filename, str) assert shortname is None or isinstance(shortname, str) assert isinstance(unit, str) assert preproc_func is None or callable(preproc_func) assert label not in field_metadata, f"Field '{label}' already exists." assert ' ' not in label, "Labels cannot contain spaces" assert ' ' not in filename, "Filenames cannot contain spaces" field_metadata[label] = { 'name': name, 'shortname': shortname or name, 'unit': unit, 'filename': filename, } self.fields[label] = function if preproc_func is not None: self._field_preproc_funcs[label] = preproc_func
[docs] @unfrozen @PETSc.Log.EventDecorator("thetis.FlowSolver.create_equations") def create_equations(self): """ Creates equation instances """ if 'uv_3d' not in self.fields: self.create_fields() if self.options.log_output and not self.options.no_exports: logfile = os.path.join(create_directory(self.options.output_directory), 'log') mode = "a" if self.keep_log else "w" filehandler = logging.logging.FileHandler(logfile, mode=mode) filehandler.setFormatter(logging.logging.Formatter('%(message)s')) output_logger.addHandler(filehandler) self.depth = DepthExpression(self.fields.bathymetry_2d, use_nonlinear_equations=self.options.use_nonlinear_equations) self.equations = AttrDict() self.equations.sw = shallowwater_eq.ModeSplit2DEquations( self.fields.solution_2d.function_space(), self.depth, self.options) expl_bottom_friction = self.options.use_bottom_friction and not self.options.use_implicit_vertical_diffusion self.equations.momentum = momentum_eq.MomentumEquation( self.fields.uv_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_nonlinear_equations=self.options.use_nonlinear_equations, use_lax_friedrichs=self.options.use_lax_friedrichs_velocity, use_bottom_friction=expl_bottom_friction, sipg_factor=self.options.sipg_factor, sipg_factor_vertical=self.options.sipg_factor_vertical) if self.options.use_implicit_vertical_diffusion: self.equations.vertmomentum = momentum_eq.MomentumEquation( self.fields.uv_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_nonlinear_equations=False, use_lax_friedrichs=self.options.use_lax_friedrichs_velocity, use_bottom_friction=self.options.use_bottom_friction, sipg_factor=self.options.sipg_factor, sipg_factor_vertical=self.options.sipg_factor_vertical) if self.options.solve_salinity: self.equations.salt = tracer_eq.TracerEquation( self.fields.salt_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_lax_friedrichs=self.options.use_lax_friedrichs_tracer, use_symmetric_surf_bnd=self.options.element_family == 'dg-dg', sipg_factor=self.options.sipg_factor_tracer, sipg_factor_vertical=self.options.sipg_factor_vertical_tracer) if self.options.use_implicit_vertical_diffusion: self.equations.salt_vdff = tracer_eq.TracerEquation( self.fields.salt_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_lax_friedrichs=self.options.use_lax_friedrichs_tracer, sipg_factor=self.options.sipg_factor_tracer, sipg_factor_vertical=self.options.sipg_factor_vertical_tracer) if self.options.solve_temperature: self.equations.temp = tracer_eq.TracerEquation( self.fields.temp_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_lax_friedrichs=self.options.use_lax_friedrichs_tracer, use_symmetric_surf_bnd=self.options.element_family == 'dg-dg', sipg_factor=self.options.sipg_factor_tracer, sipg_factor_vertical=self.options.sipg_factor_vertical_tracer) if self.options.use_implicit_vertical_diffusion: self.equations.temp_vdff = tracer_eq.TracerEquation( self.fields.temp_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_lax_friedrichs=self.options.use_lax_friedrichs_tracer, sipg_factor=self.options.sipg_factor_tracer, sipg_factor_vertical=self.options.sipg_factor_vertical_tracer) self.equations.sw.bnd_functions = self.bnd_functions['shallow_water'] self.equations.momentum.bnd_functions = self.bnd_functions['momentum'] if self.options.solve_salinity: self.equations.salt.bnd_functions = self.bnd_functions['salt'] if self.options.solve_temperature: self.equations.temp.bnd_functions = self.bnd_functions['temp'] if self.options.use_turbulence and self.options.turbulence_model_type == 'gls': if self.options.use_turbulence_advection: # explicit advection equations self.equations.tke_adv = tracer_eq.TracerEquation( self.fields.tke_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_lax_friedrichs=self.options.use_lax_friedrichs_tracer, sipg_factor=self.options.sipg_factor_turb, sipg_factor_vertical=self.options.sipg_factor_vertical_turb) self.equations.psi_adv = tracer_eq.TracerEquation( self.fields.psi_3d.function_space(), bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d, use_lax_friedrichs=self.options.use_lax_friedrichs_tracer, sipg_factor=self.options.sipg_factor_turb, sipg_factor_vertical=self.options.sipg_factor_vertical_turb) # implicit vertical diffusion eqn with production terms self.equations.tke_diff = turbulence.TKEEquation( self.fields.tke_3d.function_space(), self.turbulence_model, bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d) self.equations.psi_diff = turbulence.PsiEquation( self.fields.psi_3d.function_space(), self.turbulence_model, bathymetry=self.fields.bathymetry_2d.view_3d, v_elem_size=self.fields.v_elem_size_3d, h_elem_size=self.fields.h_elem_size_3d) # ----- Operators tot_uv_3d = self.fields.uv_3d + self.fields.uv_dav_3d self.w_solver = VerticalVelocitySolver(self.fields.w_3d, tot_uv_3d, self.fields.bathymetry_2d.view_3d, self.equations.momentum.bnd_functions) self.uv_averager = VerticalIntegrator(self.fields.uv_3d, self.fields.uv_dav_3d, bottom_to_top=True, bnd_value=Constant((0.0, 0.0, 0.0)), average=True, bathymetry=self.fields.bathymetry_2d.view_3d, elevation=self.fields.elev_cg_2d.view_3d) if self.options.use_baroclinic_formulation: if self.options.solve_salinity: s = self.fields.salt_3d else: s = self.options.constant_salinity if self.options.solve_temperature: t = self.fields.temp_3d else: t = self.options.constant_temperature if self.options.equation_of_state_type == 'linear': eos_options = self.options.equation_of_state_options self.equation_of_state = LinearEquationOfState(eos_options.rho_ref, eos_options.alpha, eos_options.beta, eos_options.th_ref, eos_options.s_ref) else: self.equation_of_state = JackettEquationOfState() if self.options.use_quadratic_density: self.density_solver = DensitySolverWeak(s, t, self.fields.density_3d, self.equation_of_state) else: self.density_solver = DensitySolver(s, t, self.fields.density_3d, self.equation_of_state) self.rho_integrator = VerticalIntegrator(self.fields.density_3d, self.fields.baroc_head_3d, bottom_to_top=False, average=False, bathymetry=self.fields.bathymetry_2d.view_3d, elevation=self.fields.elev_cg_2d.view_3d) self.int_pg_calculator = momentum_eq.InternalPressureGradientCalculator( self.fields, self.fields.bathymetry_2d.view_3d, self.bnd_functions['momentum'], internal_pg_scalar=self.options.internal_pg_scalar, solver_parameters=self.options.timestepper_options.explicit_momentum_options.solver_parameters) self.extract_surf_dav_uv = SubFunctionExtractor(self.fields.uv_dav_3d, self.fields.uv_dav_2d, boundary='top', elem_facet='top', elem_height=self.fields.v_elem_size_2d) self.copy_uv_dav_to_uv_dav_3d = ExpandFunctionTo3d(self.fields.uv_dav_2d, self.fields.uv_dav_3d, elem_height=self.fields.v_elem_size_3d) self.copy_uv_to_uv_dav_3d = ExpandFunctionTo3d(self.fields.uv_2d, self.fields.uv_dav_3d, elem_height=self.fields.v_elem_size_3d) self.mesh_updater = ALEMeshUpdater(self) if self.options.use_smagorinsky_viscosity: self.smagorinsky_diff_solver = SmagorinskyViscosity(self.fields.uv_3d, self.fields.smag_visc_3d, self.options.smagorinsky_coefficient, self.fields.h_elem_size_3d, self.fields.max_h_diff, weak_form=True)
[docs] @unfrozen @PETSc.Log.EventDecorator("thetis.FlowSolver.create_timestepper") def create_timestepper(self): """ Creates time stepper instance """ if not hasattr(self, 'equations'): self.create_equations() self.dt_mode = '3d' # 'split'|'2d'|'3d' use constant 2d/3d dt, or split if self.options.timestepper_type == 'LeapFrog': self.timestepper = coupled_timeintegrator.CoupledLeapFrogAM3(weakref.proxy(self)) elif self.options.timestepper_type == 'SSPRK22': self.timestepper = coupled_timeintegrator.CoupledTwoStageRK(weakref.proxy(self)) else: raise NotImplementedError(f"Unsupported time integrator type: {str(self.options.timestepper_type)}") self.fields.bathymetry_2d.project(self.bathymetry_cg_2d) self.mesh_updater.initialize() self.compute_mesh_stats() self.set_time_step() self.timestepper.set_dt(self.dt, self.dt_2d) self.next_export_t = self.simulation_time + self.options.simulation_export_time # compute maximal diffusivity for explicit schemes degree_h, degree_v = self.function_spaces.H.ufl_element().degree() max_diff_alpha = 1.0/60.0/max((degree_h*(degree_h + 1)), 1.0) # FIXME depends on element type and order self.fields.max_h_diff.interpolate(max_diff_alpha/self.dt * self.fields.h_elem_size_3d**2) d = self.fields.max_h_diff.dat.data print_output('max h diff {:} - {:}'.format(d.min(), d.max()))
[docs] @unfrozen @PETSc.Log.EventDecorator("thetis.FlowSolver.create_exporters") def create_exporters(self): """ Creates file exporters """ if not hasattr(self, 'timestepper'): self.create_timestepper() # ----- File exporters # create export_managers and store in a list self.exporters = OrderedDict() if not self.options.no_exports: e = exporter.ExportManager(self.options.output_directory, self.options.fields_to_export, self.fields, field_metadata, export_type='vtk', verbose=self.options.verbose > 0, preproc_funcs=self._field_preproc_funcs) self.exporters['vtk'] = e hdf5_dir = os.path.join(self.options.output_directory, 'hdf5') e = exporter.ExportManager(hdf5_dir, self.options.fields_to_export_hdf5, self.fields, field_metadata, export_type='hdf5', verbose=self.options.verbose > 0, preproc_funcs=self._field_preproc_funcs) self.exporters['hdf5'] = e
[docs] def initialize(self): """ Creates function spaces, equations, time stepper and exporters """ if not hasattr(self.function_spaces, 'U'): self.create_function_spaces() if not hasattr(self, 'equations'): self.create_equations() if not hasattr(self, 'timestepper'): self.create_timestepper() if not hasattr(self, 'exporters'): self.create_exporters() self._initialized = True
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.assign_initial_conditions") def assign_initial_conditions(self, elev=None, salt=None, temp=None, uv_2d=None, uv_3d=None, tke=None, psi=None): """ Assigns initial conditions :kwarg elev: Initial condition for water elevation :type elev: scalar 2D :class:`Function`, :class:`Constant`, or an expression :kwarg salt: Initial condition for salinity field :type salt: scalar 3D :class:`Function`, :class:`Constant`, or an expression :kwarg temp: Initial condition for temperature field :type temp: scalar 3D :class:`Function`, :class:`Constant`, or an expression :kwarg uv_2d: Initial condition for depth averaged velocity :type uv_2d: vector valued 2D :class:`Function`, :class:`Constant`, or an expression :kwarg uv_3d: Initial condition for horizontal velocity :type uv_3d: vector valued 3D :class:`Function`, :class:`Constant`, or an expression :kwarg tke: Initial condition for turbulent kinetic energy field :type tke: scalar 3D :class:`Function`, :class:`Constant`, or an expression :kwarg psi: Initial condition for turbulence generic length scale field :type psi: scalar 3D :class:`Function`, :class:`Constant`, or an expression """ if not self._initialized: self.initialize() if elev is not None: self.fields.elev_2d.project(elev) if uv_2d is not None: self.fields.uv_2d.project(uv_2d) self.fields.uv_dav_2d.project(uv_2d) if uv_3d is None: ExpandFunctionTo3d(self.fields.uv_2d, self.fields.uv_dav_3d, elem_height=self.fields.v_elem_size_3d).solve() if uv_3d is not None: self.fields.uv_3d.project(uv_3d) if salt is not None and self.options.solve_salinity: self.fields.salt_3d.project(salt) if temp is not None and self.options.solve_temperature: self.fields.temp_3d.project(temp) if self.options.use_turbulence and self.options.turbulence_model_type == 'gls': if tke is not None: self.fields.tke_3d.project(tke) if psi is not None: self.fields.psi_3d.project(psi) self.turbulence_model.initialize() if self.options.use_ale_moving_mesh: self.timestepper._update_3d_elevation() self.timestepper._update_moving_mesh() self.timestepper.initialize() # update all diagnostic variables self.timestepper._update_all_dependencies(self.simulation_time, do_2d_coupling=False, do_vert_diffusion=False, do_ale_update=True, do_stab_params=True, do_turbulence=False) if self.options.use_turbulence: self.turbulence_model.initialize()
[docs] def add_callback(self, callback, eval_interval='export'): """ Adds callback to solver object :arg callback: :class:`.DiagnosticCallback` instance :kwarg str eval_interval: Determines when callback will be evaluated, either 'export' or 'timestep' for evaluating after each export or time step. """ self.callbacks.add(callback, eval_interval)
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.export") def export(self): """ Export all fields to disk Also evaluates all callbacks set to 'export' interval. """ self.callbacks.evaluate(mode='export', index=self.i_export) # set uv to total uv instead of deviation from depth average # TODO find a cleaner way of doing this ... self.fields.uv_3d += self.fields.uv_dav_3d for e in self.exporters.values(): e.export() # restore uv_3d self.fields.uv_3d -= self.fields.uv_dav_3d
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.load_state") def load_state(self, i_stored, outputdir=None, t=None, iteration=None, i_export=None, legacy_mode=False): """ Loads simulation state from hdf5 outputs. Replaces :meth:`.assign_initial_conditions` in model initialization. For example, continue simulation from export 40, .. code-block:: python solver_obj.load_state(40) Continue simulation from another output directory, .. code-block:: python solver_obj.load_state(40, outputdir='outputs_spinup') Continue simulation from another output directory and reset the counters, .. code-block:: python solver_obj.load_state(40, outputdir='outputs_spinup', iteration=0, t=0, i_export=0) Model state can be loaded if all prognostic state variables have been stored in hdf5 format. Required state variables are: `elev_2d`, `uv_2d`, `uv_3d`, `salt_3d`, `temp_3d`, `tke_3d`, and `psi_3d`. Simulation time and iteration can be recovered automatically provided that the model setup is the same (e.g. time step and export_time). :arg int i_stored: export index to load :kwarg string outputdir: (optional) directory where files are read from. By default ``options.output_directory``. :kwarg float t: simulation time. Overrides the time stamp inferred from model options. :kwarg int iteration: Overrides the iteration count inferred from model options. :kwarg int i_export: Set initial export index for the present run. By default, `i_stored` is used. :kwarg bool legacy_mode: Load legacy `DumbCheckpoint` files. """ if not self._initialized: self.initialize() if outputdir is None: outputdir = self.options.output_directory self._simulation_continued = True # create new ExportManager with desired outputdir state_fields = ['uv_2d', 'elev_2d', 'uv_3d', 'salt_3d', 'temp_3d', 'tke_3d', 'psi_3d'] hdf5_dir = os.path.join(outputdir, 'hdf5') e = exporter.ExportManager(hdf5_dir, state_fields, self.fields, field_metadata, export_type='hdf5', legacy_mode=legacy_mode, verbose=self.options.verbose > 0) e.exporters['uv_2d'].load(i_stored, self.fields.uv_2d) e.exporters['elev_2d'].load(i_stored, self.fields.elev_2d) e.exporters['uv_3d'].load(i_stored, self.fields.uv_3d) # NOTE remove mean from uv_3d self.timestepper._remove_depth_average_from_uv_3d() salt = temp = tke = psi = None if self.options.solve_salinity: salt = self.fields.salt_3d e.exporters['salt_3d'].load(i_stored, salt) if self.options.solve_temperature: temp = self.fields.temp_3d e.exporters['temp_3d'].load(i_stored, temp) if self.options.use_turbulence: if 'tke_3d' in self.fields: tke = self.fields.tke_3d e.exporters['tke_3d'].load(i_stored, tke) if 'psi_3d' in self.fields: psi = self.fields.psi_3d e.exporters['psi_3d'].load(i_stored, psi) self.assign_initial_conditions(elev=self.fields.elev_2d, uv_2d=self.fields.uv_2d, uv_3d=self.fields.uv_3d, salt=salt, temp=temp, tke=tke, psi=psi, ) # time stepper bookkeeping for export time step if i_export is None: i_export = i_stored self.i_export = i_export self.next_export_t = self.i_export*self.options.simulation_export_time if iteration is None: iteration = int(numpy.ceil(self.next_export_t/self.dt)) if t is None: t = iteration*self.dt self.iteration = iteration self.simulation_time = t # for next export self.export_initial_state = outputdir != self.options.output_directory if self.export_initial_state: offset = 0 else: offset = 1 self.next_export_t += self.options.simulation_export_time for e in self.exporters.values(): e.set_next_export_ix(self.i_export + offset)
[docs] def print_state(self, cputime, print_header=False): """ Print a summary of the model state on stdout :arg float cputime: Measured CPU time in seconds :kwarg print_header: Whether to print column header first """ entries = [ ('exp', self.i_export, '5d'), ('iter', self.iteration, '5d'), ] # generate simulation time string if self.options.simulation_initial_date is not None: now = self.options.simulation_initial_date + datetime.timedelta(seconds=self.simulation_time) date_str = f'{now:%Y-%m-%d}'.rjust(11) time_str = f'{now:%H:%M:%S}'.rjust(9) entries += [ ('date', date_str, '11s'), ('time', time_str, '9s'), ] else: time_str = f'{self.simulation_time:.2f}'.rjust(15) entries += [ ('time', time_str, '15s'), ] norm_h = norm(self.fields.elev_2d) norm_u = norm(self.fields.uv_3d) # list of entries to print: (header, value, format) entries += [ ('eta norm', norm_h, '14.4f'), ('u norm', norm_u, '14.4f'), ('Tcpu', cputime, '6.2f'), ] if print_header: # generate header header = ' '.join([e[0].rjust(len(f'{e[1]:{e[2]}}')) for e in entries]) print_output(header) # generate line line = ' '.join([f'{e[1]:{e[2]}}' for e in entries]) print_output(line) sys.stdout.flush()
def _print_field(self, field): """ Prints min/max values of a field for debugging. :arg field: a :class:`Function` or a field string, e.g. 'salt_3d' """ if isinstance(field, str): _field = self.fields[field] else: _field = field minval = float(_field.dat.data.min()) minval = self.comm.allreduce(minval, op=MPI.MIN) maxval = float(_field.dat.data.max()) maxval = self.comm.allreduce(maxval, op=MPI.MAX) print_output(' {:}: {:.4f} {:.4f}'.format(_field.name(), minval, maxval))
[docs] def print_state_debug(self): """ Print min/max values of prognostic/diagnostic fields for debugging. """ field_list = [ 'elev_2d', 'uv_2d', 'elev_domain_2d', 'elev_cg_2d', 'uv_3d', 'w_3d', 'uv_dav_3d', 'w_mesh_3d', 'salt_3d', 'temp_3d', 'density_3d', 'baroc_head_3d', 'int_pg_3d', 'psi_3d', 'eps_3d', 'eddy_visc_3d', 'shear_freq_3d', 'buoy_freq_3d', 'coriolis_2d', 'coriolis_3d', 'wind_stress_3d', ] print_output('{:06} T={:10.2f}'.format(self.iteration, self.simulation_time)) for fieldname in field_list: if (fieldname in self.fields and isinstance(self.fields[fieldname], Function)): self._print_field(self.fields[fieldname]) self.comm.barrier()
[docs] @PETSc.Log.EventDecorator("thetis.FlowSolver.iterate") def iterate(self, update_forcings=None, update_forcings3d=None, export_func=None): """ Runs the simulation Iterates over the time loop until time ``options.simulation_end_time`` is reached. Exports fields to disk on ``options.simulation_export_time`` intervals. :kwarg update_forcings: User-defined function that takes simulation time as an argument and updates time-dependent boundary conditions of the 2D system (if any). :kwarg update_forcings_3d: User-defined function that takes simulation time as an argument and updates time-dependent boundary conditions of the 3D equations (if any). :kwarg export_func: User-defined function (with no arguments) that will be called on every export. """ if not self._initialized: self.initialize() self.options.check_salinity_conservation &= self.options.solve_salinity self.options.check_salinity_overshoot &= self.options.solve_salinity self.options.check_temperature_conservation &= self.options.solve_temperature self.options.check_temperature_overshoot &= self.options.solve_temperature self.options.check_volume_conservation_3d &= self.options.use_ale_moving_mesh self.options.use_limiter_for_tracers &= self.options.polynomial_degree > 0 self.options.use_limiter_for_velocity &= self.options.polynomial_degree > 0 self.options.use_limiter_for_velocity &= self.options.element_family == 'dg-dg' t_epsilon = 1.0e-5 cputimestamp = time_mod.perf_counter() dump_hdf5 = self.options.export_diagnostics and not self.options.no_exports if self.options.check_volume_conservation_2d: c = callback.VolumeConservation2DCallback(self, export_to_hdf5=dump_hdf5, append_to_log=True) self.add_callback(c, eval_interval='export') if self.options.check_volume_conservation_3d: c = callback.VolumeConservation3DCallback(self, export_to_hdf5=dump_hdf5, append_to_log=True) self.add_callback(c, eval_interval='export') if self.options.check_salinity_conservation: c = callback.TracerMassConservationCallback('salt_3d', self, export_to_hdf5=dump_hdf5, append_to_log=True) self.add_callback(c, eval_interval='export') if self.options.check_salinity_overshoot: c = callback.TracerOvershootCallBack('salt_3d', self, export_to_hdf5=dump_hdf5, append_to_log=True) self.add_callback(c, eval_interval='export') if self.options.check_temperature_conservation: c = callback.TracerMassConservationCallback('temp_3d', self, export_to_hdf5=dump_hdf5, append_to_log=True) self.add_callback(c, eval_interval='export') if self.options.check_temperature_overshoot: c = callback.TracerOvershootCallBack('temp_3d', self, export_to_hdf5=dump_hdf5, append_to_log=True) self.add_callback(c, eval_interval='export') if self._simulation_continued: # set all callbacks to append mode for m in self.callbacks: for k in self.callbacks[m]: self.callbacks[m][k].set_write_mode('append') initial_simulation_time = self.simulation_time internal_iteration = 0 init_date = self.options.simulation_initial_date end_date = self.options.simulation_end_date if (init_date is not None and end_date is not None): now = init_date + datetime.timedelta(initial_simulation_time) assert end_date > now, f'Simulation end date must be greater than initial time {now}' print_output( f'Running simulation\n' f' from {now:%Y-%m-%d %H:%M:%S %Z}\n' f' to {end_date:%Y-%m-%d %H:%M:%S %Z}' ) end_time = (end_date - now).total_seconds() + initial_simulation_time if self.options.simulation_end_time is not None: warning('Both simulation_end_date and simulation_end_time have been set, ignoring simulation_end_time.') self.options.simulation_end_time = end_time assert self.options.simulation_end_time is not None, 'simulation_end_time must be set' # initial export self.print_state(0.0, print_header=True) if self.export_initial_state: self.export() if export_func is not None: export_func() if 'vtk' in self.exporters: self.exporters['vtk'].export_bathymetry(self.fields.bathymetry_2d) while self.simulation_time <= self.options.simulation_end_time - t_epsilon: self.timestepper.advance(self.simulation_time, update_forcings, update_forcings3d) # Move to next time step self.iteration += 1 internal_iteration += 1 self.simulation_time = initial_simulation_time + internal_iteration*self.dt self.callbacks.evaluate(mode='timestep') # Write the solution to file if self.simulation_time >= self.next_export_t - t_epsilon: self.i_export += 1 self.next_export_t += self.options.simulation_export_time cputime = time_mod.perf_counter() - cputimestamp cputimestamp = time_mod.perf_counter() self.print_state(cputime) self.export() if export_func is not None: export_func()