-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a MA demo solving Helmholtz PDE #53
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
# Using mesh movement to optimize the mesh for PDE solution | ||
# ========================================================= | ||
# | ||
# In this demo will demonstrate how we might use mesh movement to obtain a mesh | ||
# that is optimized for solving a particular PDE. The general idea is that we | ||
# want to reduce numerical error by increasing resolution where the local error | ||
# is (expected to be) high and decrease it elsewhere. | ||
# | ||
# As an example we will look at the discretisation of the Helmholtz equation | ||
# | ||
# .. math:: | ||
# | ||
# -\nabla^2 u + u &= f | ||
# | ||
# \nabla u \cdot \vec{n} &= 0 \quad \textrm{on}\ \Gamma | ||
# | ||
# For an explanation of how we can use Firedrake to implement a Finite Element | ||
# Method (FEM) discretisation of this PDE see the `corresponding Firedrake demo | ||
# <https://www.firedrakeproject.org/demos/helmholtz.py.html>`_. The only | ||
# changes we introduce is that we choose a different, slightly more interesting | ||
# solution :math:`u` and rhs :math:`f` | ||
# | ||
# .. math:: | ||
# | ||
# u(x, y) &= \exp\left(-\frac{(x-x_0)^2 + (y-y_0)^2}{w^2}\right) | ||
# | ||
# f(x, y) &= -\nabla^2 u(x, y) + u(x, y) | ||
# = \left[ -\frac{4(x-x_0)^2 + 4(y-y_0)^2}{w^4} | ||
# + \frac{4}{w^2} + 1 \right] | ||
# \exp\left(-\frac{(x-x_0)^2 + (y-y_0)^2}{w^2}\right) | ||
# | ||
# where :math:`(x_0, y_0)` is the centre of the Gaussian with width :math:`w`. | ||
# Note that here we *first* choose the solution :math:`u` after which we can | ||
# compute what rhs :math:`f` should be, by substitution in the PDE, in order | ||
# for :math:`u` to be the analytical solution. This so called Method of | ||
# Manufactured Solutions approach is an easy way to construct PDE | ||
# configurations for which we know the analytical solution, e.g. for testing | ||
# purposes. | ||
# | ||
# Based on the code in the Firedrake demo, we first solve the PDE on a uniform mesh. | ||
# Because our chosen solution does not satisfy homogeneous Neumann boundary conditions, | ||
# we instead apply Dirichlet boundary conditions based on the chosen analytical solution. | ||
|
||
from firedrake import * | ||
from movement import MongeAmpereMover | ||
|
||
mesh = UnitSquareMesh(20, 20) # initial mesh | ||
|
||
|
||
def u_exact(mesh): | ||
"""Return UFL expression of exact solution""" | ||
x, y = SpatialCoordinate(mesh) | ||
# some arbitrarily chosen centre (x0, y0) and width w | ||
w = Constant(0.1) | ||
x0 = Constant(0.51) | ||
y0 = Constant(0.65) | ||
return exp(-((x - x0) ** 2 + (y - y0) ** 2) / w**2) | ||
|
||
|
||
def solve_helmholtz(mesh): | ||
"""Solve the Helmholtz PDE on the given mesh""" | ||
V = FunctionSpace(mesh, "CG", 1) | ||
u = TrialFunction(V) | ||
v = TestFunction(V) | ||
u_exact_expr = u_exact(mesh) | ||
f = -div(grad(u_exact_expr)) + u_exact_expr | ||
a = (inner(grad(u), grad(v)) + inner(u, v)) * dx | ||
L = inner(f, v) * dx | ||
u = Function(V) | ||
bcs = DirichletBC(V, u_exact_expr, "on_boundary") | ||
solve(a == L, u, bcs=bcs) | ||
return u | ||
|
||
|
||
u_h = solve_helmholtz(mesh) | ||
|
||
import matplotlib.pyplot as plt | ||
from firedrake.pyplot import tripcolor | ||
|
||
fig, axes = plt.subplots() | ||
contours = tripcolor(u_h, axes=axes) | ||
fig.colorbar(contours) | ||
plt.savefig("monge_ampere_helmholtz-initial_solution.jpg") | ||
|
||
# .. figure:: monge_ampere_helmholtz-initial_solution.jpg | ||
# :figwidth: 60% | ||
# :align: center | ||
# | ||
# As in the Helmholtz demo, we can compute the L2-norm of the numerical error: | ||
|
||
error = u_h - u_exact(mesh) | ||
print("L2-norm error on initial mesh:", sqrt(assemble(dot(error, error) * dx))) | ||
|
||
# .. code-block:: none | ||
# | ||
# L2-norm error on initial mesh: 0.010233816824277465 | ||
# | ||
# We will now try to use mesh movement to optimize the mesh to reduce | ||
# this numerical error. A good indicator for where resolution is required | ||
# is to look at the curvature of the solution which can be expressed | ||
# in terms of the norm of the Hessian. A monitor function | ||
# that targets high resolution in places of high curvature then looks like | ||
# | ||
# .. math:: | ||
# | ||
# m = 1 + \alpha \frac{H(u_h):H(u_h)}{\max_{{\bf x}\in\Omega} H(u_h):H(u_h)} | ||
# | ||
# where :math:`:` indicates the inner product, i.e. :math:`\sqrt{H:H}` is the Frobenius norm | ||
# of :math:`H`. We have normalized such that the minimum of the monitor function is one (where | ||
# the error is zero), and its maximum is :math:`1 + \alpha` (where the curvature is maximal). This | ||
# means we can select the ratio between the largest area and smallest area triangle in the | ||
# moved mesh as :math:`1+\alpha`. | ||
# | ||
# In the following implementation we use the exact solution :math:`u_{\text{exact}}` which we | ||
# have as a symbolic UFL expression, and thus we can also obtain the Hessian symbolically as | ||
# :code:`grad(grad(u_exact))`. To compute its maximum norm however we do interpolate it | ||
# to a P1 function space `V` and take the maximum of the array of nodal values. | ||
|
||
alpha = Constant(5.0) | ||
|
||
|
||
def monitor(mesh): | ||
V = FunctionSpace(mesh, "CG", 1) | ||
Hnorm = Function(V, name="Hnorm") | ||
H = grad(grad(u_exact(mesh))) | ||
Hnorm.interpolate(sqrt(inner(H, H))) | ||
Hnorm_max = Hnorm.dat.data.max() | ||
m = 1 + alpha * Hnorm / Hnorm_max | ||
return m | ||
|
||
|
||
fig, axes = plt.subplots() | ||
m = Function(u_h, name="monitor") | ||
m.interpolate(monitor(mesh)) | ||
contours = tripcolor(m, axes=axes) | ||
fig.colorbar(contours) | ||
plt.savefig("monge_ampere_helmholtz-monitor.jpg") | ||
fig, axes = plt.subplots() | ||
|
||
# .. figure:: monge_ampere_helmholtz-monitor.jpg | ||
# :figwidth: 60% | ||
# :align: center | ||
# | ||
# As in the `previous Monge-Ampère demo <./monge_ampere1.py.html>`__, we use the | ||
# MongeAmpereMover to perform the mesh movement based on this monitor. We need | ||
# to provide the monitor as a callback function that takes the mesh as its | ||
# input just as we have defined it above. During the iterations of the mesh | ||
# movement process the monitor will then be re-evaluated in the (iteratively) | ||
# moved mesh nodes so that, as we improve the mesh, we can also more accurately | ||
# express the monitor function in the desired high-resolution areas. | ||
|
||
mover = MongeAmpereMover(mesh, monitor, method="quasi_newton") | ||
mover.move() | ||
|
||
# For every iteration the MongeAmpereMover prints the minimum to maximum ratio of | ||
# the cell areas in the mesh, the residual in the Monge Ampere equation, and the | ||
# coefficient of variation of the cell areas: | ||
# | ||
# .. code-block:: none | ||
# | ||
# 0 Min/Max 2.0268e-01 Residual 4.7659e-01 Variation (σ/μ) 9.9384e-01 | ||
# 1 Min/Max 3.7852e-01 Residual 2.4133e-01 Variation (σ/μ) 9.9659e-01 | ||
# 2 Min/Max 5.9791e-01 Residual 1.2442e-01 Variation (σ/μ) 9.9774e-01 | ||
# 3 Min/Max 7.1000e-01 Residual 6.5811e-02 Variation (σ/μ) 9.9804e-01 | ||
# 4 Min/Max 7.7704e-01 Residual 3.4929e-02 Variation (σ/μ) 9.9818e-01 | ||
# 5 Min/Max 8.3434e-01 Residual 1.7261e-02 Variation (σ/μ) 9.9829e-01 | ||
# 6 Min/Max 8.5805e-01 Residual 7.7528e-03 Variation (σ/μ) 9.9833e-01 | ||
# 7 Min/Max 8.6653e-01 Residual 3.1551e-03 Variation (σ/μ) 9.9835e-01 | ||
# 8 Min/Max 8.6796e-01 Residual 1.1644e-03 Variation (σ/μ) 9.9835e-01 | ||
# 9 Min/Max 8.6792e-01 Residual 3.8816e-04 Variation (σ/μ) 9.9835e-01 | ||
# 10 Min/Max 8.6784e-01 Residual 1.1574e-04 Variation (σ/μ) 9.9835e-01 | ||
# 11 Min/Max 8.6778e-01 Residual 1.5645e-05 Variation (σ/μ) 9.9835e-01 | ||
# 12 Min/Max 8.6776e-01 Residual 7.5654e-06 Variation (σ/μ) 9.9835e-01 | ||
# 13 Min/Max 8.6776e-01 Residual 3.5803e-06 Variation (σ/μ) 9.9835e-01 | ||
# 14 Min/Max 8.6775e-01 Residual 1.5113e-06 Variation (σ/μ) 9.9835e-01 | ||
# 15 Min/Max 8.6775e-01 Residual 5.7080e-07 Variation (σ/μ) 9.9835e-01 | ||
# 16 Min/Max 8.6775e-01 Residual 1.9357e-07 Variation (σ/μ) 9.9835e-01 | ||
# 17 Min/Max 8.6775e-01 Residual 5.8585e-08 Variation (σ/μ) 9.9835e-01 | ||
# Converged in 17 iterations. | ||
# | ||
# Plotting the resulting mesh | ||
|
||
from firedrake.pyplot import triplot | ||
|
||
fig, axes = plt.subplots() | ||
triplot(mover.mesh, axes=axes) | ||
axes.set_aspect(1) | ||
plt.savefig("monge_ampere_helmholtz-adapted_mesh.jpg") | ||
|
||
# .. figure:: monge_ampere_helmholtz-adapted_mesh.jpg | ||
# :figwidth: 60% | ||
# :align: center | ||
# | ||
# Now let us see whether the numerical error has actually been reduced | ||
# if we solve the PDE on this mesh: | ||
|
||
u_h = solve_helmholtz(mover.mesh) | ||
error = u_h - u_exact(mover.mesh) | ||
print("L2-norm error on moved mesh:", sqrt(assemble(dot(error, error) * dx))) | ||
stephankramer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# .. code-block:: none | ||
# | ||
# L2-norm error on moved mesh: 0.005955820168534556 | ||
# | ||
# Of course, in many practical problems we do not actually have access | ||
# to the exact solution. We can then use the Hessian of the numerical | ||
# solution in the monitor function. Calculating the Hessian we have to | ||
# be a bit careful however: since our numerical FEM solution :math:`u_h` | ||
# is a piecewise linear function, its first gradient results in a | ||
# piecewise *constant* function, a vector-valued constant | ||
# function in each cell. Taking its gradient in each cell would therefore | ||
# simply be zero. Instead, we should numerically approximate the derivatives | ||
# to "recover" the Hessian, for which there are a number of different methods. | ||
# | ||
# As Hessians are often used in metric-based h-adaptivity, some of these | ||
# methods have been implemented in the :py:mod:`animate` package. | ||
# An implementation of a monitor based on the Hessian of the numerical | ||
# solution is given below. Note that this requires solving the Helmholtz | ||
# PDE in every mesh movement iteration, and thus may become significantly | ||
# slower for large problems. | ||
|
||
from animate import RiemannianMetric | ||
|
||
|
||
def monitor2(mesh): | ||
u_h = solve_helmholtz(mesh) | ||
V = FunctionSpace(mesh, "CG", 1) | ||
TV = TensorFunctionSpace(mesh, "CG", 1) | ||
H = RiemannianMetric(TV) | ||
H.compute_hessian(u_h, method="L2") | ||
|
||
Hnorm = Function(V, name="Hnorm") | ||
Hnorm.interpolate(sqrt(inner(H, H))) | ||
Hnorm_max = Hnorm.dat.data.max() | ||
m = 1 + alpha * Hnorm / Hnorm_max | ||
return m | ||
|
||
|
||
mover = MongeAmpereMover(mesh, monitor2, method="quasi_newton") | ||
mover.move() | ||
|
||
u_h = solve_helmholtz(mover.mesh) | ||
error = u_h - u_exact(mover.mesh) | ||
print("L2-norm error on moved mesh:", sqrt(assemble(dot(error, error) * dx))) | ||
|
||
# .. code-block:: none | ||
# | ||
# L2-norm error on moved mesh: 0.00630874419681285 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about including what's printed to screen in a separate code block? i.e. showing the convergence of the different metrics. We do this in Thetis demos.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea. Out of interest why do we refer to the standard deviation of cell areas as "equidistribution" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm good point, not sure how that ended up in there. Standard deviation would be a better term. We can either fix it here or open an issue for it, whichever you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any objection to UTF -8 in terminal output? "Standard Deviation" is a bit long, and not entirely accurate - all other terminology (https://en.wikipedia.org/wiki/Coefficient_of_variation) is a bit obscure so I thought to clarify with σ/μ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine by me. Nice! Even better.