Skip to content

Commit 98cc140

Browse files
Refactored boundary conditions (#619)
This PR changes the way in which boundary conditions should be normally set. In particular, it deprecates the normal nested list approach, where conditions were set for each side of all axes in a more flexible, dictionary-based approach. In the new approach, conditions are directly specified for entire axes (`x`) or for one side (`x+`). Moreover, we refactored the `Boundaries` class, introducing a new base class and added the `BoundariesSetter` class, so ghost points can be set directly using a callback function. We also cleaned up some code and moved around some classes, so this might break old code!
1 parent 6ad5c75 commit 98cc140

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+873
-377
lines changed

docs/source/manual/advanced_usage.rst

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,16 @@ field to be `4` at the lower side and its derivative (in the outward direction)
1313

1414
.. code-block:: python
1515
16-
bc_lower = {"value": 4}
17-
bc_upper = {"derivative": 2}
18-
bc = [bc_lower, bc_upper]
16+
bc = {"x-": {"value": 4}, "x+": {"derivative": 2}}
1917
2018
grid = pde.UnitGrid([16])
2119
field = pde.ScalarField(grid)
2220
field.laplace(bc)
2321
2422
Here, the Laplace operator applied to the field in the last line will respect
25-
the boundary conditions.
26-
Note that it suffices to give one condition if both sides of the axis require the same
27-
condition.
28-
For instance, to enforce a value of `3` on both side, one could simply use
29-
:code:`bc = {'value': 3}`.
23+
the boundary conditions. Note that both sides of the axis can be specified together if
24+
their conditions are the same. For instance, to enforce a value of `3` on both side, one
25+
could simply use :code:`bc = {"x": {"value": 3}}`.
3026
Vectorial boundary conditions, e.g., to calculate the vector gradient or tensor
3127
divergence, can have vectorial values for the boundary condition.
3228
Generally, only the normal components at a boundary need to be specified if an operator
@@ -39,33 +35,42 @@ which may depend on the coordinates of all axes:
3935
.. code-block:: python
4036
4137
# two different conditions for lower and upper end of x-axis
42-
bc_x = [{"derivative": 0.1}, {"value": "sin(y / 2)"}]
38+
bc_left = {"derivative": 0.1}
39+
bc_right = {"value": "sin(y / 2)"}
4340
# the same condition on the lower and upper end of the y-axis
4441
bc_y = {"value": "sqrt(1 + cos(x))"}
4542
4643
grid = UnitGrid([32, 32])
4744
field = pde.ScalarField(grid)
48-
field.laplace(bc=[bc_x, bc_y])
45+
field.laplace(bc={"x-": bc_left, "x+": bc_right, "y": bc_y})
4946
5047
.. warning::
5148
To interpret arbitrary expressions, the package uses :func:`exec`. It
5249
should therefore not be used in a context where malicious input could occur.
5350

54-
Inhomogeneous values can also be specified by directly supplying an array, whose shape
51+
Heterogeneous values can also be specified by directly supplying an array, whose shape
5552
needs to be compatible with the boundary, i.e., it needs to have the same shape as the
5653
grid but with the dimension of the axis along which the boundary is specified removed.
5754

58-
There exist also special boundary conditions that impose a more complex value of the
59-
field (:code:`bc='value_expression'`) or its derivative
60-
(:code:`bc='derivative_expression'`). Beyond the spatial coordinates that are already
55+
There also exist special boundary conditions that impose a more complex value of the
56+
field (:code:`bc="value_expression"`) or its derivative
57+
(:code:`bc="derivative_expression"`). Beyond the spatial coordinates that are already
6158
supported for the constant conditions above, the expressions of these boundary
6259
conditions can depend on the time variable :code:`t`. Moreover, these boundary
63-
conditions also except python functions (with signature `adjacent_value, dx, *coords, t`),
60+
conditions also except python functions with signature `(adjacent_value, dx, *coords, t)`,
6461
thus greatly enlarging the flexibility with which boundary conditions can be expressed.
6562
Note that PDEs need to supply the current time `t` when setting the boundary conditions,
6663
e.g., when applying the differential operators. The pre-defined PDEs and the general
6764
class :class:`~pde.pdes.pde.PDE` already support time-dependent boundary conditions.
6865

66+
To specify the same boundary conditions for many sides, the wildcard specifier can be
67+
used: For example, :code:`bc = {"*": {"value": 1}, "y+": {"derivative": 0}}` specifies
68+
Dirichlet conditions for all axes, except the upper y-axis, where a Neumann condition is
69+
imposed instead. If all axes have the same condition, the outer dictionary can be
70+
skipped, so that :code:`bc = {"*": {"value": 1}}` imposes the same conditions as
71+
:code:`bc = {"value": 1}`. Moreover, many boundaries have convenient names, so that for
72+
instance `x-` can be replaced by `left`, and `y+` can be replaced by `top`.
73+
6974
One important aspect about boundary conditions is that they need to respect the
7075
periodicity of the underlying grid. For instance, in a 2d grid with one periodic axis,
7176
the following boundary condition can be used:
@@ -74,8 +79,7 @@ the following boundary condition can be used:
7479
7580
grid = pde.UnitGrid([16, 16], periodic=[True, False])
7681
field = pde.ScalarField(grid)
77-
bc = ["periodic", {"derivative": 0}]
78-
field.laplace(bc)
82+
field.laplace({"x": "periodic", "y": {"derivative": 0})
7983
8084
For convenience, this typical situation can be described with the special boundary
8185
condition `auto_periodic_neumann`, e.g., calling the Laplace operator using
@@ -146,6 +150,13 @@ Here, :math:`\partial_n` denotes a derivative in outward normal direction, :math
146150
denotes an arbitrary function given by an expression (see next section), :math:`x`
147151
denotes coordinates along the boundary, :math:`t` denotes time.
148152
153+
Finally, we support the advanced technique of setting the virtual points at the boundary
154+
manually. This can be achieved by passing a python function that takes as
155+
its first argument a :class:`~numpy.ndarray`, which contains the full field data
156+
including the virtual points, and a second, optional argument, which is a dictionary
157+
containing additional parameters, like the current time point `t` in case of a
158+
simulation; see :class:`~pde.grids.boundaries.axes.BoundariesSetter` for more details.
159+
149160
150161
.. _documentation-expressions:
151162

docs/source/manual/contributing.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ New operators can be associated with grids by registering them using
3232
:meth:`~pde.grids.base.GridBase.register_operator`.
3333
For instance, to create a new operator for the cylindrical grid one needs to
3434
define a factory function that creates the operator. This factory function takes
35-
an instance of :class:`~pde.grids.boundaries.axes.Boundaries` as an argument and
35+
an instance of :class:`~pde.grids.boundaries.axes.BoundariesList` as an argument and
3636
returns a function that takes as an argument the actual data array for the grid.
3737
Note that the grid itself is an attribute of
38-
:class:`~pde.grids.boundaries.axes.Boundaries`.
38+
:class:`~pde.grids.boundaries.axes.BoundariesList`.
3939
This operator would be registered with the grid by calling
4040
:code:`CylindricalSymGrid.register_operator("operator", make_operator)`, where the
4141
first argument is the name of the operator and the second argument is the

examples/boundary_conditions.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@
1111
state = ScalarField.random_uniform(grid, 0.2, 0.3) # generate initial condition
1212

1313
# set boundary conditions `bc` for all axes
14-
bc_x_left = {"derivative": 0.1}
15-
bc_x_right = {"value": "sin(y / 2)"}
16-
bc_x = [bc_x_left, bc_x_right]
17-
bc_y = "periodic"
18-
eq = DiffusionPDE(bc=[bc_x, bc_y])
14+
eq = DiffusionPDE(
15+
bc={"x-": {"derivative": 0.1}, "x+": {"value": "sin(y / 2)"}, "y": "periodic"}
16+
)
1917

2018
result = eq.solve(state, t_range=10, dt=0.005)
2119
result.plot()

examples/heterogeneous_bcs.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ def bc_value(adjacent_value, dx, x, y, t):
3232
return np.sign(x)
3333

3434

35-
bc_x = "derivative"
36-
bc_y = ["derivative", {"value_expression": bc_value}]
37-
3835
# define and solve a simple diffusion equation
39-
eq = DiffusionPDE(bc=[bc_x, bc_y])
36+
eq = DiffusionPDE(bc={"*": {"derivative": 0}, "y+": {"value_expression": bc_value}})
4037
res = eq.solve(field, t_range=10, dt=0.01, backend="numpy")
4138
res.plot()

examples/jupyter/Discretized Fields.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"source": [
6565
"# apply operators to the field\n",
6666
"smoothed = scalar.smooth(1)\n",
67-
"laplace = smoothed.laplace(bc=\"natural\")\n",
67+
"laplace = smoothed.laplace(bc=\"auto_periodic_neumann\")\n",
6868
"laplace.plot(colorbar=True);"
6969
]
7070
},

examples/jupyter/Solve PDEs.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
" Taken from https://en.wikipedia.org/wiki/Chafee-Infante_equation\n",
8787
" \"\"\"\n",
8888
"\n",
89-
" def __init__(self, lmbd=1, bc=\"natural\"):\n",
89+
" def __init__(self, lmbd=1, bc=\"auto_periodic_neumann\"):\n",
9090
" super().__init__()\n",
9191
" self.bc = bc\n",
9292
" self.lmbd = lmbd\n",

examples/laplace_eq_2d.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
from pde import CartesianGrid, solve_laplace_equation
1212

13-
grid = CartesianGrid([[0, 2 * np.pi]] * 2, 64)
14-
bcs = [{"value": "sin(y)"}, {"value": "sin(x)"}]
13+
grid = CartesianGrid([[0, 2 * np.pi], [0, 2 * np.pi]], 64)
14+
bcs = {"x": {"value": "sin(y)"}, "y": {"value": "sin(x)"}}
1515

1616
res = solve_laplace_equation(grid, bcs)
1717
res.plot()

examples/poisson_eq_1d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99

1010
grid = CartesianGrid([[0, 1]], 32, periodic=False)
1111
field = ScalarField(grid, 1)
12-
result = solve_poisson_equation(field, bc=[{"value": 0}, {"derivative": 1}])
12+
result = solve_poisson_equation(field, bc={"x-": {"value": 0}, "x+": {"derivative": 1}})
1313

1414
result.plot()

examples/tutorial/Tutorial 1 - Grids and fields.ipynb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@
321321
"cell_type": "markdown",
322322
"metadata": {},
323323
"source": [
324-
"Periodic and non-periodic axes can also be mixed as in the example below. In this case, the $x$-axis is periodic and thus requires periodic boundary conditions. Conversely, the $y$-axis is non-periodic and any other boundary condition can be specified. The most generic one is a Neumann condition of vanishing derivative. For convenience, we also define `natural` boundary conditions, which indicate periodic conditions for periodic axes and Neumann conditions otherwise."
324+
"Periodic and non-periodic axes can also be mixed as in the example below. In this case, the $x$-axis is periodic and thus requires periodic boundary conditions. Conversely, the $y$-axis is non-periodic and any other boundary condition can be specified. The most generic one is a Neumann condition of vanishing derivative. For convenience, we also define `auto_periodic_neumann` boundary conditions, which indicate periodic conditions for periodic axes and Neumann conditions otherwise."
325325
]
326326
},
327327
{
@@ -336,7 +336,7 @@
336336
"field_mixed = pde.ScalarField.from_expression(grid_mixed, \"sin(x) * cos(y)\")\n",
337337
"\n",
338338
"laplace_mixed = field_mixed.laplace([\"periodic\", {\"derivative\": 0}])\n",
339-
"# laplace_mixed = field_mixed.laplace('natural')\n",
339+
"# laplace_mixed = field_mixed.laplace('auto_periodic_neumann')\n",
340340
"laplace_mixed.plot(title=\"Laplacian of mixed field\", colorbar=True);"
341341
]
342342
},
@@ -373,7 +373,7 @@
373373
"outputs": [],
374374
"source": [
375375
"field_per = pde.ScalarField.from_expression(grid_per, \"sin(x) * cos(y)\")\n",
376-
"field_grad = field_per.gradient(\"natural\")\n",
376+
"field_grad = field_per.gradient(\"auto_periodic_neumann\")\n",
377377
"field_grad"
378378
]
379379
},
@@ -463,7 +463,7 @@
463463
"metadata": {},
464464
"outputs": [],
465465
"source": [
466-
"field_hess = field_grad.gradient(\"natural\", label=\"Hessian of field\")\n",
466+
"field_hess = field_grad.gradient(\"auto_periodic_neumann\", label=\"Hessian of field\")\n",
467467
"field_hess.attributes"
468468
]
469469
},

examples/tutorial/Tutorial 2 - Solving pre-defined partial differential equations.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,9 @@
375375
"\n",
376376
" def evolution_rate(self, state, t=0):\n",
377377
" \"\"\"implement the python version of the evolution equation\"\"\"\n",
378-
" state_lap = state.laplace(bc=\"natural\")\n",
379-
" state_lap2 = state_lap.laplace(bc=\"natural\")\n",
380-
" state_grad = state.gradient(bc=\"natural\")\n",
378+
" state_lap = state.laplace(bc=\"auto_periodic_neumann\")\n",
379+
" state_lap2 = state_lap.laplace(bc=\"auto_periodic_neumann\")\n",
380+
" state_grad = state.gradient(bc=\"auto_periodic_neumann\")\n",
381381
" return -state_grad.to_scalar(\"squared_sum\") / 2 - state_lap - state_lap2\n",
382382
"\n",
383383
"\n",

0 commit comments

Comments
 (0)