Skip to content

Commit e8eb716

Browse files
authored
Merge pull request #4682 from firedrakeproject/pbrubeck/merge-release-into-main
Merge release into main
2 parents bb8447a + 53a370a commit e8eb716

File tree

20 files changed

+340
-89
lines changed

20 files changed

+340
-89
lines changed

.github/workflows/docker.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,17 @@ jobs:
7575
if: ${{ ! inputs.build_dev }}
7676
needs: docker_merge_vanilla
7777
uses: ./.github/workflows/docker_build.yml
78-
# Only build the 'firedrake' container for 'linux/amd64' because
79-
# netgen-mesher does not have ARM wheels so many Firedrake apps cannot
80-
# be installed.
78+
strategy:
79+
matrix:
80+
os: [Linux, macOS]
81+
include:
82+
- os: Linux
83+
platform: linux/amd64
84+
- os: macOS
85+
platform: linux/arm64
8186
with:
82-
os: Linux
83-
platform: linux/amd64
87+
os: ${{ matrix.os }}
88+
platform: ${{ matrix.platform }}
8489
target: firedrake
8590
tag: ${{ inputs.tag }}
8691
dockerfile: docker/Dockerfile.firedrake

AUTHORS.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Robert C. Kirby...............<https://www.baylor.edu/math/index.php?id=90540>
104104

105105
Stephan C. Kramer.............<https://www.imperial.ac.uk/people/s.kramer>
106106

107-
Tuomas Kärnä..................<https://www.tuomaskarna.com>
107+
Tuomas Kärnä
108108

109109
Michael Lange.................<https://www.linkedin.com/in/michael-lange-56675994/>
110110

