IRKsome 2026.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
irksome/__init__.py ADDED
@@ -0,0 +1,122 @@
1
+ from .tableaux.ButcherTableaux import (
2
+ Alexander,
3
+ BackwardEuler,
4
+ GaussLegendre,
5
+ LobattoIIIA,
6
+ LobattoIIIC,
7
+ PareschiRusso,
8
+ QinZhang,
9
+ RadauIIA,
10
+ )
11
+ from .tableaux.multistep_tableaux import (
12
+ BDF,
13
+ AdamsBashforth,
14
+ AdamsMoulton,
15
+ )
16
+ from .tableaux.pep_explicit_rk import PEPRK
17
+ from .ufl.deriv import Dt, expand_time_derivatives, check_irksome_import_order
18
+ from .ufl.lag import lag
19
+
20
+ check_irksome_import_order()
21
+
22
+ from .tableaux.dirk_imex_tableaux import DIRK_IMEX
23
+ from .tableaux.ars_dirk_imex_tableaux import ARS_DIRK_IMEX
24
+ from .tableaux.sspk_tableau import SSPK_DIRK_IMEX, SSPButcherTableau
25
+ from .tableaux.multistep_tableaux import MultistepTableau
26
+
27
+ from .constant import MeshConstant
28
+ from .tableaux.wso_dirk_tableaux import WSODIRK
29
+ from .scheme import create_time_quadrature
30
+ from .scheme import ContinuousPetrovGalerkinScheme, DiscontinuousGalerkinScheme
31
+ from .scheme import GalerkinCollocationScheme, DiscontinuousGalerkinCollocationScheme
32
+ from .bcs import BoundsConstrainedDirichletBC
33
+ from .dirk_stepper import DIRKTimeStepper
34
+ from .imex import RadauIIAIMEXMethod, DIRKIMEXMethod
35
+ from .stage_derivative import getForm
36
+ from .nystrom_dirk_stepper import DIRKNystromTimeStepper, ExplicitNystromTimeStepper
37
+ from .nystrom_stepper import (
38
+ StageDerivativeNystromTimeStepper,
39
+ ClassicNystrom4Tableau,
40
+ )
41
+ from .stage_value import StageValueTimeStepper
42
+
43
+ from .multistep import MultistepTimeStepper
44
+
45
+
46
+ __all__ = [
47
+ "AdamsBashforth",
48
+ "AdamsMoulton",
49
+ "Alexander",
50
+ "ARS_DIRK_IMEX",
51
+ "BackwardEuler",
52
+ "BDF",
53
+ "create_time_quadrature",
54
+ "DIRK_IMEX",
55
+ "DiscontinuousGalerkinCollocationScheme",
56
+ "Dt",
57
+ "expand_time_derivatives",
58
+ "GalerkinCollocationScheme",
59
+ "GaussLegendre",
60
+ "lag",
61
+ "LobattoIIIA",
62
+ "LobattoIIIC",
63
+ "MeshConstant",
64
+ "MultistepTableau",
65
+ "PareschiRusso",
66
+ "PEPRK",
67
+ "QinZhang",
68
+ "RadauIIA",
69
+ "SSPButcherTableau",
70
+ "SSPK_DIRK_IMEX",
71
+ "WSODIRK",
72
+ "DIRKTimeStepper",
73
+ "BoundsConstrainedDirichletBC",
74
+ "getForm",
75
+ "RadauIIAIMEXMethod",
76
+ "DIRKIMEXMethod",
77
+ "DIRKNystromTimeStepper",
78
+ "ExplicitNystromTimeStepper",
79
+ "StageDerivativeNystromTimeStepper",
80
+ "ClassicNystrom4Tableau",
81
+ "StageValueTimeStepper",
82
+ "ContinuousPetrovGalerkinTimeStepper",
83
+ "DiscontinuousGalerkinTimeStepper",
84
+ "MultistepTimeStepper",
85
+ ]
86
+
87
+
88
+ try:
89
+ import importlib
90
+
91
+ importlib.import_module("firedrake")
92
+ from .labeling import TimeQuadratureLabel
93
+ from .discontinuous_galerkin_stepper import DiscontinuousGalerkinTimeStepper
94
+ from .galerkin_stepper import ContinuousPetrovGalerkinTimeStepper
95
+
96
+ from .pc import (
97
+ ClinesBase,
98
+ ClinesLD,
99
+ NystromAuxiliaryOperatorPC,
100
+ RanaBase,
101
+ RanaDU,
102
+ RanaLD,
103
+ IRKAuxiliaryOperatorPC,
104
+ )
105
+ from .stepper import TimeStepper
106
+
107
+ __all__ += [
108
+ "TimeQuadratureLabel",
109
+ "DiscontinuousGalerkinScheme",
110
+ "ContinuousPetrovGalerkinScheme",
111
+ "ClinesBase",
112
+ "ClinesLD",
113
+ "NystromAuxiliaryOperatorPC",
114
+ "RanaBase",
115
+ "RanaDU",
116
+ "RanaLD",
117
+ "IRKAuxiliaryOperatorPC",
118
+ "TimeStepper",
119
+ ]
120
+
121
+ except ModuleNotFoundError:
122
+ pass
irksome/backend.py ADDED
@@ -0,0 +1,129 @@
1
+ from typing import Protocol, Any, Sequence
2
+ import ufl
3
+ from importlib import import_module
4
+ import types
5
+
6
+
7
+ class Backend(Protocol):
8
+ def get_function_space(self, V: ufl.Coefficient) -> ufl.FunctionSpace:
9
+ """Get a function space from the backend"""
10
+
11
+ def extract_bcs(bcs: Any) -> tuple[Any]:
12
+ """Extract boundary conditions"""
13
+
14
+ class Function:
15
+ ...
16
+
17
+ class DirichletBC:
18
+ ...
19
+
20
+ def get_stages(self, V: ufl.FunctionSpace, num_stages: int) -> ufl.Coefficient:
21
+ """
22
+ Given a function space for a single time-step, get a duplicate of this space,
23
+ repeated `num_stages` times.
24
+
25
+ Args:
26
+ V: Space for single step
27
+ num_stages: Number of stages
28
+
29
+ Returns:
30
+ A coefficient in the new function space
31
+ """
32
+
33
+ class Constant:
34
+ """MeshLess constant class"""
35
+
36
+ class MeshConstant:
37
+ def __init__(self, msh: ufl.Mesh):
38
+ """Initialize a mesh constant over a domain"""
39
+
40
+ def Constant(self, val: float = 0.0):
41
+ """Generate a constant in the backend language with a specific value"""
42
+
43
+ def ConstantOrZero(
44
+ x: float | complex, MC: MeshConstant | None = None
45
+ ) -> ufl.core.expr.Expr:
46
+ """
47
+ Create a constant with backend class if MeshConstant is not supplied.
48
+ Create UFL zero if `x` is sufficiently small
49
+ """
50
+
51
+ def get_mesh_constant(MC: MeshConstant | None) -> ufl.core.expr.Expr:
52
+ """Get a backend class to construct a mesh constant from"""
53
+
54
+ def TestFunction(space: ufl.FunctionSpace, part: int | None = None) -> ufl.Argument:
55
+ """Return a test-function that can be used by forms in the backend."""
56
+
57
+ def TrialFunction(
58
+ space: ufl.FunctionSpace, part: int | None = None
59
+ ) -> ufl.Argument:
60
+ """Return a trial-function that can be used by forms in the backend."""
61
+
62
+ def create_variational_problem(
63
+ F: ufl.Form,
64
+ u: ufl.Coefficient,
65
+ bcs: DirichletBC | Sequence | None = None,
66
+ **kwargs,
67
+ ) -> Any:
68
+ """Create a variational problem in the backend language."""
69
+
70
+ def create_variational_solver(
71
+ problem: Any,
72
+ solver_parameters: dict | None = None,
73
+ **kwargs,
74
+ ):
75
+ """Create a variational solver in the backend language."""
76
+
77
+ def get_stage_spaces(V: ufl.FunctionSpace, num_stages: int) -> ufl.FunctionSpace:
78
+ """Create a stage space with M number of components."""
79
+
80
+ def norm(
81
+ v: ufl.core.expr.Expr, norm_type: str = "L2", mesh: ufl.Mesh | None = None
82
+ ) -> float:
83
+ """Compute the norm of a function in the backend language."""
84
+
85
+ def assemble(expr: ufl.core.expr.Expr) -> Any:
86
+ """Assemble a UFL expression in the backend language."""
87
+
88
+ def derivative(
89
+ form: ufl.Form,
90
+ u: ufl.Coefficient,
91
+ du: ufl.Argument | None = None,
92
+ coefficient_derivatives: dict | None = None,
93
+ ) -> ufl.Form:
94
+ """Compute the derivative of a form with respect to a coefficient in the backend language."""
95
+
96
+ def invalidate_jacobian(solver: Any):
97
+ """Invalidate the Jacobian matrix in the backend language."""
98
+
99
+ class EquationBCSplit:
100
+ ...
101
+
102
+ class EquationBC:
103
+ ...
104
+
105
+ def create_bounds_constrained_bc(V, g, sub_domain, bounds, solver_parameters=None) -> DirichletBC:
106
+ ...
107
+
108
+
109
+ def get_backend(backend: str | types.ModuleType) -> Backend:
110
+ """Get backend class from backend name.
111
+
112
+ Args:
113
+ backend: Name of the backend to get
114
+
115
+ Returns:
116
+ Backend class
117
+ """
118
+ if isinstance(backend, types.ModuleType):
119
+ return backend
120
+ if backend == "firedrake":
121
+ from .backends import firedrake as fd_backend
122
+
123
+ return fd_backend
124
+ elif backend == "dolfinx":
125
+ from .backends import dolfinx as dx_backend
126
+
127
+ return dx_backend
128
+ else:
129
+ return import_module(backend)
File without changes
@@ -0,0 +1,184 @@
1
+ """DOLFINx backend for Irksome"""
2
+
3
+ try:
4
+ from mpi4py import MPI
5
+ from petsc4py import PETSc
6
+ import basix.ufl
7
+ import dolfinx.fem.petsc
8
+ import ufl
9
+ import typing
10
+ import numpy as np
11
+
12
+ def get_stage_space(V: ufl.FunctionSpace, num_stages: int) -> ufl.FunctionSpace:
13
+ if num_stages == 1:
14
+ me = V.ufl_elemet()
15
+ else:
16
+ el = V.ufl_element()
17
+ if el.num_sub_elements > 0:
18
+ me = basix.ufl.mixed_element(
19
+ np.tile(el.sub_elements, num_stages).tolist()
20
+ )
21
+ else:
22
+ me = basix.ufl.blocked_element(el, shape=(num_stages,))
23
+ return dolfinx.fem.functionspace(V.mesh, me)
24
+
25
+ def extract_bcs(bcs: typing.Any) -> tuple[typing.Any]:
26
+ """Extract boundary conditions"""
27
+ return bcs
28
+
29
+ def create_variational_problem(F, u, bcs=None, aP=None, **kwargs):
30
+ """Create a variational problem."""
31
+ if len(F.arguments()) == 2:
32
+ a, L = ufl.system(F)
33
+ return dolfinx.fem.petsc.LinearProblem(
34
+ a,
35
+ L,
36
+ u,
37
+ bcs=bcs,
38
+ petsc_options_prefix="IrkSomeLinearSolver",
39
+ P=aP,
40
+ **kwargs,
41
+ )
42
+ else:
43
+ return dolfinx.fem.petsc.NonlinearProblem(
44
+ F,
45
+ u,
46
+ petsc_options_prefix="IrkSomeNonlinearSolver",
47
+ bcs=bcs,
48
+ petsc_options=kwargs.get("solver_parameters"),
49
+ )
50
+
51
+ def create_variational_solver(
52
+ problem: dolfinx.fem.petsc.LinearProblem | dolfinx.fem.petsc.NonlinearProblem,
53
+ **kwargs,
54
+ ):
55
+ """Create a variational solver that uses PETSc SNES or KSP."""
56
+ solver_parameters = kwargs.get("solver_parameters", {})
57
+ solver = problem.solver
58
+ solver_prefix = problem.solver.getOptionsPrefix()
59
+ opts = PETSc.Options(
60
+ )
61
+ opts.prefixPush(solver_prefix)
62
+ for k, v in solver_parameters.items():
63
+ opts.setValue(k, v)
64
+ solver.setFromOptions()
65
+ opts.prefixPop()
66
+ # For some strange reason delValue doesn't respect prefixes
67
+ for k, v in solver_parameters.items():
68
+ opts.delValue(f"{solver_prefix}{k}")
69
+ return problem
70
+
71
+ def get_function_space(u: ufl.Coefficient) -> ufl.FunctionSpace:
72
+ return u.ufl_function_space()
73
+
74
+ def get_stages(V: dolfinx.fem.FunctionSpace, num_stages: int) -> ufl.Coefficient:
75
+ """
76
+ Given a function space for a single time-step, get a duplicate of this space,
77
+ repeated `num_stages` times.
78
+
79
+ Args:
80
+ V: Space for single step
81
+ num_stages: Number of stages
82
+
83
+ Returns:
84
+ A coefficient in the new function space
85
+ """
86
+ if V.num_sub_spaces == 0:
87
+ el = basix.ufl.mixed_element([V.ufl_element()] * num_stages)
88
+ else:
89
+ el = basix.ufl.mixed_element(V.ufl_element().sub_elements * num_stages)
90
+ Vbig = dolfinx.fem.functionspace(V.mesh, el)
91
+ return dolfinx.fem.Function(Vbig)
92
+
93
+ class MeshConstant(object):
94
+ def __init__(self, msh):
95
+ self.msh = msh
96
+ try:
97
+ import basix.ufl
98
+
99
+ r_el = basix.ufl.real_element(
100
+ msh.basix_cell(), value_shape=(), dtype=dolfinx.default_scalar_type
101
+ )
102
+ self.V = dolfinx.fem.functionspace(msh, r_el)
103
+ except TypeError:
104
+ try:
105
+ import scifem
106
+ except ModuleNotFoundError:
107
+ raise RuntimeError(
108
+ "DOLFINx with real element support or Scifem is required to make mesh-constants"
109
+ )
110
+ self.V = scifem.create_real_functionspace(msh, ())
111
+
112
+ def Constant(self, val=0.0) -> ufl.Coefficient:
113
+ v = dolfinx.fem.Function(self.V)
114
+ v.value = val
115
+ return v
116
+
117
+ def get_mesh_constant(MC: MeshConstant | None) -> ufl.core.expr.Expr:
118
+ return MC.Constant if MC is not None else ufl.constantvalue.ComplexValue
119
+
120
+ class DirichletBC(dolfinx.fem.DirichletBC):
121
+ pass
122
+
123
+ def norm(
124
+ v: ufl.core.expr.Expr, norm_type: str = "L2", mesh: ufl.Mesh | None = None
125
+ ) -> float:
126
+ """Compute the norm of a function in the backend language."""
127
+ if mesh is not None:
128
+ dx = ufl.Mesure("dx", domain=mesh)
129
+ else:
130
+ dx = ufl.dx
131
+ p = 2
132
+ if norm_type.startswith("L"):
133
+ p = int(norm_type[1:])
134
+ if p < 1:
135
+ raise ValueError(f"Invalid norm type {norm_type}")
136
+ expr = ufl.inner(v, v) ** (p / 2)
137
+ form = dolfinx.fem.form(expr * dx)
138
+ else:
139
+ raise NotImplementedError(f"Norm type {norm_type} not implemented")
140
+ norm_loc = dolfinx.fem.assemble_scalar(form)
141
+ return form.mesh.comm.Allreduce(MPI.IN_PLACE, norm_loc, op=MPI.SUM) ** (1 / p)
142
+
143
+ def assemble(expr: ufl.core.expr.Expr | float):
144
+ """Assemble a UFL expression in the backend language."""
145
+ if isinstance(expr, float):
146
+ return float
147
+ else:
148
+ form = dolfinx.fem.form(expr)
149
+ if form.rank == 0:
150
+ return dolfinx.fem.assemble_scalar(form)
151
+ elif form.rank == 1:
152
+ return dolfinx.fem.assemble_vector(form)
153
+ elif form.rank == 2:
154
+ return dolfinx.fem.assemble_matrix(form)
155
+ else:
156
+ raise ValueError(f"Cannot assemble form of rank {form.rank}")
157
+
158
+ derivative = ufl.derivative
159
+ TrialFunction = ufl.TrialFunction
160
+ Function = dolfinx.fem.Function
161
+ TestFunction = ufl.TestFunction
162
+
163
+ class Constant(ufl.constantvalue.ScalarValue):
164
+ # NOTE: If dolfinx ever get's meshless constants we should change this
165
+ def assign(self, value):
166
+ self._value = value
167
+
168
+ class EquationBCSplit:
169
+ def __init__(self, *args, **kwargs):
170
+ raise NotImplementedError("DOLFINx does not support EquationBCSplit")
171
+
172
+ class EquationBC:
173
+ def __init__(self, *args, **kwargs):
174
+ raise NotImplementedError("DOLFINx does not support EquationBC")
175
+
176
+ def invalidate_jacobian(solver: dolfinx.fem.petsc.LinearProblem):
177
+ """Invalidate the Jacobian matrix in the backend language."""
178
+ raise RuntimeError("DOLFINx does not support Jacobian invalidation")
179
+
180
+ def create_bounds_constrained_bc(V, g, sub_domain, bounds, solver_parameters=None):
181
+ raise NotImplementedError("Bounds-constrained BCs are not implemented for DOLFINx")
182
+
183
+ except ModuleNotFoundError:
184
+ pass
@@ -0,0 +1,139 @@
1
+ """Firedrake backend for Irksome"""
2
+
3
+ import firedrake
4
+ import ufl
5
+ import typing
6
+ from functools import reduce
7
+ from operator import mul
8
+
9
+
10
+ assemble = firedrake.assemble
11
+ derivative = firedrake.derivative
12
+ norm = firedrake.norm
13
+ Function = firedrake.Function
14
+ TestFunction = firedrake.TestFunction
15
+ TrialFunction = firedrake.TrialFunction
16
+ DirichletBC = firedrake.DirichletBC
17
+ EquationBC = firedrake.bcs.EquationBC
18
+ EquationBCSplit = firedrake.bcs.EquationBCSplit
19
+ Constant = firedrake.Constant
20
+
21
+
22
+ def get_stage_space(V: ufl.FunctionSpace, num_stages: int) -> ufl.FunctionSpace:
23
+ # NOTE: Should use firedrake.MixedFunctionSpace, but there is a bug for vector spaces.
24
+ return reduce(mul, (V for _ in range(num_stages)))
25
+
26
+
27
+ def extract_bcs(bcs: typing.Any) -> tuple[typing.Any]:
28
+ """Return an iterable of boundary conditions on the residual form"""
29
+ return tuple(bc.extract_form("F") for bc in firedrake.solving._extract_bcs(bcs))
30
+
31
+
32
+ def get_function_space(u: ufl.Coefficient) -> firedrake.FunctionSpace:
33
+ return u.function_space()
34
+
35
+
36
+ def get_stages(V: firedrake.FunctionSpace, num_stages: int) -> firedrake.Function:
37
+ """
38
+ Given a function space for a single time-step, get a duplicate of this space,
39
+ repeated `num_stages` times.
40
+
41
+ Args:
42
+ V: Space for single step
43
+ num_stages: Number of stages
44
+
45
+ Returns:
46
+ A coefficient in the new function space
47
+ """
48
+ Vbig = get_stage_space(V, num_stages)
49
+ return firedrake.Function(Vbig)
50
+
51
+
52
+ class MeshConstant(object):
53
+ def __init__(self, msh: ufl.Mesh):
54
+ self.msh = ufl.domain.as_domain(msh)
55
+ self.V = firedrake.FunctionSpace(self.msh, "R", 0)
56
+
57
+ def Constant(self, val=0.0) -> ufl.Coefficient:
58
+ return firedrake.Function(self.V).assign(val)
59
+
60
+
61
+ def get_mesh_constant(MC: MeshConstant | None):
62
+ return MC.Constant if MC else firedrake.Constant
63
+
64
+
65
+ def create_variational_problem(F, u, bcs=None, J=None, Jp=None, **kwargs):
66
+ if len(F.arguments()) == 2:
67
+ a, L = ufl.system(F)
68
+ kwargs.pop("is_linear", None)
69
+ problem = firedrake.LinearVariationalProblem(a, L, u, bcs=bcs, aP=Jp, **kwargs)
70
+ else:
71
+ constant_jacobian = kwargs.pop("constant_jacobian", False)
72
+ problem = firedrake.NonlinearVariationalProblem(F, u, bcs=bcs, J=J, Jp=Jp, **kwargs)
73
+ if constant_jacobian:
74
+ problem._constant_jacobian = constant_jacobian
75
+ return problem
76
+
77
+
78
+ def create_variational_solver(problem, **kwargs):
79
+ if isinstance(problem, firedrake.LinearVariationalProblem):
80
+ return firedrake.LinearVariationalSolver(problem, **kwargs)
81
+ else:
82
+ return firedrake.NonlinearVariationalSolver(problem, **kwargs)
83
+
84
+
85
+ def invalidate_jacobian(solver):
86
+ return firedrake.LinearVariationalSolver.invalidate_jacobian(solver)
87
+
88
+
89
+ class BoundsConstrainedDirichletBC(firedrake.DirichletBC):
90
+ def __init__(
91
+ self,
92
+ V,
93
+ g,
94
+ sub_domain,
95
+ bounds,
96
+ solver_parameters=None,
97
+ ):
98
+ self.g = g
99
+ self.solver_parameters = solver_parameters
100
+ self.bounds = bounds
101
+ self.gnew = Function(V)
102
+
103
+ F = ufl.inner(self.gnew - g, TestFunction(V)) * ufl.dx
104
+
105
+ if solver_parameters is None:
106
+ solver_parameters = {
107
+ "snes_type": "vinewtonrsls",
108
+ "snes_max_it": 300,
109
+ "snes_atol": 1.0e-8,
110
+ "ksp_type": "preonly",
111
+ "mat_type": "aij",
112
+ }
113
+ problem = create_variational_problem(F, self.gnew)
114
+ self.solver = create_variational_solver(
115
+ problem, solver_parameters=solver_parameters
116
+ )
117
+ super().__init__(V, g, sub_domain)
118
+
119
+ @property
120
+ def function_arg(self):
121
+ """The value of this boundary condition."""
122
+ self.solver.solve(bounds=self.bounds)
123
+ return self.gnew
124
+
125
+ @function_arg.setter
126
+ def function_arg(self, g):
127
+ """Set the value of this boundary condition."""
128
+ self.solver.solve(bounds=self.bounds)
129
+ return self.gnew
130
+
131
+ def reconstruct(self, V=None, g=None, sub_domain=None):
132
+ V = V or self.function_space()
133
+ g = g or self.g
134
+ sub_domain = sub_domain or self.sub_domain
135
+ return type(self)(V, g, sub_domain, self.bounds, self.solver_parameters)
136
+
137
+
138
+ def create_bounds_constrained_bc(V, g, sub_domain, bounds, solver_parameters=None):
139
+ return BoundsConstrainedDirichletBC(V, g, sub_domain, bounds, solver_parameters=solver_parameters)