import itertools as it
import logging
from collections import deque, namedtuple
from contextlib import contextmanager
from ctypes import c_int32, c_int64, c_void_p, POINTER, addressof, cast
from textwrap import dedent
from ..tecutil import _tecutil
from ..constant import *
from ..exception import *
from .. import session, tecutil, version
log = logging.getLogger(__name__)
@tecutil.lock_attributes
class Elementmap(c_void_p):
"""Facemap reverse look-up."""
def __init__(self, zone):
self.zone = zone
super().__init__(self._native_reference())
@tecutil.lock()
def _native_reference(self):
with self.zone.dataset.frame.activated():
return _tecutil.DataElemGetReadableRef(self.zone.index + 1)
def num_faces(self, element):
return _tecutil.DataElemGetNumFaces(self, element + 1)
def face(self, element, offset):
return _tecutil.DataElemGetFace(self, element + 1, offset + 1) - 1
def faces(self, element):
return [self.face(element, f) for f in range(self.num_faces(element))]
[docs]
@tecutil.lock_attributes
class Facemap(c_void_p):
r"""Connectivity list definition and control.
A facemap holds the connectivity for a polytopal finite-element zone. This
includes node-to-element and element-to-element connections. The following
script creates a quad of two triangles from scratch using the PyTecplot
low-level data creation interface. The data created looks like this:
.. code-block:: none
Node positions (x,y,z):
(1,1,1)
3
/ \
/ \
(0,1,.5) 2-----1 (1,0,.5)
\ /
\ /
0
(0,0,0)
Element indices are used when identifying the left and right of each face,
where :math:`-1` is used to indicate no element:
.. code-block:: none
*
-1 / \ -1
/ 1 \
*-----*
\ 0 /
-1 \ / -1
*
.. code-block:: python
:emphasize-lines: 28
import tecplot as tp
from tecplot.constant import *
nodes = ((0, 0, 0 ),
(1, 0, 0.5),
(0, 1, 0.5),
(1, 1, 1 ))
faces = ((0, 1),
(1, 2),
(2, 0),
(1, 3),
(3, 2))
elements = (( 0, 0, 0, 1, 1), # elements to the left of each face
(-1, 1, -1, -1, -1)) # elements to the right of each face
num_elements = 2
scalar_data = (0, 1, 2, 3)
ds = tp.active_frame().create_dataset('Data', ['x','y','z','s'])
z = ds.add_poly_zone(ZoneType.FEPolygon,
name='FE Polygon Float (4,2,5) Nodal',
num_points=len(nodes),
num_elements=num_elements,
num_faces=len(faces))
z.values('x')[:] = [n[0] for n in nodes]
z.values('y')[:] = [n[1] for n in nodes]
z.values('z')[:] = [n[2] for n in nodes]
z.facemap.set_mapping(faces, elements)
z.values('s')[:] = scalar_data
### setup a view of the data
plot = tp.active_frame().plot(PlotType.Cartesian3D)
plot.activate()
cont = plot.contour(0)
cont.colormap_name = 'Sequential - Yellow/Green/Blue'
cont.colormap_filter.distribution = ColorMapDistribution.Continuous
for ax in plot.axes:
ax.show = True
plot.show_mesh = False
plot.show_contour = True
plot.show_edge = True
plot.use_translucency = True
fmap = plot.fieldmap(z)
fmap.surfaces.surfaces_to_plot = SurfacesToPlot.All
fmap.effects.surface_translucency = 40
# View parameters obtained interactively from Tecplot 360
plot.view.distance = 10
plot.view.width = 2
plot.view.psi = 80
plot.view.theta = 30
plot.view.alpha = 0
plot.view.position = (-4.2, -8.0, 2.3)
# Turning on mesh, we can see all the individual triangles
plot.show_mesh = True
plot.fieldmap(z).mesh.line_pattern = LinePattern.Dashed
cont.levels.reset_to_nice()
tp.export.save_png('polygons1.png', 600, supersample=3)
.. figure:: /_static/images/polygons1.png
:width: 300px
:figwidth: 300px
Two triangle polygons showing edge and mesh lines.
"""
def __init__(self, zone):
self.zone = zone
if self.zone.num_faces > 0:
try:
ref = self._native_reference()
except:
ref = None
else:
ref = None
super().__init__(ref)
@property
def _has_data_backing(self):
return bool(self)
@tecutil.lock()
def _native_reference(self, writable=False):
try:
_dispatch = {
True: _tecutil.DataFaceMapGetWritableRef,
False: _tecutil.DataFaceMapGetReadableRef}
with self.zone.dataset.frame.activated():
return _dispatch[writable](self.zone.index + 1)
except TecplotLogicError:
raise TecplotLogicError(
'A facemap must first be created with Facemap.alloc()')
[docs]
@tecutil.lock()
def alloc(self, face_nodes, boundary_faces=0, boundary_connections=0):
"""Allocate space for the facemap.
Parameters:
face_nodes (`int`): Total number of nodes for all faces. This is
not the number of unique nodes but the total number. For
example if a facemap defines two triangle polygons that share a
common face, ``faces`` would be 5 and ``face_nodes`` would be
6, not 4.
boundary_faces (`int`, optional): Total number of boundary faces.
(default: 0)
boundary_connections (`int`, optional): Total number of boundary
face elements or boundary face element/zone pairs. (default: 0)
Returns:
`Facemap`
This is called when using the `Facemap.set_mapping()` method which is
the preferred method for filling the connectivity of polytope zones. If
the zone does not already have space allocated for a facemap and if you
wish to use the `Facemap.set_nodes()` and `Facemap.set_elements()`
methods to fill in the connectivity, then this must be called first::
>>> facemap = zone.facemap.alloc(400, 25, 50)
.. note:: Tecplot version 2017.2 or later.
Setting the boundary faces and boundary connections using PyTecplot
requires Tecplot version 2017.2 or later.
"""
if not _tecutil.DataFaceMapAlloc(self.zone.index + 1,
self.zone.num_faces, face_nodes,
boundary_faces, boundary_connections):
raise TecplotSystemError()
c_void_p.__init__(self, self._native_reference())
def __eq__(self, other):
if not (self._has_data_backing and other._has_data_backing):
return False
self_addr = addressof(cast(self, POINTER(c_int64)).contents)
other_addr = addressof(cast(other, POINTER(c_int64)).contents)
return self_addr == other_addr
def __ne__(self, other):
return not (self == other)
def __len__(self):
return self.num_faces()
[docs]
def num_faces(self, element=None):
"""The number of faces of an element in this `Facemap`.
Parameters:
element (`int`, optional): Zero-based index of an
element. If no element is given, the total number of faces
in the map are returned.
Returns:
`int`
Example usage::
>>> print(zone.facemap.num_faces())
1048576
"""
if element is None:
if not self._has_data_backing:
return self.zone.num_faces
return _tecutil.DataFaceMapGetNFaces(self)
else:
return Elementmap(self.zone).num_faces(element)
@property
def num_unique_nodes(self):
"""`int`: The number of unique nodes in this `Facemap`.
.. note:: This property is read-only.
Example usage::
>>> print(zone.facemap.num_unique_nodes)
4194304
"""
if not self._has_data_backing:
return self.zone.num_points
return _tecutil.DataFaceMapGetNumNodes(self)
[docs]
def num_nodes(self, face=None, element=None):
"""The number nodes for a given face in this `Facemap`.
Parameters:
face (`int`, required if no *element* is given): The
zero-based index of the face either within the given *element*
or globally. If no face is given, the number of nodes on the
given *element* are returned.
element (`int`, required if no face is given): The
zero-based index of an element. If no element is given, then
*face* is globally indexed from zero over the whole zone.
Returns:
`int` The number of nodes.
Example usage::
>>> print(zone.facemap.num_nodes(face=1))
4
"""
if element is None:
if face is None:
msg = 'face or element must be specified'
raise TecplotValueError(msg)
# While the following sum() is technically correct, it is
# extremely slow as currently, there is no way to query
# the total number of nodes in the connectivity list directly.
#
# return sum(self.num_nodes(i) for i in range(self.num_faces()))
else:
if self.zone.zone_type == ZoneType.FEPolygon:
return 2
else:
return _tecutil.DataFaceMapGetNFaceNodes(self, face + 1)
else:
emap = Elementmap(self.zone)
if face is None:
return sum(self.num_nodes(f) for f in emap.faces(element))
else:
return self.num_nodes(emap.face(element, face))
[docs]
def num_boundary_connections(self, face=None, element=None):
"""The number of boundary connections for a given face.
Parameters:
face (`int`, required if no element is given): The
zero-based index of the face.
element (`int`, optional): Zero-based index of an
element. If given, *face* will be locally indexed within this
element, otherwise *face* is globally indexed over the whole
zone.
Returns:
`int` The number of boundary connections.
Example usage::
>>> print(zone.facemap.num_boundary_connections(1))
1
"""
if face is None:
if element is None:
msg = 'face or element must be specified'
raise TecplotValueError(msg)
# While the following sum() is technically correct using
# the face indices here, it is extremely slow as currently,
# there is no way to query the total number of boundary
# connections directly.
#
# faces = range(self.num_faces())
else:
emap = Elementmap(self.zone)
faces = emap.faces(element)
return sum(self.num_boundary_connections(f) for f in faces)
elif element is not None:
face = self.face(element, face)
return _tecutil.DataFaceMapGetNBndryConns(self, face + 1)
_BoundaryConnection = namedtuple('BoundaryConnection', ['element', 'zone'])
[docs]
def boundary_connection(self, face, offset, element=None):
"""The connected element and zone along a boundary face.
Parameters:
face (`int`): The zero-based index of the face.
offset (`int`): The zero-based index of the node being
requested.
element (`int`, optional): Zero-based index of an
element. If given, *face* will be locally indexed within this
element, otherwise *face* is globally indexed over the whole
zone.
Returns:
`namedtuple <collections.namedtuple>`: ``(element, zone)``:
``element``
The zero-based index of the neighboring element.
``zone``
The zone holding the neighboring element.
Example usage::
>>> bconn = zone.facemap.boundary_connection(0, 2)
>>> print(bconn.element)
128
>>> print(bconn.zone.index)
2
"""
if element is not None:
face = self.face(element, face)
elem, zone = _tecutil.DataFaceMapGetBndryConn(self, face + 1,
offset + 1)
return Facemap._BoundaryConnection(elem - 1,
self.zone.dataset.zone(zone - 1))
[docs]
def node(self, face, offset, element=None):
"""The node index along a specific face.
Parameters:
face (`int`): The zero-based index of the face.
offset (`int`): The zero-based index of the node being
requested.
element (`int`, optional): Zero-based index of an
element. If given, *face* will be locally indexed within this
element, otherwise *face* is globally indexed over the whole
zone.
Returns:
`int` The node at the specified location.
Example usage::
>>> print(zone.facemap.face_node(0, 2))
128
"""
if element is not None:
face = self.face(element, face)
return _tecutil.DataFaceMapGetFaceNode(self, face + 1, offset + 1) - 1
[docs]
def left_element(self, face, element=None):
"""The element to the left of a specific face.
Parameters:
face (`int`): The zero-based index of the face.
element (`int`, optional): Zero-based index of an
element. If given, *face* will be locally indexed within this
element, otherwise *face* is globally indexed over the whole
zone.
Returns:
`int`
A negative number indicates there is no element to the left of this
face. Example usage::
>>> print(zone.facemap.left_element(0))
128
"""
if element is not None:
face = self.face(element, face)
return _tecutil.DataFaceMapGetLeftElem(self, face + 1) - 1
[docs]
def right_element(self, face, element=None):
"""The element to the right of a specific face.
Parameters:
face (`int`): The zero-based index of the face.
element (`int`, optional): Zero-based index of an
element. If given, *face* will be locally indexed within this
element, otherwise *face* is globally indexed over the whole
zone.
Returns:
`int`
A negative number indicates there is no element to the right of this
face. Example usage::
>>> print(zone.facemap.right_element(0))
-1
"""
if element is not None:
face = self.face(element, face)
return _tecutil.DataFaceMapGetRightElem(self, face + 1) - 1
[docs]
def face(self, element, offset):
"""Face index on a specific element.
Parameters:
element (`int`): The zero-based index of the element.
offset (`int`): The zero-based index of the face being
requested.
Returns:
`int` Zero-based index of the face at the specified
location.
Example usage::
>>> print(zone.facemap.face(0, 2))
128
"""
return Elementmap(self.zone).face(element, offset)
@property
def node_c_type(self):
"""The data type of the node indices.
Possible values: `ctypes.c_int32`, `ctypes.c_int64`
.. note:: This property is read-only.
"""
_ctypes = {
OffsetDataType.OffsetDataType_32Bit: c_int32,
OffsetDataType.OffsetDataType_64Bit: c_int64}
if self._has_data_backing:
data_type = _tecutil.DataFaceMapGetNodeRawItemType(self)
return _ctypes[data_type]
@property
def element_c_type(self):
"""The data type of the element indices.
Possible values: `ctypes.c_int32`, `ctypes.c_int64`
.. note:: This property is read-only.
"""
_ctypes = {
OffsetDataType.OffsetDataType_32Bit: c_int32,
OffsetDataType.OffsetDataType_64Bit: c_int64}
if self._has_data_backing:
ref = self._native_reference(writable=True)
data_type = _tecutil.DataFaceMapGetElementRawItemType(ref)
return _ctypes[data_type]
[docs]
def set_nodes(self, facemap):
"""Sets the polytope connectivity.
Parameters:
facemap (2D array of zero-based `integers <int>`): The `list` of
`lists <list>` which need not all be the same length, defining
the individual elements by their nodes.
The facemap must first be allocated with `Facemap.alloc()` and must be
called from within a `Facemap.assignment()` context and should be
followed by a call to `Facemap.set_elements()` to complete the
connectivity map information needed for rendering. It is recomended to
use the `Facemap.set_mapping()` which does all the required
book keeping.
"""
_dispatch = {
c_int32: _tecutil.DataFaceMapAssignNodes,
c_int64: _tecutil.DataFaceMapAssignNodes64}
data_type = self.node_c_type
nfaces = len(facemap)
nnodes = [len(n) for n in facemap]
nfacenodes = sum(nnodes)
if self.zone.zone_type is ZoneType.FEPolygon:
faces = None
else:
faces = (c_int32 * nfaces)(*nnodes)
nodes = (data_type * nfacenodes)(*[n + 1 for n in it.chain(*facemap)])
if __debug__:
if min(nodes) < 1 or self.num_unique_nodes < max(nodes):
raise TecplotIndexError
log.debug(dedent('''\
Facemap set nodes:
faces({}): {}
nodes: {}''').format(nfaces, tecutil.array_to_str(faces),
tecutil.array_to_str(nodes)))
ref = self._native_reference(writable=True)
_dispatch[data_type](ref, nfaces, faces, nodes)
[docs]
def set_elements(self, left_elements, right_elements):
"""Sets the polytope connectivity.
Parameters:
left_elements (array of zero-based `integers <int>`): This is an
array of the elements to the left of each face in the facemap
and must be the same length as the number of faces.
right_elements (array of zero-based `integers <int>`): Same as
*left_elements* for the right side of each face.
The facemap must first be allocated with `Facemap.alloc()` and must be
called from within a `Facemap.assignment()` context and should follow a
call to `Facemap.set_nodes()` to complete the connectivity map
information needed for rendering. Using `Facemap.set_mapping()` is
recommended, which does all the required book keeping.
"""
_dispatch = {
c_int32: _tecutil.DataFaceMapAssignElems,
c_int64: _tecutil.DataFaceMapAssignElems64}
data_type = self.element_c_type
nelements = len(left_elements)
left = (data_type * nelements)(*[e + 1 for e in left_elements])
right = (data_type * nelements)(*[e + 1 for e in right_elements])
if __debug__:
if self.zone.num_elements < max(left):
raise TecplotIndexError
if self.zone.num_elements < max(right):
raise TecplotIndexError
log.debug(dedent('''\
Facemap set elements:
left({}): {}
right({}): {}'''
).format(len(left), tecutil.array_to_str(left),
len(right), tecutil.array_to_str(right)))
ref = self._native_reference(writable=True)
_dispatch[data_type](ref, nelements, left, right)
[docs]
def set_boundary_connections(self, elements, zones):
"""Set the boundary connections.
Parameters:
elements (2D array of `integers <int>`): Zero-based indices of the
connected elements. This is a "ragged" array of dimension
:math:`(N,E_i)` where :math:`N` is the number of boundary faces
and :math:`E_i` is the number of boundary connected elements
for the :math:`i^{th}` face.
zones (2D array of `integers <int>`): Zero-based indices of the
zones for each entry given in *elements*. This must be the same
shape as *elements*.
The facemap must first be allocated with `Facemap.alloc()` and must be
called from within a `Facemap.assignment()` context, and should follow
calls to `Facemap.set_nodes()` and `Facemap.set_elements()` to complete
the connectivity map information needed for rendering. Using
`Facemap.set_mapping()` is recommended, which does all the required
book keeping.
"""
_dispatch = {
c_int32: _tecutil.DataFaceMapAssignBConns,
c_int64: _tecutil.DataFaceMapAssignBConns64}
data_type = self.node_c_type
nbfaces = len(elements)
nbconns = (c_int32 * nbfaces)(*[len(n) for n in elements])
nbelems = sum(nbconns)
def _flatten_add_one(arr):
def _f(x):
return x + 1 if x is not None else 0
return list(map(_f, it.chain(*arr)))
def _flatten_add_one_index(arr):
def _f(x):
return getattr(x, 'index', x) + 1 if x is not None else 0
return list(map(_f, it.chain(*arr)))
belems = (data_type * nbelems)(*_flatten_add_one(elements))
bzones = (c_int32 * nbelems)(*_flatten_add_one_index(zones))
if __debug__:
if min(belems) < 1 or self.zone.num_elements < max(belems):
raise TecplotIndexError
if min(bzones) < 1 or self.zone.dataset.num_zones < max(bzones):
raise TecplotIndexError
log.debug(dedent('''\
Facemap set boundary connections:
num boundary faces: {}
num boundary conns: {}
boundary elems: {}
boundary zones: {}'''.format(nbfaces,
tecutil.array_to_str(nbconns),
tecutil.array_to_str(belems),
tecutil.array_to_str(bzones))))
if version.sdk_version_info >= (2021, 2):
# The TecUtil API 2021.2+ requires boundary zone unique IDs.
bzone_uids = (c_int64 * nbelems)()
_tecutil.DataSetGetUniqueIDsForZones(bzones, nbelems, bzone_uids)
bzones = bzone_uids
ref = self._native_reference(writable=True)
_dispatch[data_type](ref, nbfaces, nbconns, belems, bzones)
[docs]
def set_mapping(self, facemap, elements, boundary_elements=None,
boundary_zones=None):
"""Set the node and element connectivity for this polytope zone.
Parameters:
facemap (2D array of zero-based `integers <int>`): The `list` of
`lists <list>` which need not all be the same length, defining
the individual elements by their nodes.
elements (2D array of zero-based `integers <int>`): This is a
:math:`(2,N)` array where :math:`N` is the number of faces and
the items are a list of the elements to the left and right of
the face respectively.
boundary_elements (2D array of `integers <int>`, optional):
Zero-based indices of the connected elements. This is a
"ragged" array of dimension :math:`(N, E_i)` where :math:`N` is
the number of boundary faces and :math:`E_i` is the number of
boundary connected elements for the :math:`i^{th}` face.
boundary_zones (2D array of `integers <int>`, optional): Zero-based
indices of the zones for each entry given in *elements*. This
must be the same shape as *elements*.
See the code example for the `Facemap` class object for details.
.. note:: **boundary_elements** and **boundary_zones**
The two parameters, **boundary_elements** and **boundary_zones**,
must be supplied together or not at all.
"""
nfacenodes = sum(len(n) for n in facemap)
if boundary_elements is not None:
nbfaces = len(boundary_elements)
nbelems = sum(len(n) for n in boundary_elements)
else:
nbfaces = 0
nbelems = 0
if __debug__:
log.debug(dedent('''\
Facemap alloc:
num face nodes: {}
num boundary faces: {}
num boundary elements: {}'''.format(nfacenodes, nbfaces,
nbelems)))
self.alloc(nfacenodes, nbfaces, nbelems)
with self.assignment():
self.set_nodes(facemap)
self.set_elements(*elements)
if boundary_elements is not None:
self.set_boundary_connections(boundary_elements, boundary_zones)
[docs]
@contextmanager
def assignment(self):
"""Context manager for assigning facemap connections.
This context ensures the proper book keeping is done when setting the
connectivity list and must be used with `Facemap.set_nodes()`,
`Facemap.set_elements()` and `Facemap.set_boundary_connections()`,
which are used to define the connectivity of the zone.
"""
if hasattr(self, '_in_assignment'):
yield
else:
with tecutil.lock():
ref = self._native_reference(writable=True)
_tecutil.DataFaceMapBeginAssign(ref)
try:
self._in_assignment = True
yield
finally:
try:
if not _tecutil.DataFaceMapEndAssign(ref):
raise TecplotSystemError()
finally:
del self._in_assignment
[docs]
def set_elementmap(self, elementmap):
"""Define connectivity per element.
Parameters:
elementmap (`integers <int>`): Zero-based indices of the nodes that
make up each face of each element. For polygons, the map is a
list of elements, each made of up a list of nodes. For
polyhedrons, this is a list of elements, made up a list of
faces, each made up of a list of nodes.
.. warning::
This method is mutually exclusive with the `Facemap.set_mapping()`
and `Facemap.assignment()`, `Facemap.set_nodes()`,
`Facemap.set_elements()` and `Facemap.set_boundary_connections()`
family of methods. The size of the underlying arrays are calculated
based on the elementmap given and a call to `Facemap.alloc()` is
made which will override any previous allocation.
This may be a more convenient way to describe the connectivity of a
polytope zone, however it does not support boundary face connections to
other zones (see `Facemap.set_mapping()`).
Here is an example of an elementmap for two triangles (polygons)::
nodes = ((0, 0, 0 ),
(1, 0, 0.5),
(0, 1, 0.5),
(1, 1, 1 ))
elementmap = ((0, 1, 2), # polygon 0, 3 faces
(1, 3, 2)) # polygon 1, 3 faces
This is an example of an elementmap for two tetrahedrons
(polyhedrons)::
nodes = ((0, 0, 0),
(1, 1, 0),
(1, 0, 1),
(0, 1, 1),
(0, 0, 1))
elementmap = (((0, 1, 2), # polyhedron 0, 4 faces
(0, 1, 3),
(1, 3, 2),
(0, 2, 3)),
((0, 2, 3), # polyhedron 1, 4 faces
(2, 3, 4),
(0, 2, 4),
(0, 4, 3)))
"""
_dispatch = {
c_int32: _tecutil.DataFaceMapAssignElemToNodeMap,
c_int64: _tecutil.DataFaceMapAssignElemToNodeMap64}
data_type = self.node_c_type
if data_type is None:
data_type = c_int32
nelems = len(elementmap)
faces = [len(e) for e in elementmap]
faces = (c_int32 * len(faces))(*faces)
if self.zone.zone_type == ZoneType.FEPolygon:
nodes = None
elemmap = [n + 1 for face in elementmap for n in face]
else: # if self.zone.zone_type == ZoneType.FEPolyhedron:
nodes = [len(face) for elem in elementmap for face in elem]
nodes = (c_int32 * len(nodes))(*nodes)
elemmap = [n + 1 for elem in elementmap
for face in elem
for n in face]
elemmap = (data_type * len(elemmap))(*elemmap)
if __debug__:
if len(elemmap):
if min(elemmap) < 1 or self.num_unique_nodes < max(elemmap):
raise TecplotIndexError
log.debug(dedent('''\
FaceMapAssign:
nelems: {}
faces: {}
nodes: {}
elemmap: {}''').format(nelems, tecutil.array_to_str(faces),
tecutil.array_to_str(nodes),
tecutil.array_to_str(elemmap)))
# reset facemap before we fill it with the nodemap
if self.zone.zone_type == ZoneType.FEPolygon:
self.alloc(int(self.zone.num_faces * self.num_nodes(0)))
else:
self.alloc(0)
with tecutil.lock():
ref = self._native_reference(writable=True)
if not _dispatch[data_type](ref, nelems, faces, nodes, elemmap):
raise TecplotSystemError()