def __init__(self, equation): """ Initialise limiter :param space : equation, as we need the broken space attached to it """ self.Vt = equation.space # check this is the right space, only currently working for 2D extruded mesh if self.Vt.extruded and self.Vt.mesh().topological_dimension() == 2: # check that horizontal degree is 1 and vertical degree is 2 if self.Vt.ufl_element().degree()[0] is not 1 or \ self.Vt.ufl_element().degree()[1] is not 2: raise ValueError('This is not the right limiter for this space.') # check that continuity of the spaces is correct # this will fail if the space does not use broken elements if self.Vt.ufl_element()._element.sobolev_space()[0].name is not 'L2' or \ self.Vt.ufl_element()._element.sobolev_space()[1].name is not 'H1': raise ValueError('This is not the right limiter for this space.') else: logger.warning('This limiter may not work for the space you are using.') self.Q1DG = FunctionSpace(self.Vt.mesh(), 'DG', 1) # space with only vertex DOFs self.vertex_limiter = VertexBasedLimiter(self.Q1DG) self.theta_hat = Function(self.Q1DG) # theta function with only vertex DOFs self.w = Function(self.Vt) self.result = Function(self.Vt) par_loop(_weight_kernel, dx, {"weight": (self.w, INC)})
def __init__(self, state, equations, alpha=0.5, quadrature_degree=None, solver_parameters=None, overwrite_solver_parameters=False, moisture=None): self.moisture = moisture if quadrature_degree is not None: self.quadrature_degree = quadrature_degree else: dgspace = state.spaces("DG") if any(deg > 2 for deg in dgspace.ufl_element().degree()): logger.warning( "default quadrature degree most likely not sufficient for this degree element" ) self.quadrature_degree = (5, 5) if logger.isEnabledFor(DEBUG): # Set outer solver to FGMRES and turn on KSP monitor for the outer system self.solver_parameters["ksp_type"] = "fgmres" self.solver_parameters["mat_type"] = "aij" self.solver_parameters["pmat_type"] = "matfree" self.solver_parameters["ksp_monitor_true_residual"] = None # Turn monitor on for the trace system self.solver_parameters["condensed_field"][ "ksp_monitor_true_residual"] = None super().__init__(state, equations, alpha, solver_parameters, overwrite_solver_parameters)
def __init__(self, state, quadrature_degree=None, solver_parameters=None, overwrite_solver_parameters=False, moisture=None): self.moisture = moisture if quadrature_degree is not None: self.quadrature_degree = quadrature_degree else: dgspace = state.spaces("DG") if any(deg > 2 for deg in dgspace.ufl_element().degree()): logger.warning("default quadrature degree most likely not sufficient for this degree element") self.quadrature_degree = (5, 5) super().__init__(state, solver_parameters, overwrite_solver_parameters)
def __init__(self, state, euler_poincare=True, linear=False, extra_terms=None, moisture=None): self.state = state if linear: self.euler_poincare = False logger.warning( 'Setting euler_poincare to False because you have set linear=True' ) else: self.euler_poincare = euler_poincare # set up functions self.Vu = state.spaces("HDiv") # this is the function that the forcing term is applied to self.x0 = Function(state.W) self.test = TestFunction(self.Vu) self.trial = TrialFunction(self.Vu) # this is the function that contains the result of solving # <test, trial> = <test, F(x0)>, where F is the forcing term self.uF = Function(self.Vu) # find out which terms we need self.extruded = self.Vu.extruded self.coriolis = state.Omega is not None or hasattr( state.fields, "coriolis") self.sponge = state.mu is not None self.hydrostatic = state.hydrostatic self.topography = hasattr(state.fields, "topography") self.extra_terms = extra_terms self.moisture = moisture # some constants to use for scaling terms self.scaling = Constant(1.) self.impl = Constant(1.) self._build_forcing_solvers()
def unsaturated_hydrostatic_balance(state, theta_d, H, exner0=None, top=False, exner_boundary=Constant(1.0), max_outer_solve_count=40, max_inner_solve_count=20): """ Given vertical profiles for dry potential temperature and relative humidity compute hydrostatically balanced virtual potential temperature, dry density and water vapour profiles. The general strategy is to split up the solving into two steps: 1) finding rho to balance the theta profile 2) finding theta_v and r_v to get back theta_d and H We iteratively solve these steps until we (hopefully) converge to a solution. :arg state: The :class:`State` object. :arg theta_d: The initial dry potential temperature profile. :arg H: The relative humidity profile. :arg exner0: Optional function to put exner pressure into. :arg top: If True, set a boundary condition at the top, otherwise it will be at the bottom. :arg exner_boundary: The value of exner on the specified boundary. :arg max_outer_solve_count: Max number of iterations for outer loop of balance solver. :arg max_inner_solve_count: Max number of iterations for inner loop of balanace solver. """ theta0 = state.fields('theta') rho0 = state.fields('rho') mr_v0 = state.fields('vapour_mixing_ratio') # Calculate hydrostatic exner pressure Vt = theta0.function_space() Vr = rho0.function_space() R_d = state.parameters.R_d R_v = state.parameters.R_v epsilon = R_d / R_v VDG = state.spaces("DG") if any(deg > 2 for deg in VDG.ufl_element().degree()): logger.warning( "default quadrature degree most likely not sufficient for this degree element" ) # apply first guesses theta0.assign(theta_d * 1.01) mr_v0.assign(0.01) v_deg = Vr.ufl_element().degree()[1] if v_deg == 0: method = Boundary_Method.physics else: method = None rho_h = Function(Vr) rho_averaged = Function(Vt) Vt_broken = FunctionSpace(state.mesh, BrokenElement(Vt.ufl_element())) rho_recoverer = Recoverer(rho0, rho_averaged, VDG=Vt_broken, boundary_method=method) w_h = Function(Vt) delta = 1.0 # make expressions for determining mr_v0 exner = thermodynamics.exner_pressure(state.parameters, rho_averaged, theta0) p = thermodynamics.p(state.parameters, exner) T = thermodynamics.T(state.parameters, theta0, exner, mr_v0) r_v_expr = thermodynamics.r_v(state.parameters, H, T, p) # make expressions to evaluate residual exner_ev = thermodynamics.exner_pressure(state.parameters, rho_averaged, theta0) p_ev = thermodynamics.p(state.parameters, exner_ev) T_ev = thermodynamics.T(state.parameters, theta0, exner_ev, mr_v0) RH_ev = thermodynamics.RH(state.parameters, mr_v0, T_ev, p_ev) RH = Function(Vt) for i in range(max_outer_solve_count): # solve for rho with theta_vd and w_v guesses compressible_hydrostatic_balance(state, theta0, rho_h, top=top, exner_boundary=exner_boundary, mr_t=mr_v0, solve_for_rho=True) # damp solution rho0.assign(rho0 * (1 - delta) + delta * rho_h) # calculate averaged rho rho_recoverer.project() RH.assign(RH_ev) if errornorm(RH, H) < 1e-10: break # now solve for r_v for j in range(max_inner_solve_count): w_h.interpolate(r_v_expr) mr_v0.assign(mr_v0 * (1 - delta) + delta * w_h) # compute theta_vd theta0.assign(theta_d * (1 + mr_v0 / epsilon)) # test quality of solution by re-evaluating expression RH.assign(RH_ev) if errornorm(RH, H) < 1e-10: break if i == max_outer_solve_count: raise RuntimeError( 'Hydrostatic balance solve has not converged within %i' % i, 'iterations') if exner0 is not None: exner = thermodynamics.exner_pressure(state.parameters, rho0, theta0) exner0.interpolate(exner) # do one extra solve for rho compressible_hydrostatic_balance(state, theta0, rho0, top=top, exner_boundary=exner_boundary, mr_t=mr_v0, solve_for_rho=True)
def saturated_hydrostatic_balance(state, theta_e, mr_t, exner0=None, top=False, exner_boundary=Constant(1.0), max_outer_solve_count=40, max_theta_solve_count=5, max_inner_solve_count=3): """ Given a wet equivalent potential temperature, theta_e, and the total moisture content, mr_t, compute a hydrostatically balance virtual potential temperature, dry density and water vapour profile. The general strategy is to split up the solving into two steps: 1) finding rho to balance the theta profile 2) finding theta_v and r_v to get back theta_e and saturation We iteratively solve these steps until we (hopefully) converge to a solution. :arg state: The :class:`State` object. :arg theta_e: The initial wet equivalent potential temperature profile. :arg mr_t: The total water pseudo-mixing ratio profile. :arg exner0: Optional function to put exner pressure into. :arg top: If True, set a boundary condition at the top, otherwise it will be at the bottom. :arg exner_boundary: The value of exner on the specified boundary. :arg max_outer_solve_count: Max number of outer iterations for balance solver. :arg max_theta_solve_count: Max number of iterations for theta solver (middle part of solve). :arg max_inner_solve_count: Max number of iterations on the inner most loop for the water vapour solver. """ theta0 = state.fields('theta') rho0 = state.fields('rho') mr_v0 = state.fields('vapour_mixing_ratio') # Calculate hydrostatic exner pressure Vt = theta0.function_space() Vr = rho0.function_space() VDG = state.spaces("DG") if any(deg > 2 for deg in VDG.ufl_element().degree()): logger.warning( "default quadrature degree most likely not sufficient for this degree element" ) theta0.interpolate(theta_e) mr_v0.interpolate(mr_t) v_deg = Vr.ufl_element().degree()[1] if v_deg == 0: boundary_method = Boundary_Method.physics else: boundary_method = None rho_h = Function(Vr) Vt_broken = FunctionSpace(state.mesh, BrokenElement(Vt.ufl_element())) rho_averaged = Function(Vt) rho_recoverer = Recoverer(rho0, rho_averaged, VDG=Vt_broken, boundary_method=boundary_method) w_h = Function(Vt) theta_h = Function(Vt) theta_e_test = Function(Vt) delta = 0.8 # expressions for finding theta0 and mr_v0 from theta_e and mr_t exner = thermodynamics.exner_pressure(state.parameters, rho_averaged, theta0) p = thermodynamics.p(state.parameters, exner) T = thermodynamics.T(state.parameters, theta0, exner, mr_v0) r_v_expr = thermodynamics.r_sat(state.parameters, T, p) theta_e_expr = thermodynamics.theta_e(state.parameters, T, p, mr_v0, mr_t) for i in range(max_outer_solve_count): # solve for rho with theta_vd and w_v guesses compressible_hydrostatic_balance(state, theta0, rho_h, top=top, exner_boundary=exner_boundary, mr_t=mr_t, solve_for_rho=True) # damp solution rho0.assign(rho0 * (1 - delta) + delta * rho_h) theta_e_test.assign(theta_e_expr) if errornorm(theta_e_test, theta_e) < 1e-8: break # calculate averaged rho rho_recoverer.project() # now solve for r_v for j in range(max_theta_solve_count): theta_h.interpolate(theta_e / theta_e_expr * theta0) theta0.assign(theta0 * (1 - delta) + delta * theta_h) # break when close enough if errornorm(theta_e_test, theta_e) < 1e-6: break for k in range(max_inner_solve_count): w_h.interpolate(r_v_expr) mr_v0.assign(mr_v0 * (1 - delta) + delta * w_h) # break when close enough theta_e_test.assign(theta_e_expr) if errornorm(theta_e_test, theta_e) < 1e-6: break if i == max_outer_solve_count: raise RuntimeError( 'Hydrostatic balance solve has not converged within %i' % i, 'iterations') if exner0 is not None: exner = thermodynamics.exner(state.parameters, rho0, theta0) exner0.interpolate(exner) # do one extra solve for rho compressible_hydrostatic_balance(state, theta0, rho0, top=top, exner_boundary=exner_boundary, mr_t=mr_t, solve_for_rho=True)
def __init__(self, v_CG1, v_DG1, method=Boundary_Method.physics, coords_to_adjust=None): self.v_DG1 = v_DG1 self.v_CG1 = v_CG1 self.v_DG1_old = Function(v_DG1.function_space()) self.coords_to_adjust = coords_to_adjust self.method = method mesh = v_CG1.function_space().mesh() VDG0 = FunctionSpace(mesh, "DG", 0) VCG1 = FunctionSpace(mesh, "CG", 1) if VDG0.extruded: cell = mesh._base_mesh.ufl_cell().cellname() DG1_hori_elt = FiniteElement("DG", cell, 1, variant="equispaced") DG1_vert_elt = FiniteElement("DG", interval, 1, variant="equispaced") DG1_element = TensorProductElement(DG1_hori_elt, DG1_vert_elt) else: cell = mesh.ufl_cell().cellname() DG1_element = FiniteElement("DG", cell, 1, variant="equispaced") VDG1 = FunctionSpace(mesh, DG1_element) self.num_ext = Function(VDG0) # check function spaces of functions if self.method == Boundary_Method.dynamics: if v_CG1.function_space() != VCG1: raise NotImplementedError( "This boundary recovery method requires v1 to be in CG1.") if v_DG1.function_space() != VDG1: raise NotImplementedError( "This boundary recovery method requires v_out to be in DG1." ) # check whether mesh is valid if mesh.topological_dimension() == 2: # if mesh is extruded then we're fine, but if not needs to be quads if not VDG0.extruded and mesh.ufl_cell().cellname( ) != 'quadrilateral': raise NotImplementedError( 'For 2D meshes this recovery method requires that elements are quadrilaterals' ) elif mesh.topological_dimension() == 3: # assume that 3D mesh is extruded if mesh._base_mesh.ufl_cell().cellname() != 'quadrilateral': raise NotImplementedError( 'For 3D extruded meshes this recovery method requires a base mesh with quadrilateral elements' ) elif mesh.topological_dimension() != 1: raise NotImplementedError( 'This boundary recovery is implemented only on certain classes of mesh.' ) if coords_to_adjust is None: raise ValueError( 'Need coords_to_adjust field for dynamics boundary methods' ) elif self.method == Boundary_Method.physics: # check that mesh is valid -- must be an extruded mesh if not VDG0.extruded: raise NotImplementedError( 'The physics boundary method only works on extruded meshes' ) # base spaces cell = mesh._base_mesh.ufl_cell().cellname() w_hori = FiniteElement("DG", cell, 0, variant="equispaced") w_vert = FiniteElement("CG", interval, 1, variant="equispaced") # build element theta_element = TensorProductElement(w_hori, w_vert) # spaces Vtheta = FunctionSpace(mesh, theta_element) Vtheta_broken = FunctionSpace(mesh, BrokenElement(theta_element)) if v_CG1.function_space() != Vtheta: raise ValueError( "This boundary recovery method requires v_CG1 to be in DG0xCG1 TensorProductSpace." ) if v_DG1.function_space() != Vtheta_broken: raise ValueError( "This boundary recovery method requires v_DG1 to be in the broken DG0xCG1 TensorProductSpace." ) else: raise ValueError( "Boundary method should be a Boundary Method Enum object.") VuDG1 = VectorFunctionSpace(VDG0.mesh(), DG1_element) x = SpatialCoordinate(VDG0.mesh()) self.interpolator = Interpolator(self.v_CG1, self.v_DG1) if self.method == Boundary_Method.dynamics: # STRATEGY # obtain a coordinate field for all the nodes self.act_coords = Function(VuDG1).project(x) # actual coordinates self.eff_coords = Function(VuDG1).project( x) # effective coordinates self.output = Function(VDG1) shapes = { "nDOFs": self.v_DG1.function_space().finat_element.space_dimension(), "dim": np.prod(VuDG1.shape, dtype=int) } num_ext_domain = ("{{[i]: 0 <= i < {nDOFs}}}").format(**shapes) num_ext_instructions = (""" <float64> SUM_EXT = 0 for i SUM_EXT = SUM_EXT + EXT_V1[i] end NUM_EXT[0] = SUM_EXT """) coords_domain = ("{{[i, j, k, ii, jj, kk, ll, mm, iii, kkk]: " "0 <= i < {nDOFs} and " "0 <= j < {nDOFs} and 0 <= k < {dim} and " "0 <= ii < {nDOFs} and 0 <= jj < {nDOFs} and " "0 <= kk < {dim} and 0 <= ll < {dim} and " "0 <= mm < {dim} and 0 <= iii < {nDOFs} and " "0 <= kkk < {dim}}}").format(**shapes) coords_insts = ( """ <float64> sum_V1_ext = 0 <int> index = 100 <float64> dist = 0.0 <float64> max_dist = 0.0 <float64> min_dist = 0.0 """ # only do adjustment in cells with at least one DOF to adjust """ if NUM_EXT[0] > 0 """ # find the maximum distance between DOFs in this cell, to serve as starting point for finding min distances """ for i for j dist = 0.0 for k dist = dist + pow(ACT_COORDS[i,k] - ACT_COORDS[j,k], 2.0) end dist = pow(dist, 0.5) {{id=sqrt_max_dist, dep=*}} max_dist = fmax(dist, max_dist) {{id=max_dist, dep=sqrt_max_dist}} end end """ # loop through cells and find which ones to adjust """ for ii if EXT_V1[ii] > 0.5 """ # find closest interior node """ min_dist = max_dist index = 100 for jj if EXT_V1[jj] < 0.5 dist = 0.0 for kk dist = dist + pow(ACT_COORDS[ii,kk] - ACT_COORDS[jj,kk], 2) end dist = pow(dist, 0.5) if dist <= min_dist index = jj end min_dist = fmin(min_dist, dist) for ll EFF_COORDS[ii,ll] = 0.5 * (ACT_COORDS[ii,ll] + ACT_COORDS[index,ll]) end end end else """ # for DOFs that aren't exterior, use the original coordinates """ for mm EFF_COORDS[ii, mm] = ACT_COORDS[ii, mm] end end end else """ # for interior elements, just use the original coordinates """ for iii for kkk EFF_COORDS[iii, kkk] = ACT_COORDS[iii, kkk] end end end """).format(**shapes) _num_ext_kernel = (num_ext_domain, num_ext_instructions) _eff_coords_kernel = (coords_domain, coords_insts) self.gaussian_elimination_kernel = kernels.GaussianElimination( VDG1) # find number of external DOFs per cell par_loop(_num_ext_kernel, dx, { "NUM_EXT": (self.num_ext, WRITE), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) # find effective coordinates logger.warning( 'Finding effective coordinates for boundary recovery. This could give unexpected results for deformed meshes over very steep topography.' ) par_loop(_eff_coords_kernel, dx, { "EFF_COORDS": (self.eff_coords, WRITE), "ACT_COORDS": (self.act_coords, READ), "NUM_EXT": (self.num_ext, READ), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) elif self.method == Boundary_Method.physics: self.bottom_kernel = kernels.PhysicsRecoveryBottom() self.top_kernel = kernels.PhysicsRecoveryTop()
def __init__(self, state, moments=AdvectedMoments.M3, limit=True): super().__init__(state) # function spaces Vt = state.fields('rain').function_space() Vu = state.fields('u').function_space() # declare properties of class self.state = state self.moments = moments self.rain = state.fields('rain') self.v = state.fields('rainfall_velocity', Vu) self.limit = limit if moments == AdvectedMoments.M0: # all rain falls at terminal velocity terminal_velocity = Constant(5) # in m/s if state.mesh.geometric_dimension() == 2: self.v.project(as_vector([0, -terminal_velocity])) elif state.mesh.geometric_dimension() == 3: self.v.project(as_vector([0, 0, -terminal_velocity])) elif moments == AdvectedMoments.M3: # this advects the third moment M3 of the raindrop # distribution, which corresponds to the mean mass rho = state.fields('rho') rho_w = Constant(1000.0) # density of liquid water # assume n(D) = n_0 * D^mu * exp(-Lambda*D) # n_0 = N_r * Lambda^(1+mu) / gamma(1 + mu) N_r = Constant(10**5) # number of rain droplets per m^3 mu = 0.0 # shape constant of droplet gamma distribution # assume V(D) = a * D^b * exp(-f*D) * (rho_0 / rho)^g # take f = 0 a = Constant(362.) # intercept for velocity distr. in log space b = 0.65 # inverse scale parameter for velocity distr. rho0 = Constant(1.22) # reference density in kg/m^3 g = Constant(0.5) # scaling of density correction # we keep mu in the expressions even though mu = 0 threshold = Constant(10**-10) # only do rainfall for r > threshold Lambda = (N_r * pi * rho_w * gamma(4 + mu) / (6 * gamma(1 + mu) * rho * self.rain))**(1. / 3) Lambda0 = (N_r * pi * rho_w * gamma(4 + mu) / (6 * gamma(1 + mu) * rho * threshold))**(1. / 3) v_expression = conditional( self.rain > threshold, (a * gamma(4 + b + mu) / (gamma(4 + mu) * Lambda**b) * (rho0 / rho)**g), (a * gamma(4 + b + mu) / (gamma(4 + mu) * Lambda0**b) * (rho0 / rho)**g)) else: raise NotImplementedError( 'Currently we only have implementations for zero and one moment schemes for rainfall. Valid options are AdvectedMoments.M0 and AdvectedMoments.M3' ) if moments != AdvectedMoments.M0: if state.mesh.geometric_dimension() == 2: self.determine_v = Projector(as_vector([0, -v_expression]), self.v) elif state.mesh.geometric_dimension() == 3: self.determine_v = Projector(as_vector([0, 0, -v_expression]), self.v) # determine whether to do recovered space advection scheme # if horizontal and vertical degrees are 0 do recovered space if state.horizontal_degree == 0 and state.vertical_degree == 0: VDG1 = FunctionSpace(Vt.mesh(), "DG", 1) VCG1 = FunctionSpace(Vt.mesh(), "CG", 1) Vbrok = FunctionSpace(Vt.mesh(), BrokenElement(Vt.ufl_element())) boundary_method = Boundary_Method.dynamics advect_options = RecoveredOptions(embedding_space=VDG1, recovered_space=VCG1, broken_space=Vbrok, boundary_method=boundary_method) else: advect_options = EmbeddedDGOptions() # need to define advection equation before limiter (as it is needed for the ThetaLimiter) advection_equation = EmbeddedDGAdvection(state, Vt, equation_form="advective", outflow=True, options=advect_options) # decide which limiter to use if self.limit: if state.horizontal_degree == 0 and state.vertical_degree == 0: limiter = VertexBasedLimiter(VDG1) elif state.horizontal_degree == 1 and state.vertical_degree == 1: limiter = ThetaLimiter(Vt) else: logger.warning( "There is no limiter yet implemented for the spaces used. NoLimiter() is being used for the rainfall in this case." ) limiter = NoLimiter() else: limiter = None # sedimentation will happen using a full advection method self.advection_method = SSPRK3(state, self.rain, advection_equation, limiter=limiter)
def __init__(self, v_CG1, v_DG1, method=Boundary_Method.physics, coords_to_adjust=None): self.v_DG1 = v_DG1 self.v_CG1 = v_CG1 self.v_DG1_old = Function(v_DG1.function_space()) self.coords_to_adjust = coords_to_adjust self.method = method mesh = v_CG1.function_space().mesh() VDG0 = FunctionSpace(mesh, "DG", 0) VCG1 = FunctionSpace(mesh, "CG", 1) VDG1 = FunctionSpace(mesh, "DG", 1) self.num_ext = Function(VDG0) # check function spaces of functions if self.method == Boundary_Method.dynamics: if v_CG1.function_space() != VCG1: raise NotImplementedError( "This boundary recovery method requires v1 to be in CG1.") if v_DG1.function_space() != VDG1: raise NotImplementedError( "This boundary recovery method requires v_out to be in DG1." ) # check whether mesh is valid if mesh.topological_dimension() == 2: # if mesh is extruded then we're fine, but if not needs to be quads if not VDG0.extruded and mesh.ufl_cell().cellname( ) != 'quadrilateral': raise NotImplementedError( 'For 2D meshes this recovery method requires that elements are quadrilaterals' ) elif mesh.topological_dimension() == 3: # assume that 3D mesh is extruded if mesh._base_mesh.ufl_cell().cellname() != 'quadrilateral': raise NotImplementedError( 'For 3D extruded meshes this recovery method requires a base mesh with quadrilateral elements' ) elif mesh.topological_dimension() != 1: raise NotImplementedError( 'This boundary recovery is implemented only on certain classes of mesh.' ) if coords_to_adjust is None: raise ValueError( 'Need coords_to_adjust field for dynamics boundary methods' ) elif self.method == Boundary_Method.physics: # check that mesh is valid -- must be an extruded mesh if not VDG0.extruded: raise NotImplementedError( 'The physics boundary method only works on extruded meshes' ) # base spaces cell = mesh._base_mesh.ufl_cell().cellname() w_hori = FiniteElement("DG", cell, 0) w_vert = FiniteElement("CG", interval, 1) # build element theta_element = TensorProductElement(w_hori, w_vert) # spaces Vtheta = FunctionSpace(mesh, theta_element) Vtheta_broken = FunctionSpace(mesh, BrokenElement(theta_element)) if v_CG1.function_space() != Vtheta: raise ValueError( "This boundary recovery method requires v_CG1 to be in DG0xCG1 TensorProductSpace." ) if v_DG1.function_space() != Vtheta_broken: raise ValueError( "This boundary recovery method requires v_DG1 to be in the broken DG0xCG1 TensorProductSpace." ) else: raise ValueError( "Boundary method should be a Boundary Method Enum object.") VuDG1 = VectorFunctionSpace(VDG0.mesh(), "DG", 1) x = SpatialCoordinate(VDG0.mesh()) self.interpolator = Interpolator(self.v_CG1, self.v_DG1) if self.method == Boundary_Method.dynamics: # STRATEGY # obtain a coordinate field for all the nodes VuDG1 = VectorFunctionSpace(mesh, "DG", 1) self.act_coords = Function(VuDG1).project(x) # actual coordinates self.eff_coords = Function(VuDG1).project( x) # effective coordinates shapes = { "nDOFs": self.v_DG1.function_space().finat_element.space_dimension(), "dim": np.prod(VuDG1.shape, dtype=int) } num_ext_domain = ("{{[i]: 0 <= i < {nDOFs}}}").format(**shapes) num_ext_instructions = (""" <float64> SUM_EXT = 0 for i SUM_EXT = SUM_EXT + EXT_V1[i] end NUM_EXT[0] = SUM_EXT """) coords_domain = ("{{[i, j, k, ii, jj, kk, ll, mm, iii, kkk]: " "0 <= i < {nDOFs} and " "0 <= j < {nDOFs} and 0 <= k < {dim} and " "0 <= ii < {nDOFs} and 0 <= jj < {nDOFs} and " "0 <= kk < {dim} and 0 <= ll < {dim} and " "0 <= mm < {dim} and 0 <= iii < {nDOFs} and " "0 <= kkk < {dim}}}").format(**shapes) coords_insts = ( """ <float64> sum_V1_ext = 0 <int> index = 100 <float64> dist = 0.0 <float64> max_dist = 0.0 <float64> min_dist = 0.0 """ # only do adjustment in cells with at least one DOF to adjust """ if NUM_EXT[0] > 0 """ # find the maximum distance between DOFs in this cell, to serve as starting point for finding min distances """ for i for j dist = 0.0 for k dist = dist + pow(ACT_COORDS[i,k] - ACT_COORDS[j,k], 2.0) end dist = pow(dist, 0.5) {{id=sqrt_max_dist, dep=*}} max_dist = fmax(dist, max_dist) {{id=max_dist, dep=sqrt_max_dist}} end end """ # loop through cells and find which ones to adjust """ for ii if EXT_V1[ii] > 0.5 """ # find closest interior node """ min_dist = max_dist index = 100 for jj if EXT_V1[jj] < 0.5 dist = 0.0 for kk dist = dist + pow(ACT_COORDS[ii,kk] - ACT_COORDS[jj,kk], 2) end dist = pow(dist, 0.5) if dist <= min_dist index = jj end min_dist = fmin(min_dist, dist) for ll EFF_COORDS[ii,ll] = 0.5 * (ACT_COORDS[ii,ll] + ACT_COORDS[index,ll]) end end end else """ # for DOFs that aren't exterior, use the original coordinates """ for mm EFF_COORDS[ii, mm] = ACT_COORDS[ii, mm] end end end else """ # for interior elements, just use the original coordinates """ for iii for kkk EFF_COORDS[iii, kkk] = ACT_COORDS[iii, kkk] end end end """).format(**shapes) elimin_domain = ( "{{[i, ii_loop, jj_loop, kk, ll_loop, mm, iii_loop, kkk_loop, iiii, iiiii]: " "0 <= i < {nDOFs} and 0 <= ii_loop < {nDOFs} and " "ii_loop + 1 <= jj_loop < {nDOFs} and ii_loop <= kk < {nDOFs} and " "ii_loop + 1 <= ll_loop < {nDOFs} and ii_loop <= mm < {nDOFs} + 1 and " "0 <= iii_loop < {nDOFs} and {nDOFs} - iii_loop <= kkk_loop < {nDOFs} + 1 and " "0 <= iiii < {nDOFs} and 0 <= iiiii < {nDOFs}}}").format( **shapes) elimin_insts = ( """ <int> ii = 0 <int> jj = 0 <int> ll = 0 <int> iii = 0 <int> jjj = 0 <int> i_max = 0 <float64> A_max = 0.0 <float64> temp_f = 0.0 <float64> temp_A = 0.0 <float64> c = 0.0 <float64> f[{nDOFs}] = 0.0 <float64> a[{nDOFs}] = 0.0 <float64> A[{nDOFs},{nDOFs}] = 0.0 """ # We are aiming to find the vector a that solves A*a = f, for matrix A and vector f. # This is done by performing row operations (swapping and scaling) to obtain A in upper diagonal form. # N.B. several for loops must be executed in numerical order (loopy does not necessarily do this). # For these loops we must manually iterate the index. """ if NUM_EXT[0] > 0.0 """ # only do Gaussian elimination for elements with effective coordinates """ for i """ # fill f with the original field values and A with the effective coordinate values """ f[i] = DG1_OLD[i] A[i,0] = 1.0 A[i,1] = EFF_COORDS[i,0] if {nDOFs} > 3 A[i,2] = EFF_COORDS[i,1] A[i,3] = EFF_COORDS[i,0]*EFF_COORDS[i,1] if {nDOFs} > 7 A[i,4] = EFF_COORDS[i,{dim}-1] A[i,5] = EFF_COORDS[i,0]*EFF_COORDS[i,{dim}-1] A[i,6] = EFF_COORDS[i,1]*EFF_COORDS[i,{dim}-1] A[i,7] = EFF_COORDS[i,0]*EFF_COORDS[i,1]*EFF_COORDS[i,{dim}-1] end end end """ # now loop through rows/columns of A """ for ii_loop A_max = fabs(A[ii,ii]) i_max = ii """ # loop to find the largest value in the ith column # set i_max as the index of the row with this largest value. """ jj = ii + 1 for jj_loop if fabs(A[jj,ii]) > A_max i_max = jj end A_max = fmax(A_max, fabs(A[jj,ii])) jj = jj + 1 end """ # if the max value in the ith column isn't in the ith row, we must swap the rows """ if i_max != ii """ # swap the elements of f """ temp_f = f[ii] {{id=set_temp_f, dep=*}} f[ii] = f[i_max] {{id=set_f_imax, dep=set_temp_f}} f[i_max] = temp_f {{id=set_f_ii, dep=set_f_imax}} """ # swap the elements of A # N.B. kk runs from ii to (nDOFs-1) as elements below diagonal should be 0 """ for kk temp_A = A[ii,kk] {{id=set_temp_A, dep=*}} A[ii, kk] = A[i_max, kk] {{id=set_A_ii, dep=set_temp_A}} A[i_max, kk] = temp_A {{id=set_A_imax, dep=set_A_ii}} end end """ # scale the rows below the ith row """ ll = ii + 1 for ll_loop if ll > ii """ # find scaling factor """ c = - A[ll,ii] / A[ii,ii] """ # N.B. mm runs from ii to (nDOFs-1) as elements below diagonal should be 0 """ for mm A[ll, mm] = A[ll, mm] + c * A[ii,mm] end f[ll] = f[ll] + c * f[ii] end ll = ll + 1 end ii = ii + 1 end """ # do back substitution of upper diagonal A to obtain a """ iii = 0 for iii_loop """ # jjj starts at the bottom row and works upwards """ jjj = {nDOFs} - iii - 1 {{id=assign_jjj, dep=*}} a[jjj] = f[jjj] {{id=set_a, dep=assign_jjj}} for kkk_loop a[jjj] = a[jjj] - A[jjj,kkk_loop] * a[kkk_loop] end a[jjj] = a[jjj] / A[jjj,jjj] iii = iii + 1 end """ # Having found a, this gives us the coefficients for the Taylor expansion with the actual coordinates. """ for iiii if {nDOFs} == 2 DG1[iiii] = a[0] + a[1]*ACT_COORDS[iiii,0] elif {nDOFs} == 4 DG1[iiii] = a[0] + a[1]*ACT_COORDS[iiii,0] + a[2]*ACT_COORDS[iiii,1] + a[3]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,1] elif {nDOFs} == 8 DG1[iiii] = a[0] + a[1]*ACT_COORDS[iiii,0] + a[2]*ACT_COORDS[iiii,1] + a[3]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,1] + a[4]*ACT_COORDS[iiii,{dim}-1] + a[5]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,{dim}-1] + a[6]*ACT_COORDS[iiii,1]*ACT_COORDS[iiii,{dim}-1] + a[7]*ACT_COORDS[iiii,0]*ACT_COORDS[iiii,1]*ACT_COORDS[iiii,{dim}-1] end end """ # if element is not external, just use old field values. """ else for iiiii DG1[iiiii] = DG1_OLD[iiiii] end end """).format(**shapes) _num_ext_kernel = (num_ext_domain, num_ext_instructions) _eff_coords_kernel = (coords_domain, coords_insts) self._gaussian_elimination_kernel = (elimin_domain, elimin_insts) # find number of external DOFs per cell par_loop(_num_ext_kernel, dx, { "NUM_EXT": (self.num_ext, WRITE), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) # find effective coordinates logger.warning( 'Finding effective coordinates for boundary recovery. This could give unexpected results for deformed meshes over very steep topography.' ) par_loop(_eff_coords_kernel, dx, { "EFF_COORDS": (self.eff_coords, WRITE), "ACT_COORDS": (self.act_coords, READ), "NUM_EXT": (self.num_ext, READ), "EXT_V1": (self.coords_to_adjust, READ) }, is_loopy_kernel=True) elif self.method == Boundary_Method.physics: top_bottom_domain = ("{[i]: 0 <= i < 1}") bottom_instructions = (""" DG1[0] = 2 * CG1[0] - CG1[1] DG1[1] = CG1[1] """) top_instructions = (""" DG1[0] = CG1[0] DG1[1] = -CG1[0] + 2 * CG1[1] """) self._bottom_kernel = (top_bottom_domain, bottom_instructions) self._top_kernel = (top_bottom_domain, top_instructions)