#!/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.scatterdofvector
[docs]
class DofVector(np.ndarray):
"""
Represents a Degree-of-Freedom (DOF) vector with entity-aware indexing.
A `DofVector` is a 1D NumPy array subclass augmented with metadata mapping simulation
entities (such as elements, constraints, or node sets) to their respective DOF indices.
This enables direct indexing and assignment using entity objects as keys.
Parameters
----------
nDof : int
The total number of degrees of freedom (length of the vector).
entitiesInDofVector : dict, optional
A dictionary mapping entities (or entity keys) to their index arrays or slices
in the DofVector.
"""
def __new__(cls, nDof: int, entitiesInDofVector: dict | None = None):
obj = np.zeros(nDof, dtype=float).view(cls)
obj.entitiesInDofVector = entitiesInDofVector if entitiesInDofVector is not None else {}
obj._scatterTemplate = None
#: Plain-ndarray alias of the same buffer, built lazily on first entity access so
#: that the hot path never triggers numpy subclass wrapping. See __getitem__.
obj._plainView = None
return obj
def __array_finalize__(self, obj):
if obj is None:
return
self.entitiesInDofVector = getattr(obj, "entitiesInDofVector", None)
self._scatterTemplate = getattr(obj, "_scatterTemplate", None)
self._plainView = None
def __getitem__(self, key):
# Hot path (called per element per increment): try the entity dictionary first, skipping an isinstance()
# chain, and return a plain ndarray view, skipping numpy's subclass wrapping. Non-entity keys (ints,
# slices, arrays) fall through to normal ndarray indexing at the cost of one exception.
try:
indices = self.entitiesInDofVector[key]
except (KeyError, TypeError):
return super().__getitem__(key)
return self.asPlainArray()[indices]
[docs]
def asPlainArray(self) -> np.ndarray:
"""The same buffer seen as a plain :class:`numpy.ndarray`, built once and cached.
Indexing through this bypasses the subclass wrapping that dominates hot-path access -- see
:meth:`__getitem__`. It aliases this vector rather than copying it, so writes through it are
writes to this vector. Callers that index a DofVector repeatedly in a loop should take this
once outside the loop instead of calling ``view(np.ndarray)`` themselves.
Returns
-------
np.ndarray
A plain-ndarray view of this vector's buffer.
"""
plainView = self._plainView
if plainView is None:
plainView = self._plainView = self.view(np.ndarray)
return plainView
def __setitem__(self, key, value):
if isinstance(key, (int, slice, np.ndarray, list, tuple)):
super().__setitem__(key, value)
return
try:
super().__setitem__(self.entitiesInDofVector[key], value)
except (KeyError, TypeError):
super().__setitem__(key, value)
[docs]
def copy(self, order: str = "C") -> "DofVector":
"""
Create a copy of this DofVector.
Parameters
----------
order : str, optional
The memory layout order ('C' for C-contiguous, 'F' for Fortran-contiguous). Default is 'C'.
Returns
-------
DofVector
A new `DofVector` instance with copied array data and an independent copy of `entitiesInDofVector`.
"""
newDofVector = super().copy(order).view(DofVector)
if self.entitiesInDofVector is not None:
newDofVector.entitiesInDofVector = self.entitiesInDofVector.copy()
return newDofVector
[docs]
def createScatterVector(self) -> "edelweissfe.numerics.scatterdofvector.ScatterDofVector":
"""
Create a scatter vector for ALL entities registered in this DofVector.
The underlying layout (entity lookup map and scatter indices) is computed once
and cached, so repeated calls (e.g. once per Newton iteration) only allocate
the zero-initialized data buffer.
Returns
-------
ScatterDofVector
A `ScatterDofVector` initialized with this vector's entity mapping and total DOF count.
Raises
------
ValueError
If `entitiesInDofVector` is None or uninitialized.
"""
if self.entitiesInDofVector is None:
raise ValueError("Cannot create a ScatterDofVector: entitiesInDofVector is None.")
# The template is inherited across views/copies, but entitiesInDofVector may since have been replaced
# (e.g. by copy()): do not reuse a template built for a stale mapping.
if self._scatterTemplate is None or self._scatterTemplate.entitiesInDofVector is not self.entitiesInDofVector:
self._scatterTemplate = edelweissfe.numerics.scatterdofvector.ScatterDofVectorTemplate(
self.entitiesInDofVector, self.size
)
return edelweissfe.numerics.scatterdofvector.ScatterDofVector(self._scatterTemplate)