← all demos

refinement

Adaptive refinement driven by an error estimator on a peaked Poisson source, against uniform refinement at the same cost.

uv run python examples/cli.py run refinement

The error against the number of unknowns, refining the whole mesh (blue) and refining where the estimator points (red). The peak is small next to the domain, so uniform refinement spends most of its unknowns where the solution is already flat; the adaptive mesh reaches the same error with about a third as many.
The error against the number of unknowns, refining the whole mesh (blue) and refining where the estimator points (red). The peak is small next to the domain, so uniform refinement spends most of its unknowns where the solution is already flat; the adaptive mesh reaches the same error with about a third as many.
Uniform mesh with a posteriori error estimate η. The estimator bounds the local discretization error; high values (red) indicate where the mesh under-resolves the solution.
Uniform mesh with a posteriori error estimate η. The estimator bounds the local discretization error; high values (red) indicate where the mesh under-resolves the solution.
After adaptive refinement driven by η: max error dropped 99% (914.898 → 6.580), ‖η‖ dropped 90% (1537.93 → 155.32).
After adaptive refinement driven by η: max error dropped 99% (914.898 → 6.580), ‖η‖ dropped 90% (1537.93 → 155.32).
Red splits an element into four; green bisects a neighbour so the mesh stays conforming.
Red splits an element into four; green bisects a neighbour so the mesh stays conforming.

Source

The functions that pose and solve the problem. The figures are below the fold.

"""Adaptive mesh refinement driven by an a posteriori error estimator, on a Poisson
problem whose solution is a sharp peak in the middle of a unit square.

`adapt`, `uniform_sweep`, and `adaptive_sweep` each pose and solve one study; `run`
calls them and returns a `RefinementStudy` of plain results. Nothing here draws:
`figures.py` does that from the `RefinementStudy`, and this file is what the gallery
shows.
"""
from dataclasses import dataclass
from math import e

import numpy as np
from mms import l2_norm

from fem.analysis.adaptivity import AdaptiveRefinement
from fem.analysis.estimators import ResidualEstimator
from fem.boundary import Dirichlet
from fem.conditions import Conditions
from fem.loads import Source
from fem.mesh.mesh import Mesh
from fem.mesh.refinement import RedGreenRefiner
from fem.mesh.structured import box_mesh
from fem.physics.equations import Poisson
from fem.post.solution import DiffusionSolution
from fem.regions import everywhere

W, H = 1.0, 1.0
A = 300      # the peak's sharpness: its width is about 1/sqrt(2a)
CENTRE = np.array([W/2, H/2])


# The source is -laplacian of a * exp(-a r^2), which is within 2e-4 of zero on the
# boundary, so that peak is the exact solution to the precision this chart needs.
def peaked_source(point):
    x, y = (point - CENTRE).T
    r2 = x**2 + y**2
    return 4*A*A*(1-A*r2)*e**(-A*r2)


def exact(points):
    r2 = np.sum((points - CENTRE)**2, axis=1)
    return A * np.exp(-A * r2)


bc = Conditions(Dirichlet(everywhere(), 0), Source(peaked_source))
equation = Poisson()
estimator = ResidualEstimator()


def square_mesh(n) -> Mesh:
    return box_mesh(corners=[[0.0, 0.0], [W, H]], resolution=(n, n))


def problem_for(m):
    return equation.problem(m, bc)


def solve(m):
    problem = problem_for(m)
    return problem, problem.solve()


def error_of(solution) -> float:
    """The L2 error of `solution` against the exact peak."""
    space = solution.space
    return l2_norm(space, solution.dofs - exact(space.node_coords))


def adapt(mesh, max_triangles=3000, max_iters=20) -> tuple[Mesh, DiffusionSolution, np.ndarray]:
    """Refine `mesh` where the estimator points, half the worst elements each round,
    until the mesh reaches `max_triangles` or `max_iters` rounds. Returns the refined
    mesh, its solution, and the estimated error per element."""
    refinement = AdaptiveRefinement(
        mesh,
        problem_for,
        estimator,
        max_triangles=max_triangles,
        max_iters=max_iters,
        refine_fraction=0.5,
    )
    solution = refinement.run()
    assert refinement.problem is not None
    return refinement.mesh, solution, estimator.estimate(refinement.problem, solution)


def uniform_sweep(resolutions) -> tuple[list[int], list[float]]:
    """The L2 error against unknowns, refining the whole square uniformly."""
    dofs, errors = [], []
    for n in resolutions:
        _, solution = solve(square_mesh(n))
        dofs.append(solution.space.n_dofs)
        errors.append(error_of(solution))
    return dofs, errors


