def _from_cell_list(dim, cells, coords, comm=None): """ Create a DMPlex from a list of cells and coords. :arg dim: The topological dimension of the mesh :arg cells: The vertices of each cell :arg coords: The coordinates of each vertex :arg comm: An optional MPI communicator to build the plex on (defaults to ``COMM_WORLD``) """ if comm is None: comm = op2.MPI.comm if comm.rank == 0: cells = np.asarray(cells, dtype=PETSc.IntType) coords = np.asarray(coords, dtype=float) comm.bcast(cells.shape, root=0) comm.bcast(coords.shape, root=0) # Provide the actual data on rank 0. return PETSc.DMPlex().createFromCellList(dim, cells, coords, comm=comm) cell_shape = list(comm.bcast(None, root=0)) coord_shape = list(comm.bcast(None, root=0)) cell_shape[0] = 0 coord_shape[0] = 0 # Provide empty plex on other ranks # A subsequent call to plex.distribute() takes care of parallel partitioning return PETSc.DMPlex().createFromCellList(dim, np.zeros(cell_shape, dtype=PETSc.IntType), np.zeros(coord_shape, dtype=float), comm=comm)
def RectangleMesh(nx, ny, Lx, Ly, quadrilateral=False, reorder=None): """Generate a rectangular mesh :arg nx: The number of cells in the x direction :arg ny: The number of cells in the y direction :arg Lx: The extent in the x direction :arg Ly: The extent in the y direction :kwarg quadrilateral: (optional), creates quadrilateral mesh, defaults to False :kwarg reorder: (optional), should the mesh be reordered The boundary edges in this mesh are numbered as follows: * 1: plane x == 0 * 2: plane x == Lx * 3: plane y == 0 * 4: plane y == Ly """ if quadrilateral: dx = float(Lx) / nx dy = float(Ly) / ny xcoords = np.arange(0.0, Lx + 0.01 * dx, dx) ycoords = np.arange(0.0, Ly + 0.01 * dy, dy) coords = np.asarray(np.meshgrid(xcoords, ycoords)).swapaxes(0, 2).reshape(-1, 2) # cell vertices i, j = np.meshgrid(np.arange(nx), np.arange(ny)) cells = [i*(ny+1) + j, i*(ny+1) + j+1, (i+1)*(ny+1) + j+1, (i+1)*(ny+1) + j] cells = np.asarray(cells).swapaxes(0, 2).reshape(-1, 4) plex = mesh._from_cell_list(2, cells, coords) else: boundary = PETSc.DMPlex().create(MPI.comm) boundary.setDimension(1) boundary.createSquareBoundary([0., 0.], [float(Lx), float(Ly)], [nx, ny]) boundary.setTriangleOptions("pqezQYSl") plex = PETSc.DMPlex().generate(boundary) # mark boundary facets plex.createLabel("boundary_ids") plex.markBoundaryFaces("boundary_faces") coords = plex.getCoordinates() coord_sec = plex.getCoordinateSection() if plex.getStratumSize("boundary_faces", 1) > 0: boundary_faces = plex.getStratumIS("boundary_faces", 1).getIndices() xtol = float(Lx)/(2*nx) ytol = float(Ly)/(2*ny) for face in boundary_faces: face_coords = plex.vecGetClosure(coord_sec, coords, face) if abs(face_coords[0]) < xtol and abs(face_coords[2]) < xtol: plex.setLabelValue("boundary_ids", face, 1) if abs(face_coords[0] - Lx) < xtol and abs(face_coords[2] - Lx) < xtol: plex.setLabelValue("boundary_ids", face, 2) if abs(face_coords[1]) < ytol and abs(face_coords[3]) < ytol: plex.setLabelValue("boundary_ids", face, 3) if abs(face_coords[1] - Ly) < ytol and abs(face_coords[3] - Ly) < ytol: plex.setLabelValue("boundary_ids", face, 4) return mesh.Mesh(plex, reorder=reorder)
def BoxMesh(nx, ny, nz, Lx, Ly, Lz, reorder=None): """Generate a mesh of a 3D box. :arg nx: The number of cells in the x direction :arg ny: The number of cells in the y direction :arg nz: The number of cells in the z direction :arg Lx: The extent in the x direction :arg Ly: The extent in the y direction :arg Lz: The extent in the z direction :kwarg reorder: (optional), should the mesh be reordered? The boundary surfaces are numbered as follows: * 1: plane x == 0 * 2: plane x == Lx * 3: plane y == 0 * 4: plane y == Ly * 5: plane z == 0 * 6: plane z == Lz """ # Create mesh from DMPlex boundary = PETSc.DMPlex().create(MPI.comm) boundary.setDimension(2) boundary.createCubeBoundary([0., 0., 0.], [Lx, Ly, Lz], [nx, ny, nz]) plex = PETSc.DMPlex().generate(boundary) # Apply boundary IDs plex.createLabel("boundary_ids") plex.markBoundaryFaces("boundary_faces") coords = plex.getCoordinates() coord_sec = plex.getCoordinateSection() if plex.getStratumSize("boundary_faces", 1) > 0: boundary_faces = plex.getStratumIS("boundary_faces", 1).getIndices() xtol = float(Lx)/(2*nx) ytol = float(Ly)/(2*ny) ztol = float(Lz)/(2*nz) for face in boundary_faces: face_coords = plex.vecGetClosure(coord_sec, coords, face) if abs(face_coords[0]) < xtol and abs(face_coords[3]) < xtol and abs(face_coords[6]) < xtol: plex.setLabelValue("boundary_ids", face, 1) if abs(face_coords[0] - Lx) < xtol and abs(face_coords[3] - Lx) < xtol and abs(face_coords[6] - Lx) < xtol: plex.setLabelValue("boundary_ids", face, 2) if abs(face_coords[1]) < ytol and abs(face_coords[4]) < ytol and abs(face_coords[7]) < ytol: plex.setLabelValue("boundary_ids", face, 3) if abs(face_coords[1] - Ly) < ytol and abs(face_coords[4] - Ly) < ytol and abs(face_coords[7] - Ly) < ytol: plex.setLabelValue("boundary_ids", face, 4) if abs(face_coords[2]) < ztol and abs(face_coords[5]) < ztol and abs(face_coords[8]) < ztol: plex.setLabelValue("boundary_ids", face, 5) if abs(face_coords[2] - Lz) < ztol and abs(face_coords[5] - Lz) < ztol and abs(face_coords[8] - Lz) < ztol: plex.setLabelValue("boundary_ids", face, 6) return mesh.Mesh(plex, reorder=reorder)
def initialise_fields(self, inputdir, outputdir): """ Initialise simulation with results from a previous simulation """ from firedrake.petsc import PETSc # mesh with timed_stage('mesh'): # Load newplex = PETSc.DMPlex().create() newplex.createFromFile(inputdir + '/myplex.h5') mesh = Mesh(newplex) DG_2d = FunctionSpace(mesh, 'DG', 1) vector_dg = VectorFunctionSpace(mesh, 'DG', 1) # elevation with timed_stage('initialising elevation'): chk = DumbCheckpoint(inputdir + "/elevation", mode=FILE_READ) elev_init = Function(DG_2d, name="elevation") chk.load(elev_init) #File(outputdir + "/elevation_imported.pvd").write(elev_init) chk.close() # velocity with timed_stage('initialising velocity'): chk = DumbCheckpoint(inputdir + "/velocity", mode=FILE_READ) uv_init = Function(vector_dg, name="velocity") chk.load(uv_init) #File(outputdir + "/velocity_imported.pvd").write(uv_init) chk.close() return elev_init, uv_init,
def _from_exodus(filename): """Read an Exodus .e or .exo file from `filename`""" plex = PETSc.DMPlex().createExodusFromFile(filename) boundary_ids = dmplex.getLabelIdIS("Face Sets").getIndices() plex.createLabel("boundary_ids") for bid in boundary_ids: faces = plex.getStratumIS("Face Sets", bid).getIndices() for f in faces: plex.setLabelValue("boundary_ids", f, bid) return plex
def load_mesh(fname, fpath='.', delete=False): """ :arg fname: file name (without '.h5' extension). :kwarg fpath: directory where the file is stored. :kwarg delete: toggle deletion of the file. :return: mesh loaded from DMPlex format. """ if COMM_WORLD.size > 1: raise IOError("Loading a mesh from HDF5 only works in serial.") newplex = PETSc.DMPlex().create() newplex.createFromFile(os.path.join(fpath, fname + '.h5')) # Optionally delete the HDF5 file if delete: os.remove(os.path.join(fpath, fname) + '.h5') return Mesh(newplex)
def _from_gmsh(filename): """Read a Gmsh .msh file from `filename`""" # Create a read-only PETSc.Viewer gmsh_viewer = PETSc.Viewer().create() gmsh_viewer.setType("ascii") gmsh_viewer.setFileMode("r") gmsh_viewer.setFileName(filename) gmsh_plex = PETSc.DMPlex().createGmsh(gmsh_viewer) if gmsh_plex.hasLabel("Face Sets"): boundary_ids = gmsh_plex.getLabelIdIS("Face Sets").getIndices() gmsh_plex.createLabel("boundary_ids") for bid in boundary_ids: faces = gmsh_plex.getStratumIS("Face Sets", bid).getIndices() for f in faces: gmsh_plex.setLabelValue("boundary_ids", f, bid) return gmsh_plex
def run_steady_turbine(**model_options): """ Consider a simple test case with two turbines positioned in a channel. The mesh has been adapted with respect to fluid speed and so has strong anisotropy in the direction of flow. If the default SIPG parameter is used, this steady state problem fails to converge. However, using the automatic SIPG parameter functionality, it should converge. """ # Load an anisotropic mesh from file plex = PETSc.DMPlex().create() abspath = os.path.realpath(__file__) plex.createFromFile( abspath.replace('test_anisotropic.py', 'anisotropic_plex.h5')) mesh2d = Mesh(plex) x, y = SpatialCoordinate(mesh2d) # Create steady state solver object solver_obj = solver2d.FlowSolver2d(mesh2d, Constant(40.0)) options = solver_obj.options options.timestep = 20.0 options.simulation_export_time = 20.0 options.simulation_end_time = 18.0 options.timestepper_type = 'SteadyState' options.timestepper_options.solver_parameters = { 'mat_type': 'aij', 'snes_type': 'newtonls', 'snes_rtol': 1e-8, 'snes_monitor': None, 'pc_type': 'lu', 'pc_factor_mat_solver_type': 'mumps', } options.output_directory = 'outputs' options.fields_to_export = ['uv_2d', 'elev_2d'] options.use_grad_div_viscosity_term = False options.element_family = 'dg-dg' options.horizontal_viscosity = Constant(1.0) options.quadratic_drag_coefficient = Constant(0.0025) options.use_lax_friedrichs_velocity = True options.lax_friedrichs_velocity_scaling_factor = Constant(1.0) options.use_grad_depth_viscosity_term = False options.update(model_options) solver_obj.create_equations() # Apply boundary conditions solver_obj.bnd_functions['shallow_water'] = { 1: { 'uv': Constant([3.0, 0.0]) }, 2: { 'elev': Constant(0.0) }, 3: { 'un': Constant(0.0) }, } def bump(fs, locs, scale=1.0): """Scaled bump function for turbines.""" i = 0 for j in range(len(locs)): x0 = locs[j][0] y0 = locs[j][1] r = locs[j][2] expr1 = (x - x0) * (x - x0) + (y - y0) * (y - y0) expr2 = scale * exp(1 - 1 / (1 - (x - x0) * (x - x0) / r**2)) * exp(1 - 1 / (1 - (y - y0) * (y - y0) / r**2)) i += conditional(lt(expr1, r * r), expr2, 0) return i # Set up turbine array L = 1000.0 # domain length W = 300.0 # domain width D = 18.0 # turbine diameter A = pi * (D / 2)**2 # turbine area locs = [(L / 2 - 8 * D, W / 2, D / 2), (L / 2 + 8 * D, W / 2, D / 2)] # turbine locations # NOTE: We include a correction to account for the fact that the thrust coefficient is based # on an upstream velocity, whereas we are using a depth averaged at-the-turbine velocity # (see Kramer and Piggott 2016, eq. (15)). correction = 4 / (1 + sqrt(1 - A / (40.0 * D)))**2 scaling = len(locs) / assemble( bump(solver_obj.function_spaces.P1DG_2d, locs) * dx) farm_options = TidalTurbineFarmOptions() farm_options.turbine_density = bump(solver_obj.function_spaces.P1DG_2d, locs, scale=scaling) farm_options.turbine_options.diameter = D farm_options.turbine_options.thrust_coefficient = 0.8 * correction solver_obj.options.tidal_turbine_farms['everywhere'] = farm_options # Apply initial guess of inflow velocity and solve solver_obj.assign_initial_conditions(uv=Constant([3.0, 0.0])) solver_obj.iterate()
def _from_cgns(filename): """Read a CGNS .cgns file from `filename`""" plex = PETSc.DMPlex().createCGNSFromFile(filename) # TODO: Add boundary IDs return plex
def fixed_point_iteration(self, **parsed_args): """ Apply a goal-oriented metric-based mesh adaptation fixed point iteration loop for a tidal farm modelling problem. """ parsed_args = AttrDict(parsed_args) options = self.options expected = { "miniter", "maxiter", "load_index", "qoi_rtol", "element_rtol", "error_indicator", "approach", "h_min", "h_max", "turbine_h_min", "turbine_h_max", "a_max", "target_complexity", "base_complexity", "flux_form", "norm_order", "no_final_run", } if not expected.issubset(set(parsed_args.keys())): missing = expected.difference(set(parsed_args.keys())) raise AttributeError(f"Missing required arguments {missing}") output_dir = options.output_directory end_time = options.simulation_end_time dt = options.timestep approach = parsed_args.approach h_min = parsed_args.h_min h_max = parsed_args.h_max turbine_h_min = parsed_args.turbine_h_min turbine_h_max = parsed_args.turbine_h_max a_max = parsed_args.a_max num_timesteps = int(np.round(end_time / dt)) target = num_timesteps * parsed_args.target_complexity base = num_timesteps * parsed_args.base_complexity num_subintervals = self.num_subintervals timesteps = [dt] * num_subintervals p = parsed_args.norm_order no_final_run = parsed_args.no_final_run if COMM_WORLD.size > 1: raise NotImplementedError( "Mmg2d only supports serial mesh adaptation") # Enter fixed point iteration miniter = parsed_args.miniter maxiter = parsed_args.maxiter if miniter > maxiter: print_output( f"miniter {miniter} and maxiter {maxiter} are incompatible") miniter = maxiter print_output(f"Setting miniter={miniter}, maxiter={maxiter}") qoi_rtol = parsed_args.qoi_rtol element_rtol = parsed_args.element_rtol converged_reason = None load_index = parsed_args.load_index if load_index > 0: self.keep_log = True fp_iteration = load_index msg = "Termination due to {:s} after {:d} iterations" cells = [] if load_index == 0 else list( np.load(f"{output_dir}/num_cells_progress.npy")) qois = [] if load_index == 0 else list( np.load(f"{output_dir}/J_progress.npy")) self.J = 0 if load_index == 0 else qois[-1] def check_cell_count_convergence(): if len(cells) >= max(2, miniter): elements_converged = True for nc, nc_old in zip(cells[-1], cells[-2]): if abs(nc - nc_old) > element_rtol * nc_old: elements_converged = False break if elements_converged: return "converged element counts" def check_qoi_convergence(): if len(qois) >= max(2, miniter): if abs(qois[-1] - qois[-2]) < qoi_rtol * qois[-2]: return "converged quantity of interest" # Check for convergence (of loaded data) converged_reason = check_qoi_convergence( ) or check_cell_count_convergence() # Load meshes, if requested if load_index > 0: for i in range(num_subintervals): fname = f"{output_dir}/mesh_fp{fp_iteration}_{i}" if not os.path.exists(fname + ".h5"): raise IOError(f"Cannot load mesh file {fname}.") print_output(f"\n--- Loading plex {i+1}\n{fname}") plex = PETSc.DMPlex().create() plex.createFromFile(fname + ".h5") self.meshes[i] = Mesh(plex) # Do final run, if loaded data has already converged if converged_reason is not None: if not no_final_run: print(f"converged_reason: {converged_reason}") self._final_run() print_output(msg.format(converged_reason, fp_iteration + 1)) print_output(f"Energy output: {self.J/3.6e+09} MWh") return # Enter the fixed point iteration loop while fp_iteration <= maxiter: print_output( f"Start time for fp_iteration {fp_iteration}: {datetime.datetime.now()}" ) outfiles = AttrDict({}) if fp_iteration < miniter: converged_reason = None elif fp_iteration == maxiter: converged_reason = converged_reason or "maximum number of iterations reached" # Ramp up the target complexity target_ramp = ramp_complexity(base, target, fp_iteration) # Create metrics kw = dict(metric_parameters=dict(dm_plex_metric_verbosity=10)) metrics = [RiemannianMetric(mesh, **kw) for mesh in self.meshes] metric_fns = [metric.function for metric in metrics] # Load metric data, if available loaded = False if fp_iteration == load_index: for i, metric in enumerate(metric_fns): fpath = self.root_dir if load_index == 0 else output_dir ext = "" if load_index == 0 else "_fp{fp_iteration}" fname = f"{fpath}/metric{i}{ext}" if os.path.exists(fname + ".h5"): print_output( f"\n--- Loading metric on mesh {i+1}\n{fname}") try: with DumbCheckpoint(fname, mode=FILE_READ) as chk: chk.load(metric, name="Metric") loaded = True except Exception: raise IOError( f"Cannot load metric data on mesh {i+1}") elif loaded: raise IOError("Remove partial metric data") if not loaded: # Solve forward and adjoint on each subinterval if converged_reason is None: print_output( f"\n--- Forward-adjoint sweep {fp_iteration}\n") solutions = self.solve_adjoint() else: if not no_final_run: print(f"converged_reason: {converged_reason}") self._final_run() # Check for QoI convergence converged_reason = converged_reason or check_qoi_convergence() if converged_reason is not None: if not no_final_run: print(f"converged_reason: {converged_reason}") self._final_run() qois.append(self.J) np.save(f"{output_dir}/J_progress.npy", qois) # Escape if converged if converged_reason is not None: print_output(msg.format(converged_reason, fp_iteration + 1)) print_output(f"Energy output: {self.J/3.6e+09} MWh") break # Create vtu output files outfiles.forward = File(f"{output_dir}/Forward2d.pvd") outfiles.forward_old = File(f"{output_dir}/ForwardOld2d.pvd") outfiles.adjoint_next = File(f"{output_dir}/AdjointNext2d.pvd") outfiles.adjoint = File(f"{output_dir}/Adjoint2d.pvd") # Construct metric with pyadjoint.stop_annotating(): print_output(f"\n--- Error estimation {fp_iteration}\n") for i, mesh in enumerate(self.meshes): options.rebuild_mesh_dependent_components(mesh) options.get_bnd_conditions( self.function_spaces.swe2d[i]) update_forcings = options.update_forcings # Create error estimator ee = ErrorEstimator( options, mesh=mesh, metric=approach, error_estimator=parsed_args.error_indicator, ) # Loop over all exported timesteps N = len(solutions.swe2d.adjoint[i]) for j in range(N): if i < num_subintervals - 1 and j == N - 1: continue # Plot fields args = [] for f in outfiles: args.extend(solutions.swe2d[f][i][j].split()) name = "adjoint " if "adjoint" in f else "" args[-2].rename( (name + "velocity").capitalize()) args[-1].rename( (name + "elevation").capitalize()) outfiles[f].write(*args[-2:]) # Construct metric at current timestep t = i * end_time / num_subintervals + dt * (j + 1) update_forcings(t) metric_step = ee.metric(*args, **parsed_args) # Apply trapezium rule metric_step *= 0.5 * dt if j in (0, N - 1) else dt metric_fns[i] += metric_step # Stash metric data print_output( f"\n--- Storing metric data on mesh {i+1}\n") fname = f"{output_dir}/metric{i}_fp{fp_iteration}" with DumbCheckpoint(fname, mode=FILE_CREATE) as chk: chk.store(metric_fns[i], name="Metric") if fp_iteration == 0: fname = f"{self.root_dir}/metric{i}" with DumbCheckpoint(fname, mode=FILE_CREATE) as chk: chk.store(metric_fns[i], name="Metric") # Apply space-time normalisation print_output(f"\n--- Metric processing {fp_iteration}\n") space_time_normalise(metric_fns, end_time, timesteps, target_ramp, p) # Enforce element constraints, accounting for turbines hmins = [] hmaxs = [] for mesh in self.meshes: P0 = get_functionspace(mesh, "DG", 0) hmin = Function(P0).assign(h_min) hmax = Function(P0).assign(h_max) for tag in self.qoi_farm_ids.flatten(): hmin.assign(turbine_h_min, subset=mesh.cell_subset(tag)) hmax.assign(turbine_h_max, subset=mesh.cell_subset(tag)) hmins.append(hmin) hmaxs.append(hmax) enforce_element_constraints(metric_fns, hmins, hmaxs, a_max) # Plot metrics outfiles.metric = File(f"{output_dir}/Metric2d.pvd") for metric in metric_fns: outfiles.metric.write(metric) # Adapt meshes print_output(f"\n--- Mesh adaptation {fp_iteration}\n") outfiles.mesh = File(f"{output_dir}/Mesh2d.pvd") for i, metric in enumerate(metrics): self.meshes[i] = Mesh(adapt(self.meshes[i], metric)) outfiles.mesh.write(self.meshes[i].coordinates) cells.append([mesh.num_cells() for mesh in self.meshes]) np.save(f"{output_dir}/num_cells_progress.npy", cells) # Check for convergence of element count check_cell_count_convergence() # Save mesh data to disk for i, mesh in enumerate(self.meshes): fname = f"{output_dir}/mesh_fp{fp_iteration+1}_{i}.h5" viewer = PETSc.Viewer().createHDF5(fname, "w") viewer(mesh.topology_dm) # Increment print_output( f"End time for fp_iteration {fp_iteration}: {datetime.datetime.now()}" ) fp_iteration += 1 print_output(msg.format(converged_reason, fp_iteration + 1)) print_output(f"Energy output: {self.J/3.6e+09} MWh")