示例#1
0
文件: mesh.py 项目: hyharry/firedrake
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)
示例#2
0
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)
示例#3
0
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)
示例#4
0
    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,
示例#5
0
文件: mesh.py 项目: hyharry/firedrake
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
示例#6
0
文件: io.py 项目: mc4117/adapt_utils
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)
示例#7
0
文件: mesh.py 项目: hyharry/firedrake
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
示例#8
0
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()
示例#9
0
文件: mesh.py 项目: hyharry/firedrake
def _from_cgns(filename):
    """Read a CGNS .cgns file from `filename`"""
    plex = PETSc.DMPlex().createCGNSFromFile(filename)

    # TODO: Add boundary IDs
    return plex
示例#10
0
    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")