Skip to content

Commit 320519d

Browse files
authored
Improve demos by: (#4000)
1. Add unique feature to `demo_pml.py`: 1-sided interior facet integrals with manual specfication of integration entities. 2. Add proper referencing to scipy functions. 3. Fix typos and doc rendering
1 parent 8fcd034 commit 320519d

3 files changed

Lines changed: 95 additions & 35 deletions

File tree

python/demo/demo_pml.py

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# # Electromagnetic scattering from a wire with PML
22
#
3-
# Copyright (C) 2022 Michele Castriotta, Igor Baratta, Jørgen S. Dokken
3+
# Copyright (C) 2022-2025 Michele Castriotta, Igor Baratta
4+
# and Jørgen S. Dokken
45
#
56
# ```{admonition} Download sources
67
# :class: download
@@ -12,6 +13,7 @@
1213
# - Use complex quantities in FEniCSx
1314
# - Setup and solve Maxwell's equations
1415
# - Implement (rectangular) perfectly matched layers (PMLs)
16+
# - Use custom integration entities for one-sided interior facet integrals
1517
#
1618
# First, we import the required modules
1719

@@ -549,7 +551,7 @@ def pml_coordinates(x: ufl.indexed.Indexed, alpha: float, k0: complex, l_dom: fl
549551
#
550552
# with $A^{-1}=\operatorname{det}(\mathbf{J})$.
551553
#
552-
# We use `ufl.grad` to calculate the Jacobian of our coordinate
554+
# We use {py:func}`ufl.grad` to calculate the Jacobian of our coordinate
553555
# transformation for the different PML regions, and then we can
554556
# implement this Jacobian for calculating
555557
# $\boldsymbol{\varepsilon}_{pml}$ and $\boldsymbol{\mu}_{pml}$. The
@@ -710,9 +712,73 @@ def create_eps_mu(
710712
)
711713

712714
# We calculate the numerical efficiencies in the same way as done in
713-
# `demo_scattering_boundary_conditions.py`, with the only difference
714-
# that now the scattering efficiency needs to be calculated over an
715-
# inner facet, and therefore it requires a slightly different approach:
715+
# [Electromagnetic scattering demo](./demo_scattering_boundary_conditions),
716+
# with the only difference that now the scattering efficiency needs to be
717+
# calculated over an inner facet, and therefore it requires a slightly
718+
# different approach:
719+
720+
# ### One-sided interior facet integrals
721+
722+
# An integration entity of an integral over a single facet in DOLFINx is
723+
# defined as a tuple, `(cell_idx, local_facet_idx)`, where `cell_idx` is
724+
# the index of a cell containing the facet, (local to process),
725+
# while `local_facet_idx` is the relative index of the facet in the cell.
726+
# For exterior facets, this is straightforward to obtain,
727+
# as a facet is then connected to only one cell.
728+
# However, for an interior facet a facet is connected to at lest two cells.
729+
# As we would like to compute the outwards facing flux through the wire,
730+
# we want to be able to integrate only from the side of the cell that has
731+
# a normal pointing outwards.
732+
# We start by identifying all cells that are interior to the scatter tag.
733+
734+
cell_map = mesh_data.mesh.topology.index_map(tdim)
735+
num_cells_local = cell_map.size_local + cell_map.num_ghosts
736+
midpoints = mesh.compute_midpoints(mesh_data.mesh, tdim, np.arange(num_cells_local, dtype=np.int32))
737+
is_inner_cell = (midpoints[:, 0] ** 2 + midpoints[:, 1] ** 2) < (radius_scatt) ** 2
738+
739+
# Next, we compute the integration entity for the facets in question.
740+
# We start by finding all facets owned by the current process (to assure
741+
# that we only integrate over each facet once), and then for each facet,
742+
# we find the connected cells.
743+
744+
# +
745+
# Get connectivity between facets and cells
746+
mesh_data.mesh.topology.create_connectivity(tdim - 1, tdim)
747+
mesh_data.mesh.topology.create_connectivity(tdim, tdim - 1)
748+
f_to_c = mesh_data.mesh.topology.connectivity(tdim - 1, tdim)
749+
c_to_f = mesh_data.mesh.topology.connectivity(tdim, tdim - 1)
750+
751+
# Filter facets to only those owned by the current process
752+
num_facets_local = mesh_data.mesh.topology.index_map(tdim - 1).size_local
753+
scatt_facets = mesh_data.facet_tags.find(scatt_tag)
754+
scatt_facets = scatt_facets[scatt_facets < num_facets_local]
755+
756+
# Pack integration data
757+
integration_entities = np.empty((len(scatt_facets), 2), dtype=np.int32)
758+
for i, facet in enumerate(scatt_facets):
759+
connected_cells = f_to_c.links(facet)
760+
inner_cell_idx = np.flatnonzero(is_inner_cell[connected_cells])
761+
assert len(inner_cell_idx) == 1, "Expected exactly one inner cell per facet."
762+
inner_cell = connected_cells[inner_cell_idx[0]]
763+
local_facets = c_to_f.links(inner_cell)
764+
local_facet_idx = np.flatnonzero(local_facets == facet)[0]
765+
integration_entities[i, :] = (inner_cell, local_facet_idx)
766+
# -
767+
768+
# Now that we have computed the integration entities, we define
769+
# an integration measure for one-sided facet integrals `ufl.ds`:
770+
771+
ds_scatter = ufl.ds(
772+
domain=mesh_data.mesh,
773+
subdomain_data=[(scatt_tag, integration_entities.flatten())],
774+
subdomain_id=scatt_tag,
775+
)
776+
777+
778+
scatt_facets = mesh_data.facet_tags.find(scatt_tag)
779+
incident_cells = mesh.compute_incident_entities(
780+
mesh_data.mesh.topology, scatt_facets, tdim - 1, tdim
781+
)
716782

717783
# +
718784
Z0 = np.sqrt(mu_0 / epsilon_0) # Vacuum impedance
@@ -727,22 +793,8 @@ def create_eps_mu(
727793
n = ufl.FacetNormal(mesh_data.mesh)
728794
n_3d = ufl.as_vector((n[0], n[1], 0))
729795

730-
# Create a marker for the integration boundary for the scattering
731-
# efficiency
732-
marker = fem.Function(D)
733-
scatt_facets = mesh_data.facet_tags.find(scatt_tag)
734-
incident_cells = mesh.compute_incident_entities(
735-
mesh_data.mesh.topology, scatt_facets, tdim - 1, tdim
736-
)
737-
738-
mesh_data.mesh.topology.create_connectivity(tdim, tdim)
739-
midpoints = mesh.compute_midpoints(mesh_data.mesh, tdim, incident_cells)
740-
inner_cells = incident_cells[(midpoints[:, 0] ** 2 + midpoints[:, 1] ** 2) < (radius_scatt) ** 2]
741-
742-
marker.x.array[inner_cells] = 1
743-
744796
# Quantities for the calculation of efficiencies
745-
P = 0.5 * ufl.inner(ufl.cross(Esh_3d, ufl.conj(Hsh_3d)), n_3d) * marker
797+
P = 0.5 * ufl.inner(ufl.cross(Esh_3d, ufl.conj(Hsh_3d)), n_3d)
746798
Q = 0.5 * eps_au.imag * k0 * (ufl.inner(E_3d, E_3d)) / (Z0 * n_bkg)
747799

748800
# Normalized absorption efficiency
@@ -752,10 +804,7 @@ def create_eps_mu(
752804
q_abs_fenics = mesh_data.mesh.comm.allreduce(q_abs_fenics_proc, op=MPI.SUM)
753805

754806
# Normalized scattering efficiency
755-
dS = ufl.Measure("dS", mesh_data.mesh, subdomain_data=mesh_data.facet_tags)
756-
q_sca_fenics_proc = (
757-
fem.assemble_scalar(fem.form((P("+") + P("-")) * dS(scatt_tag))) / (gcs * I0)
758-
).real
807+
q_sca_fenics_proc = (fem.assemble_scalar(fem.form(P * ds_scatter)) / (gcs * I0)).real
759808

760809
# Sum results from all MPI processes
761810
q_sca_fenics = mesh_data.mesh.comm.allreduce(q_sca_fenics_proc, op=MPI.SUM)

python/demo/demo_scattering_boundary_conditions.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
# # Electromagnetic scattering from a wire with scattering BCs
1717
#
18-
# Copyright (C) 2022 Michele Castriotta, Igor Baratta, Jørgen S. Dokken
18+
# Copyright (C) 2022-2025 Michele Castriotta, Igor Baratta
19+
# and Jørgen S. Dokken
1920
#
2021
# ```{admonition} Download sources
2122
# :class: download
@@ -194,12 +195,13 @@ def generate_mesh_wire(
194195
# $$
195196

196197

197-
# The functions that we import from `scipy.special` correspond to:
198+
# The functions that we import from {py:mod}`scipy.special` correspond to:
198199
#
199-
# - `jv(nu, x)` ⟷ $J_\nu(x)$,
200-
# - `jvp(nu, x, 1)` ⟷ $J_\nu^{\prime}(x)$,
201-
# - `hankel2(nu, x)` ⟷ $H_\nu^{(2)}(x)$,
202-
# - `h2vp(nu, x, 1)` ⟷ $H_\nu^{(2){\prime}}(x)$.
200+
# - {py:obj}`jv(nu, x)<scipy.special.jv>` ⟷ $J_\nu(x)$,
201+
# - {py:func}`jvp(nu, x, 1)<scipy.special.jvp>` ⟷ $J_\nu^{\prime}(x)$,
202+
# - {py:obj}`hankel2(nu, x)<scipy.special.hankel2>` ⟷ $H_\nu^{(2)}(x)$,
203+
# - {py:func}`h2vp(nu, x, 1)<scipy.special.h2vp>`
204+
# ⟷ $H_\nu^{(2){\prime}}(x)$.
203205
#
204206
# Next, we define a function for calculating the analytical efficiencies
205207
# in Python. The inputs of the function are:
@@ -212,6 +214,8 @@ def generate_mesh_wire(
212214
# We also define a nested function for the calculation of $a_l$. For the
213215
# final calculation of the efficiencies, the summation over the different
214216
# orders of the Bessel functions is truncated at $\nu=50$.
217+
218+
215219
# +
216220
def compute_a(nu: int, m: complex, alpha: float) -> float:
217221
J_nu_alpha = jv(nu, alpha)
@@ -691,8 +695,7 @@ def curl_2d(f: fem.Function):
691695
# scattering and extinction efficiencies, which are quantities that
692696
# define how much light is absorbed and scattered by the wire. First of
693697
# all, we calculate the analytical efficiencies with the
694-
# `calculate_analytical_efficiencies` function defined in a separate
695-
# file:
698+
# `calculate_analytical_efficiencies` function defined above.
696699

697700
# Calculation of analytical efficiencies
698701
q_abs_analyt, q_sca_analyt, q_ext_analyt = calculate_analytical_efficiencies(
@@ -707,10 +710,10 @@ def curl_2d(f: fem.Function):
707710
# \begin{align}
708711
# & Q_{abs} = \operatorname{Re}\left(\int_{\Omega_{m}} \frac{1}{2}
709712
# \frac{\operatorname{Im}(\varepsilon_m)k_0}{Z_0n_b}
710-
# \mathbf{E}\cdot\hat{\mathbf{E}}dx\right) \\
713+
# \mathbf{E}\cdot\hat{\mathbf{E}}~\mathrm{d}x\right) \\
711714
# & Q_{sca} = \operatorname{Re}\left(\int_{\partial\Omega} \frac{1}{2}
712715
# \left(\mathbf{E}_s\times\bar{\mathbf{H}}_s\right)
713-
# \cdot\mathbf{n}ds\right)\\ \\
716+
# \cdot\mathbf{n}~\mathrm{d}s\right)\\ \\
714717
# & Q_{ext} = Q_{abs} + Q_{sca},
715718
# \end{align}
716719
# $$

python/pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ docs = [
3535
"matplotlib",
3636
"myst_parser",
3737
"numba",
38+
"scipy",
3839
"packaging",
3940
"pyyaml",
4041
"sphinx",
@@ -44,7 +45,14 @@ docs = [
4445
lint = ["ruff>=0.2.0"]
4546
optional = ["numba", "pyamg"]
4647
petsc4py = ["petsc4py"]
47-
test = ["matplotlib", "networkx", "packaging", "pytest>=9.0.0", "scipy", "fenics-dolfinx[optional]"]
48+
test = [
49+
"matplotlib",
50+
"networkx",
51+
"packaging",
52+
"pytest>=9.0.0",
53+
"scipy",
54+
"fenics-dolfinx[optional]",
55+
]
4856
ci = [
4957
"mypy",
5058
"pytest-xdist",

0 commit comments

Comments
 (0)