def adaptive_sweep(mesh, rounds, max_dofs) -> tuple[list[int], list[float]]:
    """The L2 error against unknowns, refining adaptively from `mesh`. Each round
    refines the worst elements once and re-solves; each point is one round. Stops
    once a round passes `max_dofs`."""
    _, solution = solve(mesh)
    dofs = [solution.space.n_dofs]
    errors = [error_of(solution)]
    for _ in range(rounds):
        step = AdaptiveRefinement(mesh, problem_for, estimator, max_triangles=10**9,
                                  max_iters=1, refine_fraction=0.5)
        solution = step.run()
        mesh = step.mesh
        if solution.space.n_dofs > max_dofs:
            break
        dofs.append(solution.space.n_dofs)
        errors.append(error_of(solution))
    return dofs, errors


def red_green_example() -> tuple[Mesh, Mesh, list[str]]:
    """What red-green splitting does to an element: a four-triangle square, two of them
    refined red and then a third, with the leaves classified red or green."""
    vertices = np.array([[0, 0], [1, 0], [1, 1], [0, 1], [0.5, 0.5]])
    elements = np.array([[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]])
    boundary = [[0, 1], [1, 2], [2, 3], [3, 0]]
    original = Mesh(vertices, elements, boundary)
    small = original

    refiner = RedGreenRefiner(small)
    small = refiner.refine([0, 2])
    small = refiner.refine([1])
    return original, small, refiner.leaf_classifications()


@dataclass
class RefinementStudy:
    """Everything `run` computed, for the figures to read."""
    coarse_mesh: Mesh
    coarse_solution: DiffusionSolution
    coarse_error: np.ndarray        # the estimator's eta per element
    refined_mesh: Mesh
    refined_solution: DiffusionSolution
    refined_error: np.ndarray
    uniform_dofs: list[int]
    uniform_errors: list[float]
    adaptive_dofs: list[int]
    adaptive_errors: list[float]
    red_green_original: Mesh
    red_green_refined: Mesh
    red_green_classes: list[str]

    @property
    def n_coarse(self) -> int:
        return len(self.coarse_mesh.elements)

    @property
    def n_refined(self) -> int:
        return len(self.refined_mesh.elements)

    @property
    def coarse_max(self) -> float:
        return float(self.coarse_error.max())

    @property
    def coarse_norm(self) -> float:
        return float(np.sqrt(np.sum(self.coarse_error**2)))

    @property
    def refined_max(self) -> float:
        return float(self.refined_error.max())

    @property
    def refined_norm(self) -> float:
        return float(np.sqrt(np.sum(self.refined_error**2)))

    @property
    def max_reduction(self) -> float:
        """Percent drop in the largest element error from coarse to refined."""
        return 100 * (1 - self.refined_max / self.coarse_max)

    @property
    def norm_reduction(self) -> float:
        """Percent drop in the estimator's norm from coarse to refined."""
        return 100 * (1 - self.refined_norm / self.coarse_norm)


