#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
#
# _____ _ _ _ _____ _____
# | ____|__| | ___| |_ _____(_)___ ___| ___| ____|
# | _| / _` |/ _ \ \ \ /\ / / _ \ / __/ __| |_ | _|
# | |__| (_| | __/ |\ V V / __/ \__ \__ \ _| | |___
# |_____\__,_|\___|_| \_/\_/ \___|_|___/___/_| |_____|
#
#
# Unit of Strength of Materials and Structural Analysis
# University of Innsbruck,
# 2017 - today
#
# Matthias Neuner matthias.neuner@uibk.ac.at
#
# This file is part of EdelweissFE.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# The full text of the license can be found in the file LICENSE.md at
# the top level directory of EdelweissFE.
# ---------------------------------------------------------------------
import numpy as np
import edelweissfe.numerics.dofvector
[docs]
class ScatterDofVectorTemplate:
"""
The precomputed, immutable layout of a :class:`ScatterDofVector`:
the entity lookup map and the gather/scatter index array.
Since the layout only depends on the entities and their indices in the DofVector,
it can be computed once and shared by all scatter vectors of the same equation
system, e.g. across Newton iterations.
Parameters
----------
entitiesInDofVector : dict
The dictionary mapping entities to their global index arrays in the `DofVector`.
nDof : int
The total number of global degrees of freedom.
"""
def __init__(self, entitiesInDofVector: dict, nDof: int):
sizes = np.array([len(v) for v in entitiesInDofVector.values()], dtype=np.intc)
total_size = int(np.sum(sizes))
offsets = np.zeros(len(entitiesInDofVector) + 1, dtype=np.intc)
np.cumsum(sizes, out=offsets[1:])
# map entity -> (offset, size), such that a single lookup suffices for access
self.offsetMap = {
entity: (offset, size) for entity, offset, size in zip(entitiesInDofVector.keys(), offsets, sizes)
}
self.globalIndices = np.empty(total_size, dtype=np.int32)
current_offset = 0
for entity, indices in entitiesInDofVector.items():
n = len(indices)
self.globalIndices[current_offset : current_offset + n] = indices
current_offset += n
self.entitiesInDofVector = entitiesInDofVector
self.nDof = nDof
self.totalSize = total_size
[docs]
class ScatterDofVector(np.ndarray):
"""
A Scatter Vector that stores data for entities contiguously.
Includes a fast lookup map to support random access by Entity.
Parameters
----------
template
The precomputed layout of the scatter vector.
"""
def __new__(cls, template: ScatterDofVectorTemplate):
obj = np.zeros(template.totalSize, dtype=float).view(cls)
obj._offset_map = template.offsetMap
obj._global_indices = template.globalIndices
obj._entitiesInDofVector = template.entitiesInDofVector
obj._nDof = template.nDof
#: Plain-ndarray alias of the same buffer; see __getitem__.
obj._plainView = None
return obj
def __array_finalize__(self, obj):
if obj is None:
return
self._offset_map = getattr(obj, "_offset_map", None)
self._entitiesInDofVector = getattr(obj, "_entitiesInDofVector", None)
self._nDof = getattr(obj, "_nDof", None)
self._global_indices = getattr(obj, "_global_indices", None)
self._plainView = None
@property
def offsetMap(self) -> dict:
"""The entity-to-``(offset, size)`` layout of this buffer.
For a caller that plans its own writes ahead of time -- the explicit element loop, which
assembles a whole chunk of elements into a buffer of its own and then places it here in
one go -- instead of taking a view per entity.
Returns
-------
dict
Mapping of each entity to its ``(offset, size)`` within this buffer.
"""
return self._offset_map
def __getitem__(self, key):
"""
Returns a view into the contiguous scatter buffer.
Parameters
----------
key : int, slice, np.ndarray, list, tuple, or entity
The key for indexing, either standard numpy index/slice or an entity key.
Returns
-------
ndarray or float
The slice view corresponding to the key.
"""
# Hot path: return a plain ndarray view (avoids __array_finalize__); it aliases the
# scatter buffer, so element kernels still write straight into it.
try:
offset, size = self._offset_map[key]
except (KeyError, TypeError):
return super().__getitem__(key)
plainView = self._plainView
if plainView is None:
plainView = self._plainView = self.view(np.ndarray)
return plainView[offset : offset + size]
def __setitem__(self, key, value):
"""
Assign values to a slice in the scatter buffer.
Parameters
----------
key : int, slice, np.ndarray, list, tuple, or entity
The key for indexing, either standard numpy index/slice or an entity key.
value : array_like or float
The value(s) to assign.
"""
if isinstance(key, (int, slice, np.ndarray, list, tuple)):
super().__setitem__(key, value)
return
try:
offset, size = self._offset_map[key]
super().__setitem__(slice(offset, offset + size), value)
except (KeyError, TypeError):
super().__setitem__(key, value)
[docs]
def copy(self, order: str = "C") -> "ScatterDofVector":
"""
Create a copy of this ScatterDofVector.
Parameters
----------
order : str, optional
The memory layout order ('C' for C-contiguous, 'F' for Fortran-contiguous). Default is 'C'.
Returns
-------
ScatterDofVector
A new `ScatterDofVector` instance with copied array data and metadata.
"""
new_vec = super().copy(order).view(ScatterDofVector)
if self._offset_map is not None:
new_vec._offset_map = self._offset_map.copy()
if self._entitiesInDofVector is not None:
new_vec._entitiesInDofVector = self._entitiesInDofVector.copy()
if self._global_indices is not None:
new_vec._global_indices = self._global_indices.copy()
new_vec._nDof = self._nDof
return new_vec
[docs]
def assembleInto(self, targetDofVector: np.ndarray, absolute: bool = False) -> None:
"""
Scatter-add values into a global DOF vector.
Parameters
----------
targetDofVector : ndarray
The target vector (e.g. `DofVector`) to assemble into.
absolute : bool, optional
If True, accumulate absolute values. Default is False.
"""
data = np.abs(self) if absolute else self
# bincount: duplicate-resolving scatter-add in a single pass
targetDofVector += np.bincount(self._global_indices, weights=data, minlength=self._nDof)
[docs]
def toDofVector(self, absolute: bool = False) -> "edelweissfe.numerics.dofvector.DofVector":
"""
Create a new global DofVector by scattering values from this scatter vector.
Parameters
----------
absolute : bool, optional
If True, use absolute values. Default is False.
Returns
-------
DofVector
A new `DofVector` containing the accumulated entries.
"""
new_dof_vector = edelweissfe.numerics.dofvector.DofVector(self._nDof, self._entitiesInDofVector)
self.assembleInto(new_dof_vector, absolute=absolute)
return new_dof_vector