Coverage for pySDC/implementations/transfer_classes/TransferMesh.py: 82%
99 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
1import numpy as np
2import scipy.sparse as sp
4import pySDC.helpers.transfer_helper as th
5from pySDC.core.errors import TransferError
6from pySDC.core.space_transfer import SpaceTransfer
9def _unit_grid(nvars, periodic):
10 """
11 Grid of one spatial dimension, scaled to a domain of length one.
13 The interpolation weights depend only on ratios of distances, so building the grids on the unit
14 domain instead of on the problem's own ``dx`` gives the same operator for any domain length.
15 For periodic grids it is also what makes the operator correct at all: the helpers in
16 ``transfer_helper`` hardcode a period of one.
18 Args:
19 nvars (int): number of degrees of freedom in this dimension
20 periodic (bool): whether this dimension is periodic
22 Returns:
23 np.ndarray: the grid, within [0, 1)
24 """
25 return np.arange(nvars) / nvars if periodic else (np.arange(nvars) + 1) / (nvars + 1)
28def _transfer_matrices_1d(nvars_fine, nvars_coarse, params):
29 """
30 Interpolation and restriction matrix of one spatial dimension.
32 The ``equidist_nested`` shortcut of ``interpolation_matrix_1d`` and the restriction scale of 0.5
33 are only right for grids that are nested with coarsening factor two, that is ``nf = 2 nc`` for
34 periodic and ``nf = 2 nc + 1`` for Dirichlet grids. Any other pair of sizes takes the general
35 interpolation, and its restriction is the transposed interpolation normalised to reproduce
36 constants, which is what the factor 0.5 does on the nested grids.
38 Args:
39 nvars_fine (int): number of degrees of freedom on the fine level
40 nvars_coarse (int): number of degrees of freedom on the coarse level
41 params: the parameters of the transfer class
43 Returns:
44 tuple: the interpolation matrix (nf x nc) and the restriction matrix (nc x nf)
45 """
46 # if number of variables is the same on both levels, Rspace and Pspace are identity
47 if nvars_coarse == nvars_fine:
48 return sp.eye(nvars_fine), sp.eye(nvars_coarse)
50 nested = nvars_fine == 2 * nvars_coarse + (not params.periodic)
51 if not nested and min(params.iorder, params.rorder) == 0:
52 raise TransferError(
53 f'Order 0 (injection) needs grids nested with factor 2, got {nvars_fine} and {nvars_coarse} points'
54 )
56 fine_grid = _unit_grid(nvars_fine, params.periodic)
57 coarse_grid = _unit_grid(nvars_coarse, params.periodic)
59 def interpolation_matrix(k):
60 return th.interpolation_matrix_1d(
61 fine_grid,
62 coarse_grid,
63 k=k,
64 periodic=params.periodic,
65 equidist_nested=params.equidist_nested and nested,
66 )
68 # assemble restriction as transpose of interpolation
69 Pspace = interpolation_matrix(params.iorder)
70 RspaceT = Pspace.T if params.iorder == params.rorder else interpolation_matrix(params.rorder).T
72 if not nested:
73 return Pspace, sp.diags(1.0 / np.asarray(RspaceT.sum(axis=1)).ravel()) @ RspaceT
75 if params.rorder > 0:
76 restr_factor = 0.5
77 else:
78 restr_factor = 1.0
79 return Pspace, restr_factor * RspaceT
82class mesh_to_mesh(SpaceTransfer):
83 """
84 Space transfer between nd meshes by sparse interpolation matrices of even order, restricting by their transpose.
86 This implementation can restrict and prolong between nd meshes with dirichlet-0 or periodic boundaries
87 via matrix-vector products.
89 Attributes:
90 Rspace: spatial restriction matrix, dim. Nc x Nf
91 Pspace: spatial prolongation matrix, dim. Nf x Nc
92 """
94 def __init__(self, fine_prob, coarse_prob, params):
95 """
96 Initialization routine
98 Args:
99 fine_prob: fine problem
100 coarse_prob: coarse problem
101 params: parameters for the transfer operators
102 """
104 # invoke super initialization
105 super().__init__(fine_prob, coarse_prob, params)
107 if self.params.rorder % 2 != 0:
108 raise TransferError('Need even order for restriction')
110 if self.params.iorder % 2 != 0:
111 raise TransferError('Need even order for interpolation')
113 if type(self.fine_prob.nvars) is tuple:
114 if type(self.coarse_prob.nvars) is not tuple:
115 raise TransferError('nvars parameter of coarse problem needs to be a tuple')
116 if not len(self.fine_prob.nvars) == len(self.coarse_prob.nvars):
117 raise TransferError('nvars parameter of fine and coarse level needs to have the same length')
118 elif type(self.fine_prob.nvars) is int:
119 if type(self.coarse_prob.nvars) is not int:
120 raise TransferError('nvars parameter of coarse problem needs to be an int')
121 else:
122 raise TransferError("unknow type of nvars for transfer, got %s" % self.fine_prob.nvars)
124 # we have a 1d problem
125 if type(self.fine_prob.nvars) is int:
126 self.Pspace, self.Rspace = _transfer_matrices_1d(self.fine_prob.nvars, self.coarse_prob.nvars, self.params)
128 # we have an n-d problem
129 else:
130 Rspace = []
131 Pspace = []
132 for i in range(len(self.fine_prob.nvars)):
133 P, R = _transfer_matrices_1d(self.fine_prob.nvars[i], self.coarse_prob.nvars[i], self.params)
134 Pspace.append(P)
135 Rspace.append(R)
137 # kronecker 1-d operators for n-d
138 self.Pspace = Pspace[0]
139 for i in range(1, len(Pspace)):
140 self.Pspace = sp.kron(self.Pspace, Pspace[i], format='csc')
142 self.Rspace = Rspace[0]
143 for i in range(1, len(Rspace)):
144 self.Rspace = sp.kron(self.Rspace, Rspace[i], format='csc')
146 # Carry the operators at the precision of what they produce, rather than always at float64.
147 # A float64 operator applied to a reduced-precision vector upcasts, so the transfer would do
148 # its work in double and only round on the way into the destination, which is the one place
149 # a reduced-precision level would silently keep paying full freight. `promote_types` with
150 # float32 keeps it legal for SciPy, which has no half-precision sparse matrix.
151 self.Rspace = self.Rspace.astype(np.promote_types(self.coarse_prob.init[-1], np.float32))
152 self.Pspace = self.Pspace.astype(np.promote_types(self.fine_prob.init[-1], np.float32))
154 # Which side of the PCI bus this runs on is a property of the problem, not something the
155 # transfer is told -- the same way `TransferMesh_MPIFFT` decides it. The operators are
156 # assembled with SciPy either way, since that work is small, one-off and full of host-side
157 # index arithmetic; only the finished matrices move.
158 if 'cupy' in self.fine_prob.dtype_u.__name__.lower():
159 import cupyx.scipy.sparse as csp
161 self.Rspace = csp.csr_matrix(self.Rspace)
162 self.Pspace = csp.csr_matrix(self.Pspace)
164 def restrict(self, F):
165 """
166 Restriction implementation
167 Args:
168 F: the fine level data (easier to access than via the fine attribute)
169 """
170 G = type(F)(self.coarse_prob.init)
172 def _restrict(fine, coarse):
173 if hasattr(self.fine_prob, 'ncomp'):
174 for i in range(self.fine_prob.ncomp):
175 if fine.shape[-1] == self.fine_prob.ncomp:
176 tmpF = fine[..., i].flatten()
177 tmpG = self.Rspace.dot(tmpF)
178 coarse[..., i] = tmpG.reshape(self.coarse_prob.nvars)
179 elif fine.shape[0] == self.fine_prob.ncomp:
180 tmpF = fine[i, ...].flatten()
181 tmpG = self.Rspace.dot(tmpF)
182 coarse[i, ...] = tmpG.reshape(self.coarse_prob.nvars)
183 else:
184 raise TransferError('Don\'t know how to restrict for this problem with multiple components')
185 else:
186 tmpF = fine.flatten()
187 tmpG = self.Rspace.dot(tmpF)
188 coarse[:] = tmpG.reshape(self.coarse_prob.nvars)
190 if hasattr(type(F), 'components'):
191 for comp in F.components:
192 _restrict(getattr(F, comp), getattr(G, comp))
193 elif type(F).__name__ in ['mesh', 'cupy_mesh']:
194 _restrict(F, G)
195 else:
196 raise TransferError('Wrong data type for restriction, got %s' % type(F))
197 return G
199 def prolong(self, G):
200 """
201 Prolongation implementation
202 Args:
203 G: the coarse level data (easier to access than via the coarse attribute)
204 """
205 F = type(G)(self.fine_prob.init)
207 def _prolong(coarse, fine):
208 if hasattr(self.fine_prob, 'ncomp'):
209 for i in range(self.fine_prob.ncomp):
210 if coarse.shape[-1] == self.fine_prob.ncomp:
211 tmpG = coarse[..., i].flatten()
212 tmpF = self.Pspace.dot(tmpG)
213 fine[..., i] = tmpF.reshape(self.fine_prob.nvars)
214 elif coarse.shape[0] == self.fine_prob.ncomp:
215 tmpG = coarse[i, ...].flatten()
216 tmpF = self.Pspace.dot(tmpG)
217 fine[i, ...] = tmpF.reshape(self.fine_prob.nvars)
218 else:
219 raise TransferError('Don\'t know how to prolong for this problem with multiple components')
220 else:
221 tmpG = coarse.flatten()
222 tmpF = self.Pspace.dot(tmpG)
223 fine[:] = tmpF.reshape(self.fine_prob.nvars)
224 return fine
226 if hasattr(type(F), 'components'):
227 for comp in G.components:
228 _prolong(getattr(G, comp), getattr(F, comp))
229 elif type(G).__name__ in ['mesh', 'cupy_mesh']:
230 F[:] = _prolong(G, F)
231 else:
232 raise TransferError('Wrong data type for prolongation, got %s' % type(G))
233 return F