reduce_on_subface#

iskra.topology.reduce_on_subface(data: Tensor, faces: Tensor, n_subfaces: int, reduce: Literal['sum', 'prod', 'mean', 'amax', 'amin'], *, face_ndim: int = 2, data_ndim: int | None = None) → Tensor[SOURCE]#

Scatter-reduce data from faces onto constitutive subfaces.

This function distributes, i.e., scatters, data (scalar, vector, tensor, etc.) defined on faces to each face’s subfaces. A naive scatter would result in each subface receiving multiple competing values, one for each face that contains it. Therefore, each operation is actually a scatter-reduce (see reduce argument). So, e.g., a triangle face can scatter its value to its 3 edges. That value is then, e.g., averaged onto each edge with the values of the other triangles that share that same edge.

The faces tensor is a (batched) tensor of (nested) faces, where the number of face dimension equals face_ndim, and the number of batch dimensions is equal to batch_ndim = faces.ndim - face_ndim.

The function slices up the data tensor into three parts: [Bs, Fs, Ds], where Bs is the batch shape, Ds are the data payload dimensions; Fs represents the “domain” of the data. On a triangle mesh, Fs = [F] implies one data payload per triangle, whereas Fs = [F, 3] implies one data payload per triangle-corner or triangle-side.

Length of Ds is specified by data_ndim. Data dimensions are greedy when data_ndim is None: by default (and at minimum), we assume Fs = [F] and that everything to the right of Fs is the data payload. If data_ndim is specified, Fs can expand. E.g., if data.shape = [B, F, 3, 3] and Fs = [F], we have one data payload per face, whereas if we set data_ndim=1, then Fs = [F, 3] and we have one data payload per triangle-corner. See more concrete examples below.

Tip

Fs represents the size of the source domain where the data is defined, whereas faces tells us how that source domain connects to the target domain below it, and faces_ndim is simply there to disambiguate batch dimensions.

Caution

Note that Fs is not the same as face_ndim, face_ndim specifies how many dimensions corresponding to the mesh domain there are in faces, and therefore implicitly the number of batch dimensions as batch_ndim = faces.ndim - face_ndim. This is preferrable to specifying batch_ndim as an argument because it lets the function work both on batched and unbatched inputs.

Tip

This function is one of the three building blocks of iskra’s scatter-gather framework, together with iskra.topology.get_subfaces() which constructs the face hierarchy and iskra.topology.face_index(), which performs the gather operation.

This function moves data down the face hierarchy.

See Scatter-Gather Dataflow in iskra ✨ for an explanation of iskra’s tensor-based scatter-gather framework.

Parameters:
  • data (Tensor[DType, [Bs, Fs, Ds]]) – Data defined on mesh faces (Fs=[F]) or face-subfaces (Fs=[F, FS]).

  • faces (Tensor[Int64, [Bs, F] | [Bs, F, FS] | [Bs, F, FSs (Shape of (nested) subfaces in a face. E.g., a tetrahedron’s side-triangles’ corners (`[F, FSs] = [Tets, FSs]) has FSs=[4, 3].)`]]) – Face-subface indices, possibly nested.

  • n_subfaces (int) – Total number of subfaces in the mesh (e.g., total number of vertices or edges in a triangle mesh).

  • reduce (Literal["sum", "prod", "mean", "amax", "amin"]) – Reduction operation, see torch.scatter_reduce() for more details.

  • face_ndim (int) – The dimensionality of the face index. By default the function assumes it is working with (possibly batched) face-vertex relations, which are stored as 2D tensors. Hence face_ndim=2 by default. If we were, e.g., gathering from vertices using a nested tet-tri-vert index we would specify face_ndim=3 and for a 1D list of vertices face_ndim=1.

  • data_ndim (int | None) – Num. dimensions of the per-face (or per-face-subface) payload. See above for more details on default behavior.

Raises:
  • ValueError – Throws an exception if faces.ndim is smaller than face_ndim. Everything to the left of the face index is read as the batch, so, e.g., handing a 1D list of vertices to the default face_ndim=2 would ask for -1 batch dimensions. We have to specify face_ndim=1 instead.

  • ValueError – Throws an exception if the batch dimensions of faces and data disagree. It is face_ndim that decides where the batch stops and the face index starts, so this usually means face_ndim is wrong.

  • ValueError – Throws error if data_ndim is negative, or big enough to leave no face dimension in data.

Returns:
  • ​ (Tensor[DType, [Bs, S, Ds]]) – Data reduced onto the S subfaces,

  • ​​ (where :abbr:`S (Number of subfaces in the mesh. Usually S is documented together with some F as point of reference. E.g., if F is the number of triangles, S could be the number of edges or vertices in the mesh.)` equals the value of the argument n_subfaces.)

Example

data.shape

faces.shape

data_ndim

result.shape

Example: from → to

[B, F]

[B, F, 3]

0

[B, V]

face scalars → vertices

[B, F, 3]

[B, F, 3]

0

[B, V]

corner scalars → vertices

[B, F, 2]

[B, F, 3]

1 | None

[B, V, 2]

face 2-vectors → vertices

[B, F, 3, 2]

[B, F, 3]

1

[B, V, 2]

corner 2-vectors → vertices

[B, F, 3]

[B, F, 3]

1 | None

[B, V, 3]

face normals → vertices

[B, F, 3, 3]

[B, F, 3]

1

[B, V, 3]

corner normals → vertices

[B, F, 3, 3]

[B, F, 3]

2 | None

[B, V, 3, 3]

face covariances → vertices