demos/demo_references.bib

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,21 @@ @article{Krishna:1997
331331

332332
@article{BaierReinio:2025,
333333
title={High-order finite element methods for three-dimensional multicomponent convection-diffusion},
334-
author={Baier-Reinio, Aaron and Farrell, Patrick E},
334+
author={Baier-Reinio, Aaron and Farrell, Patrick E.},
335335
journal={arXiv preprint arXiv:2408.17390},
336-
year={2025}
336+
year={2025},
337+
doi={10.48550/arXiv.2408.17390}
338+
}
339+
340+
@article{VanBrunt:2025,
341+
title={Finite element methods for multicomponent convection-diffusion},
342+
author={Aznaran, Francis R. A. and Farrell, Patrick E. and Monroe, Charles W. and Van-Brunt, Alexander J.},
343+
journal={IMA J. Numer. Anal.},
344+
volume={45},
345+
number={1},
346+
pages={188--222},
347+
year={2025},
348+
doi={10.1093/imanum/drae001}
337349
}
338350

339351
@article{Brubeck2022,

demos/multicomponent/multicomponent.py.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ The domain and mesh are visualised below.
7878

7979
To model the mixture we employ the Stokes--Onsager--Stefan--Maxwell (SOSM)
8080
partial differential equations and discretise with the method of :cite:`BaierReinio:2025`.
81+
We use the material property values specified in :cite:`VanBrunt:2025`,
82+
wherein a similar benzene-cyclohexane simulation is considered.
8183
In what follows species 1 refers to benzene and species 2 to cyclohexane.
8284
We shall discretise the following unknowns:
8385

docker/Dockerfile.firedrake

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
1-
# Dockerfile for Firedrake with a full set of capabilities and applications installed.
1+
# Dockerfile for Firedrake with a full set of capabilities installed.
22

33
FROM firedrakeproject/firedrake-vanilla-default:latest
44

55
# Install optional dependencies
66
RUN pip install --verbose \
77
--extra-index-url https://download.pytorch.org/whl/cpu \
8-
jax ngsPETSc torch vtk
8+
jax torch
99

10-
# Set PYTHONPATH so netgen can be found
11-
# (see https://github.com/NGSolve/netgen/issues/213)
12-
ENV PYTHONPATH=/usr/local/lib/python3.12/site-packages:$PYTHONPATH
13-
14-
# Install Firedrake apps
15-
RUN pip install --verbose --src /opt \
16-
-e git+https://github.com/firedrakeproject/asQ.git#egg=asQ \
17-
-e git+https://bitbucket.org/pefarrell/defcon.git#egg=defcon \
18-
-e git+https://bitbucket.org/pefarrell/fascd.git#egg=fascd \
19-
-e git+https://github.com/FEMlium/FEMlium.git#egg=FEMlium \
20-
-e git+https://github.com/g-adopt/g-adopt.git#egg=gadopt \
21-
-e git+https://github.com/firedrakeproject/gusto.git#egg=gusto \
22-
-e git+https://github.com/firedrakeproject/Irksome.git#egg=Irksome \
23-
-e git+https://github.com/icepack/icepack.git#egg=icepack \
24-
-e git+https://github.com/thetisproject/thetis.git#egg=thetis
10+
# Install ngsPETSc and dependencies. Netgen do not package ARM wheels so
11+
# we have to build it from source in that case. If on x86 then we have to
12+
# set PYTHONPATH so netgen can be found (see https://github.com/NGSolve/netgen/issues/213).
13+
RUN \
14+
if [ "$(dpkg --print-architecture)" == "arm64" ]; then \
15+
apt-get update \
16+
&& apt-get -y install python3-tk libxmu-dev tk-dev tcl-dev cmake g++ libglu1-mesa-dev liblapacke-dev libocct-data-exchange-dev libocct-draw-dev occt-misc libtbb-dev libxi-dev \
17+
&& rm -rf /var/lib/apt/lists/* \
18+
&& mkdir /opt/ngsuite \
19+
&& git clone --branch=v6.2.2505 --single-branch https://github.com/NGSolve/netgen.git /opt/ngsuite/netgen-src \
20+
&& git -C /opt/ngsuite/netgen-src submodule update --init --recursive \
21+
&& mkdir /opt/ngsuite/netgen-build \
22+
&& mkdir /opt/ngsuite/netgen-install \
23+
&& cmake -DCMAKE="-cxx-flags=-flax-vector-conversions" -DCMAKE_INSTALL_PREFIX=/opt/ngsuite/netgen-install /opt/ngsuite/netgen-src \
24+
&& make \
25+
&& make install \
26+
&& export PATH=/opt/ngsuite/netgen-install/bin:$PATH \
27+
&& export PYTHONPATH=/opt/ngsuite/netgen-install/lib/python3.12/site-packages:$PYTHONPATH \
28+
&& pip install ngsPETSc --no-deps; \
29+
else \
30+
export PYTHONPATH=/usr/local/lib/python3.12/site-packages:$PYTHONPATH \
31+
&& pip install ngsPETSc; \
32+
fi
2533

2634
# Install some other niceties
2735
RUN apt-get update \

docs/source/conf.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,12 @@
164164
r'http://www.cs.virginia.edu/stream/',
165165
r'https://www.sciencedirect.com',
166166
r'https://.*\.baylor\.edu.*',
167-
r'https://www.tuomaskarna.com',
168167
r'https://www.crosscountrytrains.co.uk/',
169168
r'https://www.siam.org/',
170169
r'https://aims.ac.rw',
171170
r'https://mpecdt.ac.uk',
172171
r'https://www.hilton.com/en/hotels/leehnhn-hilton-leeds-city/',
173-
r'https://www.radissonhotels.com/en-us/hotels/park-plaza-leeds',
174-
r'https://www.radissonhotels.com/en-us/hotels/radisson-blu-leeds'
175-
r'https://www.radissonhotels.com/en-us/hotels/radisson-blu-leeds',
172+
r'https://www.radissonhotels.com/*',
176173
r'https://all.accor.com/hotel/*',
177174
]
178175
linkcheck_timeout = 30

docs/source/team.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Christian T. Jacobs:
9595
Darko Janeković:
9696
Nick Johnson:
9797
Anna Kalogirou:
98-
Tuomas Kärnä: https://www.tuomaskarna.com
98+
Tuomas Kärnä:
9999
Stephan C. Kramer: https://www.imperial.ac.uk/people/s.kramer
100100
Nicolas Loriant:
101101
Fabio Luporini:

docs/source/visualisation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,5 +332,5 @@ matplotlib.
332332
.. _matplotlib: http://matplotlib.org
333333
.. _Arbitrary: https://www.kitware.com/modeling-arbitrary-order-lagrange-finite-elements-in-the-visualization-toolkit/
334334
__ Arbitrary_
335-
.. _Tessellate: https://www.paraview.org/paraview-docs/nightly/python/paraview.simple.Tessellate.html
335+
.. _Tessellate: https://www.paraview.org/paraview-docs/nightly/python/paraview.simple.__init__.Tessellate.html
336336
.. _Tessellation: https://ieeexplore.ieee.org/document/1634311/

firedrake/assemble.py

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ def allocation_integral_types(self):
386386
else:
387387
return self._allocation_integral_types
388388

389+
@staticmethod
390+
def _as_pyop2_type(tensor, indices=None):
391+
if isinstance(tensor, (firedrake.Cofunction, firedrake.Function)):
392+
return OneFormAssembler._as_pyop2_type(tensor, indices=indices)
393+
elif isinstance(tensor, ufl.Matrix):
394+
return ExplicitMatrixAssembler._as_pyop2_type(tensor, indices=indices)
395+
else:
396+
assert indices is None
397+
return tensor
398+
389399
def assemble(self, tensor=None, current_state=None):
390400
"""Assemble the form.
391401
@@ -410,21 +420,22 @@ def assemble(self, tensor=None, current_state=None):
410420
"""
411421
def visitor(e, *operands):
412422
t = tensor if e is self._form else None
413-
return self.base_form_assembly_visitor(e, t, *operands)
423+
# Deal with 2-form bcs inside the visitor
424+
bcs = self._bcs if isinstance(e, ufl.BaseForm) and len(e.arguments()) == 2 else ()
425+
return self.base_form_assembly_visitor(e, t, bcs, *operands)
414426

415427
# DAG assembly: traverse the DAG in a post-order fashion and evaluate the node on the fly.
416428
visited = {}
417429
result = BaseFormAssembler.base_form_postorder_traversal(self._form, visitor, visited)
418430

419-
# Apply BCs after assembly
431+
# Deal with 1-form bcs outside the visitor
420432
rank = len(self._form.arguments())
421433
if rank == 1 and not isinstance(result, ufl.ZeroBaseForm):
422434
for bc in self._bcs:
423435
OneFormAssembler._apply_bc(self, result, bc, u=current_state)
424-
425436
return result
426437

427-
def base_form_assembly_visitor(self, expr, tensor, *args):
438+
def base_form_assembly_visitor(self, expr, tensor, bcs, *args):
428439
r"""Assemble a :class:`~ufl.classes.BaseForm` object given its assembled operands.
429440
430441
This functions contains the assembly handlers corresponding to the different nodes that
@@ -445,7 +456,7 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
445456
assembler = OneFormAssembler(form, form_compiler_parameters=self._form_compiler_params,
446457
zero_bc_nodes=self._zero_bc_nodes, diagonal=self._diagonal, weight=self._weight)
447458
elif rank == 2:
448-
assembler = TwoFormAssembler(form, bcs=self._bcs, form_compiler_parameters=self._form_compiler_params,
459+
assembler = TwoFormAssembler(form, bcs=bcs, form_compiler_parameters=self._form_compiler_params,
449460
mat_type=self._mat_type, sub_mat_type=self._sub_mat_type,
450461
options_prefix=self._options_prefix, appctx=self._appctx, weight=self._weight,
451462
allocation_integral_types=self.allocation_integral_types)
@@ -456,13 +467,12 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
456467
if len(args) != 1:
457468
raise TypeError("Not enough operands for Adjoint")
458469
mat, = args
459-
res = tensor.petscmat if tensor else PETSc.Mat()
460-
petsc_mat = mat.petscmat
470+
result = tensor.petscmat if tensor else PETSc.Mat()
461471
# Out-of-place Hermitian transpose
462-
petsc_mat.hermitianTranspose(out=res)
463-
(row, col) = mat.arguments()
464-
return matrix.AssembledMatrix((col, row), self._bcs, res,
465-
options_prefix=self._options_prefix)
472+
mat.petscmat.hermitianTranspose(out=result)
473+
if tensor is None:
474+
tensor = self.assembled_matrix(expr, bcs, result)
475+
return tensor
466476
elif isinstance(expr, ufl.Action):
467477
if len(args) != 2:
468478
raise TypeError("Not enough operands for Action")
@@ -480,7 +490,7 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
480490
result = tensor.petscmat if tensor else PETSc.Mat()
481491
lhs.petscmat.matMult(rhs.petscmat, result=result)
482492
if tensor is None:
483-
tensor = self.assembled_matrix(expr, result)
493+
tensor = self.assembled_matrix(expr, bcs, result)
484494
return tensor
485495
else:
486496
raise TypeError("Incompatible RHS for Action.")
@@ -499,9 +509,6 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
499509
raise TypeError("Mismatching weights and operands in FormSum")
500510
if len(args) == 0:
501511
raise TypeError("Empty FormSum")
502-
if tensor:
503-
tensor.zero()
504-
505512
# Assemble weights
506513
weights = []
507514
for w in expr.weights():
@@ -519,27 +526,54 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
519526
raise ValueError("Expecting a scalar weight expression")
520527
weights.append(w)
521528

529+
# Scalar FormSum
522530
if all(isinstance(op, numbers.Complex) for op in args):
523-
result = sum(weight * arg for weight, arg in zip(weights, args))
524-
return tensor.assign(result) if tensor else result
525-
elif (all(isinstance(op, firedrake.Cofunction) for op in args)
531+
result = numpy.dot(weights, args)
532+
return tensor.assign(result) if tensor else result.item()
533+
534+
# Accumulate coefficients in a dictionary for each unique Dat/Mat
535+
terms = defaultdict(PETSc.ScalarType)
536+
for arg, weight in zip(args, weights):
537+
t = self._as_pyop2_type(arg)
538+
terms[t] += weight
539+
540+
# Zero the output tensor, or rescale it if it appears in the sum
541+
tensor_scale = terms.pop(self._as_pyop2_type(tensor), 0)
542+
if tensor is None or tensor_scale == 1:
543+
pass
544+
elif tensor_scale == 0:
545+
tensor.zero()
546+
elif isinstance(tensor, (firedrake.Cofunction, firedrake.Function)):
547+
with tensor.dat.vec as v:
548+
v.scale(tensor_scale)
549+
elif isinstance(tensor, ufl.Matrix):
550+
tensor.petscmat.scale(tensor_scale)
551+
else:
552+
raise ValueError("Expecting tensor to be None, Function, Cofunction, or Matrix")
553+
554+
# Compute the linear combination
555+
if (all(isinstance(op, firedrake.Cofunction) for op in args)
526556
or all(isinstance(op, firedrake.Function) for op in args)):
557+
# Vector FormSum
527558
V, = set(a.function_space() for a in args)
528559
result = tensor if tensor else firedrake.Function(V)
529-
result.dat.maxpy(weights, [a.dat for a in args])
560+
weights = terms.values()
561+
dats = terms.keys()
562+
result.dat.maxpy(weights, dats)
530563
return result
531564
elif all(isinstance(op, ufl.Matrix) for op in args):
565+
# Matrix FormSum
532566
result = tensor.petscmat if tensor else PETSc.Mat()
533-
for (op, w) in zip(args, weights):
567+
for (op, w) in terms.items():
534568
if result:
535569
# If result is not void, then accumulate on it
536-
result.axpy(w, op.petscmat)
570+
result.axpy(w, op.handle)
537571
else:
538572
# If result is void, then allocate it with first term
539-
op.petscmat.copy(result=result)
573+
op.handle.copy(result=result)
540574
result.scale(w)
541575
if tensor is None:
542-
tensor = self.assembled_matrix(expr, result)
576+
tensor = self.assembled_matrix(expr, bcs, result)
543577
return tensor
544578
else:
545579
raise TypeError("Mismatching FormSum shapes")
@@ -571,9 +605,8 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
571605
# Occur in situations such as Interpolate composition
572606
operand = assembled_operand[0]
573607

574-
reconstruct_interp = expr._ufl_expr_reconstruct_
575608
if (v, operand) != expr.argument_slots():
576-
expr = reconstruct_interp(operand, v=v)
609+
expr = expr._ufl_expr_reconstruct_(operand, v=v)
577610

578611
rank = len(expr.arguments())
579612
if rank > 2:
@@ -586,7 +619,7 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
586619
default_missing_val = interp_data.pop('default_missing_val', None)
587620
if rank == 1 and isinstance(tensor, firedrake.Function):
588621
V = tensor
589-
interpolator = firedrake.Interpolator(expr, V, **interp_data)
622+
interpolator = firedrake.Interpolator(expr, V, bcs=bcs, **interp_data)
590623
# Assembly
591624
return interpolator.assemble(tensor=tensor, default_missing_val=default_missing_val)
592625
elif tensor and isinstance(expr, (firedrake.Function, firedrake.Cofunction, firedrake.MatrixBase)):
@@ -598,8 +631,8 @@ def base_form_assembly_visitor(self, expr, tensor, *args):
598631
else:
599632
raise TypeError(f"Unrecognised BaseForm instance: {expr}")
600633

601-
def assembled_matrix(self, expr, petscmat):
602-
return matrix.AssembledMatrix(expr.arguments(), self._bcs, petscmat,
634+
def assembled_matrix(self, expr, bcs, petscmat):
635+
return matrix.AssembledMatrix(expr.arguments(), bcs, petscmat,
603636
options_prefix=self._options_prefix)
604637

605638
@staticmethod
@@ -1448,10 +1481,11 @@ def _apply_bc(self, tensor, bc, u=None):
14481481
index = 0 if V.index is None else V.index
14491482
space = V if V.parent is None else V.parent
14501483
if isinstance(bc, DirichletBC):
1451-
if space != spaces[0]:
1452-
raise TypeError("bc space does not match the test function space")
1453-
elif space != spaces[1]:
1454-
raise TypeError("bc space does not match the trial function space")
1484+
if not any(space == fs for fs in spaces):
1485+
raise TypeError("bc space does not match the test or trial function space")
1486+
if spaces[0] != spaces[1]:
1487+
# Not on a diagonal block, we cannot set diagonal entries
1488+
return
14551489

14561490
# Set diagonal entries on bc nodes to 1 if the current
14571491
# block is on the matrix diagonal and its index matches the

firedrake/interpolation.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,12 @@ def __init__(self, expr, V, bcs=None, **kwargs):
17031703
super(MixedInterpolator, self).__init__(expr, V, bcs=bcs, **kwargs)
17041704
expr = self.ufl_interpolate
17051705
self.arguments = expr.arguments()
1706-
rank = len(self.arguments)
1706+
# Get the primal spaces
1707+
spaces = tuple(a.function_space().dual() if isinstance(a, Coargument) else a.function_space()
1708+
for a in self.arguments)
1709+
# TODO consider a stricter equality test for indexed MixedFunctionSpace
1710+
# See https://github.com/firedrakeproject/firedrake/issues/4668
1711+
space_equals = lambda V1, V2: V1 == V2 and V1.parent == V2.parent and V1.index == V2.index
17071712

17081713
# We need a Coargument in order to split the Interpolate
17091714
needs_action = len([a for a in self.arguments if isinstance(a, Coargument)]) == 0
@@ -1722,12 +1727,10 @@ def __init__(self, expr, V, bcs=None, **kwargs):
17221727
continue
17231728
vi, _ = form.argument_slots()
17241729
Vtarget = vi.function_space().dual()
1725-
if bcs and rank != 0:
1726-
args = form.arguments()
1727-
Vsource = args[1-vi.number()].function_space()
1728-
sub_bcs = [bc for bc in bcs if bc.function_space() in {Vsource, Vtarget}]
1729-
else:
1730-
sub_bcs = None
1730+
sub_bcs = []
1731+
for space, index in zip(spaces, indices):
1732+
subspace = space.sub(index)
1733+
sub_bcs.extend(bc for bc in bcs if space_equals(bc.function_space(), subspace))
17311734
if needs_action:
17321735
# Take the action of each sub-cofunction against each block
17331736
form = action(form, dual_split[indices[-1:]])

0 commit comments

Comments
 (0)