#!/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.
# ---------------------------------------------------------------------
# Created on Tue Dec 18 09:18:25 2018
# @author: Matthias Neuner
import numpy as np
from edelweissfe.points.node import Node
from edelweissfe.sets.elementset import ElementSet
from edelweissfe.sets.nodeset import NodeSet
class NodeFieldSubset:
pass
[docs]
class NodeField:
"""
This class represents a node field.
A node field associates every node with the entries of a field variable listed in :attr:`ENTRIES`:
the solution ``U``, its increment ``dU``, the nodal force ``P``, and the rates ``V`` and ``A``. Every
node field holds all of them, from its construction on; their meaning is defined once, in
:doc:`/documentation/nodefieldresults`.
Furthermore, for convencience, it allows to get fast access to values for individual nodes or node sets.
The purpose is to store field data in an efficient, contiguos manner rather than distributing it across
all individual nodes.
.. code-block:: console
Example:
NodeField 'Displacement'
values: {'U' : [[0,0,0], # Node (1)
[1,0,0], # Node (2)
[0,1,0]]} # Node (3)
Spatial representation:
(1) --------- (2)
| * *
| *+---------+ *+---------+
| | [0,0,0] | | [1,0,0] |
(3) +---------+ +---------+
*
* +---------+
| [0,1,0] |
+---------+
Parameters
----------
fieldName
The name of the field.
dimension
The dimension of the field.
nodes
The associated nodes. Only nodes with active fields are considered.
"""
#: The entries every node field holds, see :doc:`/documentation/nodefieldresults`: the solution ``U``,
#: the increment ``dU`` of the last accepted increment, the nodal force ``P``, and the rate ``V`` and
#: second rate ``A`` of the solution. There are no others.
ENTRIES = ("U", "dU", "P", "V", "A")
def __init__(self, fieldName: str, dimension: int, nodeSet: NodeSet):
self.name = fieldName
self.associatedSet = nodeSet
self.nodes = [n for n in nodeSet if fieldName in n.fields]
self.dimension = dimension
self._indicesOfNodesInArray = {n: i for i, n in enumerate(self.nodes)}
self._subsetCache = dict()
self._values = {entry: np.zeros((len(self.nodes), dimension), dtype=float) for entry in self.ENTRIES}
self._version = 0 #: bumped on every in-place mutation (resize); stable identity for AMR
[docs]
def resize(self, nodes) -> None:
"""Resize this NodeField in-place for a new list of associated nodes, preserving values
for every node identity present both before and after the resize (e.g. AMR-retained
nodes keep their converged state; new nodes get a zeroed entry until a caller fills it
in, e.g. with an interpolated warm start).
This object's identity is preserved -- consumers holding a reference to this NodeField
(or a :class:`NodeFieldSubset` of it) see the new size without re-fetching from the
model.
Parameters
----------
nodes
The new (possibly larger or smaller) list of nodes this NodeField is associated with.
Only nodes with this field active are retained, exactly as at construction.
"""
oldIndicesOfNodesInArray = self._indicesOfNodesInArray
oldValues = self._values
self.nodes = [n for n in nodes if self.name in n.fields]
self._indicesOfNodesInArray = {n: i for i, n in enumerate(self.nodes)}
# nodes retained across the resize (e.g. AMR-untouched nodes); computed once and reused for
# every value entry below, instead of a per-node dict lookup repeated for each entry
commonNodes = oldIndicesOfNodesInArray.keys() & self._indicesOfNodesInArray.keys()
oldIdx = np.fromiter((oldIndicesOfNodesInArray[n] for n in commonNodes), dtype=np.intp, count=len(commonNodes))
newIdx = np.fromiter(
(self._indicesOfNodesInArray[n] for n in commonNodes), dtype=np.intp, count=len(commonNodes)
)
newValues = dict()
for entry, oldArray in oldValues.items():
newArray = np.zeros((len(self.nodes), self.dimension), dtype=float)
if len(oldIdx):
newArray[newIdx] = oldArray[oldIdx]
newValues[entry] = newArray
self._values = newValues
self._version += 1
def _getNodeFieldSubsetClass(
self,
):
return NodeFieldSubset
def __getitem__(self, key):
return self._values[key]
[docs]
def indexOfNode(self, node) -> int:
"""
The row index of a node's entries within this field's value arrays.
Parameters
----------
node
The node.
Returns
-------
int
The row index.
"""
return self._indicesOfNodesInArray[node]
[docs]
def subset(self, subset) -> NodeFieldSubset:
"""
Get a view on a subset of the field.
Parameters
----------
subset
The subset, e.g., a single :class:`Node, or a :class:`NodeSet or :class:`ElementSet.
Returns
-------
NodeFieldSubset
The subset of the present NodeField.
"""
return self._getSubsetFromCache(subset)
def _getSubsetFromCache(self, subset) -> NodeFieldSubset:
"""
Exploit a cache to reuse already constructed NodeFieldSubsets.
If the subset does not exist, it will be created here.
Parameters
----------
subset
The subset, e.g., a single Node, or a NodeSet or ElementSet.
Returns
-------
NodeFieldSubset
The subset of the present NodeField.
"""
if subset in self._subsetCache:
return self._subsetCache[subset]
else:
self._subsetCache[subset] = self._getNodeFieldSubsetClass()(self, subset)
return self._subsetCache[subset]
[docs]
def copyEntriesFromOther(self, other, fieldValueEntries: list[str] = None):
"""
Copy values from another NodeField.
Of the nodes, the intersection is considered; the other nodes of this field get zero.
Parameters
----------
other
The NodeField to copy from.
fieldValueEntries
The list of entries which should be copied. Default: all entries are copied.
"""
if not fieldValueEntries:
fieldValueEntries = self.ENTRIES
commonNodes = self._indicesOfNodesInArray.keys() & other._indicesOfNodesInArray.keys()
for fieldValueEntry in fieldValueEntries:
self[fieldValueEntry][:] = 0.0
idcsHere = [self._indicesOfNodesInArray[n] for n in commonNodes]
idcsOther = [other._indicesOfNodesInArray[n] for n in commonNodes]
self[fieldValueEntry][idcsHere] = other[fieldValueEntry][idcsOther]
[docs]
def addEntriesFromOther(self, other, fieldValueEntries: list[str] | dict[str, str] = None):
"""
Add values from another NodeField.
Of the nodes, the intersection is considered.
Parameters
----------
other
The NodeField to add from.
fieldValueEntries
The entries which should be added. Default: all entries are added.
May be a list of entry names shared by both fields, or a dict mapping an
entry name on ``other`` to the (possibly differently named) entry name on
``self`` it should be accumulated into.
"""
if not fieldValueEntries:
fieldValueEntries = self.ENTRIES
if not isinstance(fieldValueEntries, dict):
fieldValueEntries = {entry: entry for entry in fieldValueEntries}
commonNodes = self._indicesOfNodesInArray.keys() & other._indicesOfNodesInArray.keys()
idcsHere = [self._indicesOfNodesInArray[n] for n in commonNodes]
idcsOther = [other._indicesOfNodesInArray[n] for n in commonNodes]
for otherEntry, selfEntry in fieldValueEntries.items():
self[selfEntry][idcsHere] += other[otherEntry][idcsOther]
[docs]
class NodeFieldSubset(NodeField):
def __init__(self, parentNodeField, subset):
self.parentNodeField = parentNodeField
self.name = parentNodeField.name
self.associatedSet = subset
self._seenParentVersion = parentNodeField._version
self.nodes = self._getSubsetNodes(subset)
self._indicesOfNodesInParentArray = np.array([parentNodeField._indicesOfNodesInArray[n] for n in self.nodes])
def _healIfParentResized(self):
"""Rebuild the cached parent-index array if the parent NodeField was resized (e.g. by AMR)
since this subset was created or last accessed. Since the parent NodeField (and, for a
NodeSet/ElementSet-based subset, ``associatedSet`` itself) has stable identity, this makes
the subset transparent to a mesh mutation -- no observer registration needed.
This keys off the *parent's* version only, not ``associatedSet``'s own version -- correct
as long as every mutator that changes a NodeSet/ElementSet's membership also resizes every
NodeField of that model in the same call (true today: hadaptivity._materialize always goes
through FEModel._resizeNodeFieldsForNodes, which resizes every NodeField, whenever any set
changes). A future mutator that changes set membership without touching NodeFields would
need this subset to key off ``associatedSet._version`` as well, or this heal would silently
miss it."""
if self.parentNodeField._version != self._seenParentVersion:
self.nodes = self._getSubsetNodes(self.associatedSet)
self._indicesOfNodesInParentArray = np.array(
[self.parentNodeField._indicesOfNodesInArray[n] for n in self.nodes]
)
self._seenParentVersion = self.parentNodeField._version
def __getitem__(self, key):
self._healIfParentResized()
return self.parentNodeField[key][self._indicesOfNodesInParentArray]
def __contains__(self, key):
self._healIfParentResized()
return key in self.nodes
[docs]
def subset(self, subset):
raise Exception("Subsets of subsets are not yet implemented!")
def _getSubsetNodes(self, subset) -> list[Node]:
"""
Get the nodes associated with a subset.
Only nodes with the active field are considered.
Parameters
----------
subset
The subset, e.g., a single Node, a NodeSet or ElementSet.
Returns
-------
list[Node]
The list of subset nodes.
"""
if isinstance(subset, Node):
nodeCandidates = [
subset,
]
elif isinstance(subset, ElementSet):
nodeCandidates = subset.extractNodeSet()
elif isinstance(subset, NodeSet):
nodeCandidates = subset
else:
raise Exception("Invalid subset")
return [n for n in nodeCandidates if n in self.parentNodeField._indicesOfNodesInArray]