I'm in the process of designing a finite element library. For a given problem, the finite element mesh used can have elements of different dimensions (for example tetrahedra and triangles), and combining different elements of the same dimension is also possible (for example tetrahedra and hexahedra). Therefore, I need a data structure that stores the finite elements' information. The most fundamental information is the elements' connectivity (the node IDs that define the element). For example, I need to store somehow that triangular element 4 is connected to nodes 5, 6, and 10.
My first attempt was to create a list whose index is the dimension (0,1,2 or 3) and that stores dictionaries. These dictionaries have string keys (identifiers) and the values are numpy arrays (each row representing an element connectivity). I need to do this because the numpy arrays for a given dimension have different shapes depending on the string identifiers.
This is the class:
import os from collections import OrderedDict import numpy.ma as ma flatten = lambda l: [item for sublist in l for item in sublist] class ElementData(list): def __init__(self, *args, **kwargs): self.reset() super(ElementData, self).__init__(*args, **kwargs) def __iter__(self): for k, v in self[self.idx].items(): for i, e in enumerate(v): yield (k,i,e) if not ma.is_masked(e) else (k,i, None) self.reset() def __call__(self, idx): self.idx = idx-1 return self def __getitem__(self, index): if index >= len(self): self.expand(index) return super(ElementData, self).__getitem__(index) def __setitem__(self, index, value): if index >= len(self): self.expand(index) list.__setitem__(self, index, value) def __str__(self): return "Element dimensions present: {}\n".format([i for i in range(len(self)) if self[i]]) + super(ElementData, self).__str__() def keys(self): return flatten([list(self[i].keys()) for i in range(len(self))]) def reset(self): self.idx = -1 self.d = -1 def expand(self, index): self.d = max(index, self.d) for i in range(index + 1 - len(self)): self.append(OrderedDict()) def strip(self, value=None): if not callable(value): saved_value, value = value, lambda k,v: saved_value return ElementData([OrderedDict({k:value(k, v) for k,v in i.items()}) for i in super(ElementData, self).__iter__()]) def numElements(self, d): def elementsOfDimension(d): # loop over etypes nelems = 0 for v in self[d].values(): nelems += v.shape[0] if not isinstance(v, ma.MaskedArray) else v.shape[0] - v.mask.any(axis=1).sum() return nelems # compute the number of all elements if d == -1: nelems = 0 for i in range(self.d+1): nelems += elementsOfDimension(i) return nelems else: # of specific dimension only return elementsOfDimension(d)
The class works nicely, and it allows me to loop seamlessly through all items of a particular dimension. However, there are other data associated with each element that's stored separately, for example its material. Therefore, I decided to use the same data structure to refer to other properties. To that end I use the strip
function of the class, to return me the entire structure without the numpy arrays.
The problem that I is that the original data structure is dynamic, and if I change it, I have to modify every other structure that depends on it. I really think I went in the wrong direction while designing this class. Perhaps there's a simpler way to approach this problem? I thought about storing the extra information next to the numpy arrays (as tuples for example), but I don't know whether this is good or not. The choices made while designing software can really make our life miserable later, and I'm starting realize this now.
UPDATE
Using the class above, one example could be the following:
Element dimensions present: [0, 1, 2] [OrderedDict([('n1', array([[0], [1], [3]]))]), OrderedDict([('l2', array([[1, 2]]))]), OrderedDict([('q4', array([[0, 1, 5, 4], [5, 1, 2, 6], [6, 2, 3, 7], [7, 3, 0, 4], [4, 5, 6, 7]]))])]
where the data structure has been used to store elements of 0 (node), 1 (line) and 2 (quadrangles) dimensions.
2 Answers
Answers 1
When said "Complex data structure's dependences", you probably may speak about "graph", did you double check if any existing graph theory fit your needs ? (like "Route problems")
Answers 2
Comment: the design goes against the logical structure of the program.
I used the given Element data example and didn't expect to get the whole picture at once.
Comment: Each element has a unique dimension (a triangle has always dimension 2, as a tetrahedron has always dimension 3 and a node dimension 0).
Sorry, I misinterpreted from the question "... elements of different dimensions..." with "Element have different dimensions". I adapted my Proposal.
Comment: ... the problem arises when I modify the data structure (for example by adding elements)
As long as you don't show a Data example for this problem, I can't thinking above a solution.
My Proposal:
class Dimension(object): """ Base class for all Dimensions Dimension has no knowledge to what the data belongs to """ def __init__(self, data=None): pass class Element(object): """ Base class for all Elements Hold on class Dimension(object): """ def __init__(self, dimension=None): pass class Triangle(Element): def __init__(self, dimension): super().__init__(dimension=dimension) class Tetrahedron(Element): def __init__(self, dimension): super().__init__(dimension=dimension) class Node(Element): def __init__(self, dimension): super().__init__(dimension=dimension)
Usage: Building your example Element
node = Node(dimension=[0,1,3]) line = Triangle(dimension=[0,2]) quad = Tetrahedron(dimension=[[0, 1, 5, 4], [5, 1, 2, 6], [6, 2, 3, 7], [7, 3, 0, 4], [4, 5, 6, 7]])
New Future Element, only to show class Element
could extended:
# New future dimensions - No changes to class Element() required class FutureNodTriTet(Element): def __init__(self, dimension): super().__init__(dimension=dimension) future_NTT = FutureNodTriTet(dimension=[0, (1,3), (4,5,6)])
Please comment if this is closer to your needs.
0 comments:
Post a Comment