def run(_mesh, uniform_resolutions=(10, 20, 40, 80, 160), adaptive_rounds=30,
        coarse_resolution=20) -> RefinementStudy:
    """Solve on a coarse square, refine it adaptively, and compare the error against
    cost of uniform and adaptive refinement. The registry's mesh is ignored: the study
    builds its own squares at the resolutions it needs."""
    mesh = square_mesh(coarse_resolution)

    coarse_mesh = mesh
    coarse_problem, coarse_solution = solve(coarse_mesh)
    coarse_error = estimator.estimate(coarse_problem, coarse_solution)

    refined_mesh, refined_solution, refined_error = adapt(mesh)

    uniform_dofs, uniform_errors = uniform_sweep(uniform_resolutions)
    adaptive_dofs, adaptive_errors = adaptive_sweep(mesh, adaptive_rounds,
                                                    max(uniform_dofs) // 2)

    original, refined, classes = red_green_example()
    return RefinementStudy(coarse_mesh, coarse_solution, coarse_error,
                           refined_mesh, refined_solution, refined_error,
                           uniform_dofs, uniform_errors, adaptive_dofs, adaptive_errors,
                           original, refined, classes)
The figures, and the demo that assembles them
"""The figures of the adaptive refinement demo, drawn from a `RefinementStudy`."""
from functools import partial

from demo_registry import Demo, DemoResult, Figure

from demos.refinement import physics
from demos.refinement.physics import RefinementStudy, run
from fem.mesh.structured import box_mesh
from fem.plot.plotter import Plotter


def _error_clim(s: RefinementStudy) -> tuple[float, float]:
    """A shared log scale for both error plots."""
    min_error = min(s.coarse_error.min(), s.refined_error.min())
    max_error = max(s.coarse_error.max(), s.refined_error.max())
    return (max(min_error, 1e-10), max_error)


def _before_figure(s: RefinementStudy) -> Figure:
    before = Plotter(1, 3, title=f'Before: uniform mesh ({s.n_coarse} elements)')
    before.plot(s.coarse_mesh, s.coarse_solution.dofs, mode='surface', title='Solution',
                idx=(0, 0))
    before.plot(s.coarse_mesh, mode='mesh', title='Mesh', idx=(0, 1))
    before.plot(s.coarse_mesh, s.coarse_error, mode='colored',
                title=f'Error η (max: {s.coarse_max:.3f}, ‖η‖: {s.coarse_norm:.2f})',
                idx=(0, 2), clim=_error_clim(s), cmap='YlOrRd', log_scale=True)
    return Figure(
        before,
        'Uniform mesh with a posteriori error estimate η. The estimator bounds '
        'the local discretization error; high values (red) indicate where the '
        'mesh under-resolves the solution.',
        'before')


def _after_figure(s: RefinementStudy) -> Figure:
    after = Plotter(1, 3, title=f'After: adaptive refinement ({s.n_refined} elements)')
    after.plot(s.refined_mesh, s.refined_solution.dofs, mode='surface', title='Solution',
               idx=(0, 0))
    after.plot(s.refined_mesh, mode='mesh', title='Mesh', idx=(0, 1))
    after.plot(s.refined_mesh, s.refined_error, mode='colored',
               title=f'Error η (max: {s.refined_max:.3f}, ‖η‖: {s.refined_norm:.2f})',
               idx=(0, 2), clim=_error_clim(s), cmap='YlOrRd', log_scale=True)
    return Figure(
        after,
        f'After adaptive refinement driven by η: max error dropped {s.max_reduction:.0f}% '
        f'({s.coarse_max:.3f} → {s.refined_max:.3f}), ‖η‖ dropped {s.norm_reduction:.0f}% '
        f'({s.coarse_norm:.2f} → {s.refined_norm:.2f}).',
        'after')


def _payoff_figure(s: RefinementStudy) -> Figure:
    """The payoff: error against cost, refining uniformly and adaptively."""
    payoff = Plotter(title='Error against cost')
    chart = payoff.chart_ax(xlabel='degrees of freedom', ylabel='L2 error')
    chart.loglog(s.uniform_dofs, s.uniform_errors, 'o-', color='tab:blue', label='uniform')
    chart.loglog(s.adaptive_dofs, s.adaptive_errors, '.-', color='tab:red', label='adaptive')
    chart.set_title('Uniform against adaptive refinement')
    chart.grid(True, which='both', alpha=0.3)
    chart.legend()
    return Figure(
        payoff,
        'The error against the number of unknowns, refining the whole mesh (blue) '
        'and refining where the estimator points (red). The peak is small next to '
        'the domain, so uniform refinement spends most of its unknowns where the '
        'solution is already flat; the adaptive mesh reaches the same error with '
        'about a third as many.',
        'payoff')


def _machinery_figure(s: RefinementStudy) -> Figure:
    machinery = Plotter(1, 2, title='Red-green refinement', axis_labels=False)
    machinery.plot(s.red_green_original, mode='mesh', idx=(0, 0), title='Original')
    machinery.plot(s.red_green_refined, values=s.red_green_classes, mode='refinement',
                   idx=(0, 1), title='Refined (red / green)')
    return Figure(
        machinery,
        'Red splits an element into four; green bisects a neighbour so '
        'the mesh stays conforming.',
        'red-green')


def demo(_mesh, **kwargs) -> DemoResult:
    """Adaptive refinement driven by an error estimator on a peaked Poisson source,
    against uniform refinement at the same cost."""
    s = run(_mesh, **kwargs)
    return DemoResult([
        _payoff_figure(s),
        _before_figure(s),
        _after_figure(s),
        _machinery_figure(s),
    ])


DEMO = Demo('refinement', demo, section='Accuracy & performance',
            domain=partial(box_mesh, [[0.0, 0.0], [1.0, 1.0]], (40, 40)),
            show_source=physics,
            smoke_kwargs={'uniform_resolutions': (10, 20), 'adaptive_rounds': 2})