You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
8699 lines
375 KiB
8699 lines
375 KiB
2 years ago
|
"""
|
||
|
Exomerge is a lightweight Python interface for manipulating ExodusII files.
|
||
|
|
||
|
Copyright(C) 1999-2020, 2022, 2023 National Technology & Engineering Solutions
|
||
|
of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with
|
||
|
NTESS, the U.S. Government retains certain rights in this software.
|
||
|
|
||
|
See packages/seacas/LICENSE for details
|
||
|
|
||
|
Author: Tim Kostka (tdkostk@sandia.gov)
|
||
|
Created: May 3, 2012
|
||
|
|
||
|
Simple example:
|
||
|
>>> import exomerge
|
||
|
>>> model = exomerge.import_model('results.e')
|
||
|
>>> model.delete_element_block(1)
|
||
|
>>> model.export_model('most_results.e')
|
||
|
|
||
|
Documentation can be accessed online through Python's pydoc utility:
|
||
|
$ pydoc exomerge
|
||
|
$ pydoc exomerge.ExodusModel.delete_element_block
|
||
|
|
||
|
Documentation and sample scripts can be found in the following report.
|
||
|
"Exomerge User's Manual: A lightweight Python interface for manipulating
|
||
|
Exodus files" (SAND2013-0725)
|
||
|
|
||
|
For additional information or to provide feedback or feature requests, please
|
||
|
contact the author.
|
||
|
|
||
|
This is the python-3 version
|
||
|
"""
|
||
|
|
||
|
# import standard modules
|
||
|
import string
|
||
|
import textwrap
|
||
|
import sys
|
||
|
import traceback
|
||
|
import os
|
||
|
import inspect
|
||
|
import re
|
||
|
import datetime
|
||
|
import itertools
|
||
|
import math
|
||
|
import struct
|
||
|
import bisect
|
||
|
import colorsys
|
||
|
import difflib
|
||
|
import operator
|
||
|
|
||
|
if sys.version_info[0] < 3:
|
||
|
raise Exception("Python-3 version. If using python-2, try `import exomerge2 as exomerge`")
|
||
|
|
||
|
# import exodus module
|
||
|
# (exodus.py should be in the same directory as this file)
|
||
|
try:
|
||
|
import exodus
|
||
|
except Exception:
|
||
|
import exodus3 as exodus
|
||
|
|
||
|
# informal version number of this module
|
||
|
__version__ = "8.6.1"
|
||
|
VERSION = __version__
|
||
|
|
||
|
# contact person for issues
|
||
|
CONTACT = 'Tim Kostka <tdkostk@sandia.gov>'
|
||
|
|
||
|
# show the banner on first use
|
||
|
SHOW_BANNER = True
|
||
|
|
||
|
# if true, will crash if warnings are generated
|
||
|
EXIT_ON_WARNING = False
|
||
|
|
||
|
# if true, will suppress output from exodus.py
|
||
|
# this will not suppress the banner output
|
||
|
SUPPRESS_EXODUS_OUTPUT = False
|
||
|
|
||
|
# a list of deprecated or renamed functions
|
||
|
# When the user calls one of these, it will issue a warning message
|
||
|
# saying it is deprecated. If it has simply been renamed, we will call that
|
||
|
# function. If it has been deleted, we will error out.
|
||
|
# This is a dict where the key is the deprecated function name and the value
|
||
|
# is the renamed function, or None.
|
||
|
# e.g. DEPRECATED_FUNCTIONS['build_hex8_cube'] = 'create_hex8_cube'
|
||
|
DEPRECATED_FUNCTIONS = dict()
|
||
|
DEPRECATED_FUNCTIONS['write'] = 'export'
|
||
|
|
||
|
|
||
|
class DummyFile(object):
|
||
|
"""Dummy class used to suppress output to stdout."""
|
||
|
def write(self, x):
|
||
|
"""Ignore the write command."""
|
||
|
|
||
|
|
||
|
def import_model(filename, *args, **kwargs):
|
||
|
"""
|
||
|
Load information from an ExodusII file.
|
||
|
|
||
|
This function is a wrapper around 'ExodusModel.import_model(...)' and is
|
||
|
provided for convenience. Internally, this is equivalent to executing the
|
||
|
following two statements.
|
||
|
|
||
|
>>> model = ExodusModel()
|
||
|
>>> model.import_model(...)
|
||
|
|
||
|
See 'ExodusModel.import_model' for additional information.
|
||
|
|
||
|
"""
|
||
|
model = ExodusModel()
|
||
|
model.import_model(filename, *args, **kwargs)
|
||
|
return model
|
||
|
|
||
|
|
||
|
class ExodusModel(object):
|
||
|
"""This class holds all information stored within an ExodusII file."""
|
||
|
|
||
|
# define translation from two faces to a 3D cohesive element
|
||
|
COHESIVE_FORMULA = dict()
|
||
|
COHESIVE_FORMULA['quad4'] = ['hex8', (0, 1, 2, 3, 4, 5, 6, 7)]
|
||
|
COHESIVE_FORMULA['tri3'] = ['wedge6', (0, 1, 2, 3, 4, 5)]
|
||
|
COHESIVE_FORMULA['tri6'] = [
|
||
|
'wedge12', (0, 1, 2, 6, 7, 8, 3, 4, 5, 9, 10, 11)
|
||
|
]
|
||
|
|
||
|
# define calculation for element length/area/volume
|
||
|
VOLUME_FORMULA = dict()
|
||
|
VOLUME_FORMULA['line2'] = [1.0, (0, 1)]
|
||
|
VOLUME_FORMULA['line3'] = VOLUME_FORMULA['line2']
|
||
|
VOLUME_FORMULA['tri3'] = [0.5, (0, 1), (0, 2)]
|
||
|
VOLUME_FORMULA['tri6'] = VOLUME_FORMULA['tri3']
|
||
|
VOLUME_FORMULA['quad4'] = [0.5, (0, 2), (1, 3)]
|
||
|
VOLUME_FORMULA['quad6'] = VOLUME_FORMULA['quad4']
|
||
|
VOLUME_FORMULA['quad8'] = VOLUME_FORMULA['quad4']
|
||
|
VOLUME_FORMULA['tet4'] = [1.0 / 6.0, (0, 1), (0, 2), (0, 3)]
|
||
|
VOLUME_FORMULA['tet10'] = VOLUME_FORMULA['tet4']
|
||
|
VOLUME_FORMULA['wedge6'] = [
|
||
|
0.5, ((0, 3), (1, 4)), ((0, 3), (2, 5)), ((0, 1, 2), (3, 4, 5))
|
||
|
]
|
||
|
VOLUME_FORMULA['wedge12'] = VOLUME_FORMULA['wedge6']
|
||
|
VOLUME_FORMULA['wedge15'] = VOLUME_FORMULA['wedge6']
|
||
|
VOLUME_FORMULA['wedge16'] = VOLUME_FORMULA['wedge6']
|
||
|
VOLUME_FORMULA['hex8'] = [
|
||
|
1.0, ((0, 3, 4, 7), (1, 2, 5, 6)), ((0, 1, 4, 5), (2, 3, 6, 7)),
|
||
|
((0, 1, 2, 3), (4, 5, 6, 7))
|
||
|
]
|
||
|
VOLUME_FORMULA['hex20'] = VOLUME_FORMULA['hex8']
|
||
|
|
||
|
# define mapping of the external faces of an element
|
||
|
FACE_MAPPING = dict()
|
||
|
FACE_MAPPING['hex8'] = [('quad4', (0, 1, 5, 4)), ('quad4', (1, 2, 6, 5)),
|
||
|
('quad4', (2, 3, 7, 6)), ('quad4', (3, 0, 4, 7)),
|
||
|
('quad4', (0, 3, 2, 1)), ('quad4', (4, 5, 6, 7))]
|
||
|
FACE_MAPPING['hex20'] = [('quad8', (0, 1, 5, 4, 8, 13, 16, 12)),
|
||
|
('quad8', (1, 2, 6, 5, 9, 14, 17, 13)),
|
||
|
('quad8', (2, 3, 7, 6, 10, 15, 18, 14)),
|
||
|
('quad8', (3, 0, 4, 7, 11, 12, 19, 15)),
|
||
|
('quad8', (0, 3, 2, 1, 11, 10, 9, 8)),
|
||
|
('quad8', (4, 5, 6, 7, 16, 17, 18, 19))]
|
||
|
FACE_MAPPING['tet4'] = [('tri3', (0, 1, 3)), ('tri3', (1, 2, 3)),
|
||
|
('tri3', (2, 0, 3)), ('tri3', (0, 2, 1))]
|
||
|
FACE_MAPPING['tet10'] = [('tri6', (0, 1, 3, 4, 8, 7)),
|
||
|
('tri6', (1, 2, 3, 5, 9, 8)),
|
||
|
('tri6', (2, 0, 3, 6, 7, 9)),
|
||
|
('tri6', (0, 2, 1, 6, 5, 4))]
|
||
|
FACE_MAPPING['wedge6'] = [('quad4', (0, 1, 4, 3)), ('quad4', (1, 2, 5, 4)),
|
||
|
('quad4', (2, 0, 3, 5)), ('tri3', (0, 2, 1)),
|
||
|
('tri3', (3, 4, 5))]
|
||
|
FACE_MAPPING['wedge12'] = [('quad6', (0, 1, 4, 3, 6, 9)),
|
||
|
('quad6', (1, 2, 5, 4, 7, 10)),
|
||
|
('quad6', (2, 0, 3, 5, 8, 11)),
|
||
|
('tri6', (0, 2, 1, 8, 7, 6)),
|
||
|
('tri6', (3, 4, 5, 11, 10, 9))]
|
||
|
FACE_MAPPING['wedge15'] = [('quad8', (0, 1, 4, 3, 6, 10, 12, 9)),
|
||
|
('quad8', (1, 2, 5, 4, 7, 11, 13, 10)),
|
||
|
('quad8', (2, 0, 3, 5, 8, 9, 14, 11)),
|
||
|
('tri6', (0, 2, 1, 8, 7, 6)),
|
||
|
('tri6', (3, 4, 5, 12, 13, 14))]
|
||
|
FACE_MAPPING['wedge16'] = FACE_MAPPING['wedge15']
|
||
|
FACE_MAPPING['tri3'] = [('line2', (0, 1)), ('line2', (1, 2)),
|
||
|
('line2', (2, 0))]
|
||
|
FACE_MAPPING['tri6'] = [('line3', (0, 1, 3)), ('line3', (1, 2, 4)),
|
||
|
('line3', (2, 0, 5))]
|
||
|
FACE_MAPPING['quad4'] = [('line2', (0, 1)), ('line2', (1, 2)),
|
||
|
('line2', (2, 3)), ('line2', (3, 0))]
|
||
|
FACE_MAPPING['quad6'] = [('line3', (0, 1, 4)), ('line2', (1, 2)),
|
||
|
('line3', (2, 5, 3)), ('line2', (3, 0))]
|
||
|
FACE_MAPPING['quad8'] = [('line3', (0, 1, 4)), ('line3', (1, 2, 5)),
|
||
|
('line3', (2, 3, 6)), ('line3', (3, 0, 7))]
|
||
|
FACE_MAPPING['line2'] = [('point', tuple([0])), ('point', tuple([1]))]
|
||
|
FACE_MAPPING['line3'] = FACE_MAPPING['line2']
|
||
|
FACE_MAPPING['point'] = []
|
||
|
|
||
|
# define all standard element types
|
||
|
STANDARD_ELEMENT_TYPES = set(FACE_MAPPING.keys())
|
||
|
|
||
|
# define a connectivity permutation which inverts the element
|
||
|
# for 2d elements, this flips the element normal
|
||
|
# for 1d elements, this changes the direction
|
||
|
INVERTED_CONNECTIVITY = dict()
|
||
|
INVERTED_CONNECTIVITY['hex8'] = (4, 5, 6, 7, 0, 1, 2, 3)
|
||
|
INVERTED_CONNECTIVITY['hex20'] = (4, 5, 6, 7, 0, 1, 2, 3, 16, 17, 18, 19,
|
||
|
8, 9, 10, 11, 12, 13, 14, 15)
|
||
|
INVERTED_CONNECTIVITY['tet4'] = (0, 2, 1, 3)
|
||
|
INVERTED_CONNECTIVITY['tet10'] = (0, 2, 1, 3, 6, 5, 4, 7, 9, 8)
|
||
|
INVERTED_CONNECTIVITY['wedge6'] = (3, 4, 5, 0, 1, 2)
|
||
|
INVERTED_CONNECTIVITY['wedge12'] = (3, 4, 5, 0, 1, 2, 9, 10, 11, 6, 7, 8)
|
||
|
INVERTED_CONNECTIVITY['wedge15'] = (3, 4, 5, 0, 1, 2, 12, 13, 14, 6, 7, 8,
|
||
|
9, 10, 11)
|
||
|
INVERTED_CONNECTIVITY['wedge16'] = (3, 4, 5, 0, 1, 2, 12, 13, 14, 6, 7, 8,
|
||
|
9, 10, 11, 15)
|
||
|
INVERTED_CONNECTIVITY['tri3'] = (0, 2, 1)
|
||
|
INVERTED_CONNECTIVITY['tri6'] = (0, 2, 1, 5, 4, 3)
|
||
|
INVERTED_CONNECTIVITY['quad4'] = (0, 3, 2, 1)
|
||
|
INVERTED_CONNECTIVITY['quad6'] = (3, 2, 1, 0, 5, 4)
|
||
|
INVERTED_CONNECTIVITY['quad8'] = (0, 3, 2, 1, 7, 6, 5, 4)
|
||
|
INVERTED_CONNECTIVITY['line2'] = (1, 0)
|
||
|
INVERTED_CONNECTIVITY['line3'] = (1, 0, 2)
|
||
|
INVERTED_CONNECTIVITY['point'] = tuple([0])
|
||
|
|
||
|
# define a connectivity permutation which rotates the given 2d face
|
||
|
ROTATED_CONNECTIVITY = dict()
|
||
|
ROTATED_CONNECTIVITY['quad4'] = (3, 0, 1, 2)
|
||
|
ROTATED_CONNECTIVITY['quad8'] = (3, 0, 1, 2, 7, 4, 5, 6)
|
||
|
ROTATED_CONNECTIVITY['tri3'] = (2, 0, 1)
|
||
|
ROTATED_CONNECTIVITY['tri6'] = (2, 0, 1, 5, 3, 4)
|
||
|
|
||
|
# define the topological dimension of each element
|
||
|
DIMENSION = dict()
|
||
|
DIMENSION['point'] = 0
|
||
|
DIMENSION['line2'] = 1
|
||
|
DIMENSION['line3'] = 1
|
||
|
DIMENSION['tri3'] = 2
|
||
|
DIMENSION['tri6'] = 2
|
||
|
DIMENSION['quad4'] = 2
|
||
|
DIMENSION['quad6'] = 2
|
||
|
DIMENSION['quad8'] = 2
|
||
|
DIMENSION['hex8'] = 3
|
||
|
DIMENSION['hex20'] = 3
|
||
|
DIMENSION['tet4'] = 3
|
||
|
DIMENSION['tet10'] = 3
|
||
|
DIMENSION['wedge6'] = 3
|
||
|
DIMENSION['wedge12'] = 3
|
||
|
DIMENSION['wedge15'] = 3
|
||
|
DIMENSION['wedge16'] = 3
|
||
|
|
||
|
# define the number of nodes per element
|
||
|
NODES_PER_ELEMENT = dict(
|
||
|
(key, len(value))
|
||
|
for key, value in list(INVERTED_CONNECTIVITY.items()))
|
||
|
|
||
|
# define how to triangulate faces of 3D elements
|
||
|
TRIANGULATED_FACES = dict()
|
||
|
TRIANGULATED_FACES['tri3'] = [(0, 1, 2)]
|
||
|
TRIANGULATED_FACES['tri6'] = [(0, 3, 5), (1, 4, 3), (2, 5, 4), (3, 4, 5)]
|
||
|
TRIANGULATED_FACES['quad4'] = [(0, 1, (0, 1, 2, 3)), (1, 2, (0, 1, 2, 3)),
|
||
|
(2, 3, (0, 1, 2, 3)), (3, 0, (0, 1, 2, 3))]
|
||
|
TRIANGULATED_FACES['quad6'] = [(0, 4, (4, 5)), (4, 1, (4, 5)),
|
||
|
(1, 2, (4, 5)), (2, 5, (4, 5)),
|
||
|
(5, 3, (4, 5)), (3, 0, (4, 5))]
|
||
|
TRIANGULATED_FACES['quad8'] = [(4, 7, 0), (5, 4, 1), (6, 5, 2), (7, 6, 3),
|
||
|
(7, 4, (4, 5, 6, 7)), (4, 5, (4, 5, 6, 7)),
|
||
|
(5, 6, (4, 5, 6, 7)), (6, 7, (4, 5, 6, 7))]
|
||
|
|
||
|
# define formulas for converting between element types
|
||
|
ELEMENT_CONVERSIONS = dict()
|
||
|
ELEMENT_CONVERSIONS['hex8'] = dict()
|
||
|
ELEMENT_CONVERSIONS['hex8']['hex20'] = [[
|
||
|
0, 1, 2, 3, 4, 5, 6, 7, (0, 1), (1, 2), (2, 3), (0, 3), (0, 4), (1, 5),
|
||
|
(2, 6), (3, 7), (4, 5), (5, 6), (6, 7), (4, 7)
|
||
|
]]
|
||
|
ELEMENT_CONVERSIONS['hex20'] = dict()
|
||
|
ELEMENT_CONVERSIONS['hex20']['hex8'] = [list(range(8))]
|
||
|
ELEMENT_CONVERSIONS['tet4'] = dict()
|
||
|
ELEMENT_CONVERSIONS['tet4']['tet10'] = [[
|
||
|
0, 1, 2, 3, (0, 1), (1, 2), (0, 2), (0, 3), (1, 3), (2, 3)
|
||
|
]]
|
||
|
ELEMENT_CONVERSIONS['tet10'] = dict()
|
||
|
ELEMENT_CONVERSIONS['tet10']['tet4'] = [list(range(4))]
|
||
|
ELEMENT_CONVERSIONS['wedge6'] = dict()
|
||
|
ELEMENT_CONVERSIONS['wedge6']['wedge15'] = [[
|
||
|
0, 1, 2, 3, 4, 5, (0, 1), (1, 2), (0, 2), (0, 3), (1, 4), (2, 5),
|
||
|
(3, 4), (4, 5), (3, 5)
|
||
|
]]
|
||
|
ELEMENT_CONVERSIONS['wedge6']['wedge16'] = [[
|
||
|
0, 1, 2, 3, 4, 5, (0, 1), (1, 2), (0, 2), (0, 3), (1, 4), (2, 5),
|
||
|
(3, 4), (4, 5), (3, 5), (0, 1, 2, 3, 4, 5)
|
||
|
]]
|
||
|
ELEMENT_CONVERSIONS['wedge12'] = dict()
|
||
|
ELEMENT_CONVERSIONS['wedge12']['wedge6'] = [list(range(6))]
|
||
|
ELEMENT_CONVERSIONS['wedge12']['wedge15'] = [[
|
||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, (0, 3), (1, 4), (2, 5), 9, 10, 11
|
||
|
]]
|
||
|
ELEMENT_CONVERSIONS['wedge15'] = dict()
|
||
|
ELEMENT_CONVERSIONS['wedge15']['wedge6'] = [list(range(6))]
|
||
|
ELEMENT_CONVERSIONS['wedge15']['wedge16'] = [[
|
||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, (0, 1, 2, 3, 4, 5)
|
||
|
]]
|
||
|
ELEMENT_CONVERSIONS['wedge16'] = dict()
|
||
|
ELEMENT_CONVERSIONS['wedge16']['wedge6'] = [list(range(6))]
|
||
|
ELEMENT_CONVERSIONS['wedge16']['wedge15'] = [list(range(15))]
|
||
|
|
||
|
# define the order of each element
|
||
|
ELEMENT_ORDER = dict()
|
||
|
ELEMENT_ORDER['hex8'] = 1
|
||
|
ELEMENT_ORDER['hex20'] = 2
|
||
|
ELEMENT_ORDER['tet4'] = 1
|
||
|
ELEMENT_ORDER['tet10'] = 2
|
||
|
ELEMENT_ORDER['wedge6'] = 1
|
||
|
ELEMENT_ORDER['wedge12'] = 1
|
||
|
ELEMENT_ORDER['wedge15'] = 2
|
||
|
ELEMENT_ORDER['wedge16'] = 2
|
||
|
ELEMENT_ORDER['tri3'] = 1
|
||
|
ELEMENT_ORDER['tri6'] = 2
|
||
|
ELEMENT_ORDER['quad4'] = 1
|
||
|
ELEMENT_ORDER['quad6'] = 1
|
||
|
ELEMENT_ORDER['quad8'] = 2
|
||
|
ELEMENT_ORDER['line2'] = 1
|
||
|
ELEMENT_ORDER['line3'] = 2
|
||
|
ELEMENT_ORDER['point'] = 1
|
||
|
|
||
|
# define components of multi-component fields
|
||
|
MULTI_COMPONENT_FIELD_SUBSCRIPTS = dict()
|
||
|
MULTI_COMPONENT_FIELD_SUBSCRIPTS['vector'] = ('x', 'y', 'z')
|
||
|
MULTI_COMPONENT_FIELD_SUBSCRIPTS['symmetric_3x3_tensor'] = ('xx', 'yy',
|
||
|
'zz', 'xy',
|
||
|
'yz', 'zx')
|
||
|
MULTI_COMPONENT_FIELD_SUBSCRIPTS['full_3x3_tensor'] = ('xx', 'yy', 'zz',
|
||
|
'xy', 'yz', 'zx',
|
||
|
'yx', 'zy', 'xz')
|
||
|
ALL_MULTI_COMPONENT_FIELD_SUBSCRIPTS = set(
|
||
|
itertools.chain(*list(MULTI_COMPONENT_FIELD_SUBSCRIPTS.values())))
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Initialize the model."""
|
||
|
# (only) the first time this module is used, show an info banner
|
||
|
global SHOW_BANNER
|
||
|
if SHOW_BANNER:
|
||
|
print(('\n\nYou are using Exomerge v%s -- A lightweight Python '
|
||
|
'interface for manipulating\nExodusII files. (Python-3 version)' % (VERSION)))
|
||
|
# print out the exodus banner after formatting it to fit within
|
||
|
# 79 characters
|
||
|
exodus_text = exodus.EXODUS_PY_COPYRIGHT
|
||
|
exodus_text = exodus_text.strip().replace('\n', ' ')
|
||
|
exodus_text = exodus_text.replace('. ', '. ')
|
||
|
exodus_text = exodus_text.replace('. ', '. ')
|
||
|
exodus_text = textwrap.fill(exodus_text, width=79)
|
||
|
print(('\n%s\n' % exodus_text))
|
||
|
SHOW_BANNER = False
|
||
|
# list of [x, y, z] coordinates for each node
|
||
|
self.nodes = []
|
||
|
# self.node_fields[name] = values
|
||
|
# with values[timestep_index][node_index]
|
||
|
self.node_fields = {}
|
||
|
# self.global_variables[name] = values
|
||
|
# with values[timestep_index]
|
||
|
self.global_variables = {}
|
||
|
# self.element_blocks[id] = [name, info, connectivity, fields]
|
||
|
# with name = string name of element block or '' if unnamed
|
||
|
# with info = ['hex8', elements, 8, 0]
|
||
|
# with connectivity a shallow list of connectivity
|
||
|
# with fields['eqps'] = values
|
||
|
# with values[timestep][local_element_index]
|
||
|
self.element_blocks = {}
|
||
|
# self.side_sets[side_set_id] = [name, members, fields]
|
||
|
# with name = string name of side set or '' if unnamed
|
||
|
# with members = list of (element_block_id, element_index, side_index)
|
||
|
# with fields[name] = values
|
||
|
# with values[timestep_index][local_member_index]
|
||
|
self.side_sets = {}
|
||
|
# self.node_sets[node_set_id] = [name, members, fields]
|
||
|
# with name = string name of node set or '' if unnamed
|
||
|
# with members = list of node indices
|
||
|
# with fields[name] = values
|
||
|
# with values[timestep_index][local_member_index]
|
||
|
self.node_sets = {}
|
||
|
# list of timesteps
|
||
|
self.timesteps = []
|
||
|
# list of info records
|
||
|
self.info_records = []
|
||
|
# list of qa records
|
||
|
self.qa_records = []
|
||
|
# title of the database
|
||
|
self.title = None
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
"""
|
||
|
Try to find the given attribute.
|
||
|
|
||
|
This is a special Python method which gets called if the attribute
|
||
|
cannot be found. We use to to suggest similar names in the case the
|
||
|
user made a typo.
|
||
|
|
||
|
We also use this to alias the plural forms of the delete functions.
|
||
|
|
||
|
"""
|
||
|
# for special methods, use default behavior
|
||
|
if name[:2] == '__':
|
||
|
raise AttributeError
|
||
|
# get non-special function names
|
||
|
names = [x for x in dir(self.__class__) if x[:2] != '__']
|
||
|
# if the name appears to be singular, search for the plural version
|
||
|
if not name.endswith('s'):
|
||
|
trial = name + 's'
|
||
|
if trial in names:
|
||
|
return getattr(self, trial)
|
||
|
# if the name appears to be plural, search for the singular version
|
||
|
if name.endswith('s'):
|
||
|
trial = name[:-1]
|
||
|
if not trial.endswith('s') and trial in names:
|
||
|
return getattr(self, trial)
|
||
|
# we can't find the name, so find names close to it to offer as
|
||
|
# suggestions
|
||
|
# filter names by closeness
|
||
|
names = dict(
|
||
|
(x, difflib.SequenceMatcher(None, x, name).ratio()) for x in names)
|
||
|
sorted_names = sorted(iter(names.items()),
|
||
|
key=operator.itemgetter(1),
|
||
|
reverse=True)
|
||
|
# get the closest 5 matching function names
|
||
|
closest_names = [x[0] for x in sorted_names[0:5]]
|
||
|
# in an interactive shell, just issue a warning, else
|
||
|
# issue an error an exit
|
||
|
if self._is_interactive():
|
||
|
call = self._warning
|
||
|
else:
|
||
|
call = self._error
|
||
|
call(
|
||
|
'Function not found.',
|
||
|
'The function "%s" does not exist. Perhaps you meant to '
|
||
|
'call one of the following functions:\n\n%s' %
|
||
|
(name, '\n'.join(closest_names)))
|
||
|
|
||
|
@staticmethod
|
||
|
def _is_interactive():
|
||
|
"""Return True if we're in an interactive shell."""
|
||
|
import __main__ as main
|
||
|
return not hasattr(main, '__file__')
|
||
|
|
||
|
@staticmethod
|
||
|
def _assert(condition):
|
||
|
"""
|
||
|
Raise a 'ValueError' if the given condition is not true.
|
||
|
|
||
|
Unlike the assert builtin function, this checks the condition
|
||
|
regardless of the state of '__debug__'.
|
||
|
|
||
|
"""
|
||
|
if not condition:
|
||
|
raise ValueError
|
||
|
|
||
|
@staticmethod
|
||
|
def _cubic_interpolation(x, x0, x1, x2, x3):
|
||
|
"""
|
||
|
Return proportions using the cubic interpolation formula.
|
||
|
|
||
|
Find the proportions of 'y0', 'y1', 'y2', 'y3' to take to find 'y(x)'
|
||
|
for 'x1 <= x <= x2'. This requires 'x0 < x1 < x2 < x3'.
|
||
|
|
||
|
Example:
|
||
|
>>> model._cubic_interpolation(1.71, 0.0, 1.0, 2.0, 3.0)
|
||
|
[-0.0298555, 0.2766165, 0.8263335, -0.0730945]
|
||
|
|
||
|
"""
|
||
|
assert x0 < x1 < x2 < x3
|
||
|
assert x1 <= x <= x2
|
||
|
values = []
|
||
|
# proportion of y0
|
||
|
values.append(((x - x1) * (x - x2)**2) / ((x0 - x2) * (x1 - x2)**2))
|
||
|
# proportion of y1
|
||
|
values.append(-(((x - x2) *
|
||
|
(-(x * x1 * (x1 + 3 * x2)) - x1 *
|
||
|
(x1**2 - 4 * x1 * x2 + x2**2) + x**2 *
|
||
|
(x1 + x2 - 2 * x3) + x2 * (-3 * x1 + x2) * x3 + x *
|
||
|
(3 * x1 + x2) * x3)) / ((x1 - x2)**3 * (x1 - x3))))
|
||
|
# proportion of y2
|
||
|
values.append(
|
||
|
((x - x1) *
|
||
|
(x0 * x1 * (x1 - 3 * x2) + x**2 * (-2 * x0 + x1 + x2) - x2 *
|
||
|
(x1**2 - 4 * x1 * x2 + x2**2) + x *
|
||
|
(-(x2 * (3 * x1 + x2)) + x0 * (x1 + 3 * x2)))) / ((x0 - x2) *
|
||
|
(-x1 + x2)**3))
|
||
|
# proportion of y3
|
||
|
values.append(((x - x1)**2 * (x - x2)) / ((x1 - x2)**2 * (x3 - x1)))
|
||
|
return values
|
||
|
|
||
|
def _new_element_block_id(self):
|
||
|
"""Return an element block id which is not used in the model."""
|
||
|
id_ = 1
|
||
|
while self.element_block_exists(id_):
|
||
|
id_ += 1
|
||
|
return id_
|
||
|
|
||
|
def _new_side_set_id(self):
|
||
|
"""Return a side set id which is not used in the model."""
|
||
|
id_ = 1
|
||
|
while self.side_set_exists(id_):
|
||
|
id_ += 1
|
||
|
return id_
|
||
|
|
||
|
def _new_node_set_id(self):
|
||
|
"""Return a node set id which is not used in the model."""
|
||
|
id_ = 1
|
||
|
while self.node_set_exists(id_):
|
||
|
id_ += 1
|
||
|
return id_
|
||
|
|
||
|
def _new_element_field_name(self, quantity=1):
|
||
|
"""
|
||
|
Return an element field name which is not used in the model.
|
||
|
|
||
|
If 'quantity=1', this will return a single string. Else, it will
|
||
|
return a list of strings.
|
||
|
"""
|
||
|
id_ = 1
|
||
|
names = []
|
||
|
for _ in range(quantity):
|
||
|
name = 'temp%d' % id_
|
||
|
while name in self.get_element_field_names():
|
||
|
id_ += 1
|
||
|
name = 'temp%d' % id_
|
||
|
names.append(name)
|
||
|
id_ += 1
|
||
|
if quantity == 1:
|
||
|
return names[0]
|
||
|
return names
|
||
|
|
||
|
def _new_node_field_name(self):
|
||
|
"""Return a node field name which is not used in the model."""
|
||
|
id_ = 1
|
||
|
name = 'temp%d' % id_
|
||
|
while name in self.get_node_field_names():
|
||
|
id_ += 1
|
||
|
name = 'temp%d' % id_
|
||
|
return name
|
||
|
|
||
|
def _new_side_set_field_name(self):
|
||
|
"""Return a side set field name which is not used in the model."""
|
||
|
id_ = 1
|
||
|
name = 'temp%d' % id_
|
||
|
while name in self.get_side_set_field_names():
|
||
|
id_ += 1
|
||
|
name = 'temp%d' % id_
|
||
|
return name
|
||
|
|
||
|
def _new_node_set_field_name(self):
|
||
|
"""Return a node set field name which is not used in the model."""
|
||
|
id_ = 1
|
||
|
name = 'temp%d' % id_
|
||
|
while name in self.get_node_set_field_names():
|
||
|
id_ += 1
|
||
|
name = 'temp%d' % id_
|
||
|
return name
|
||
|
|
||
|
def _delete_elements(self, element_block_id, element_indices):
|
||
|
"""
|
||
|
Delete the specified elements from the given element block.
|
||
|
|
||
|
This will also delete all references to those elements in element
|
||
|
fields, side sets, and side set fields. This will also delete nodes
|
||
|
which were used by those elements which are no longer used.
|
||
|
|
||
|
Element indices are local to that element block.
|
||
|
|
||
|
"""
|
||
|
# validate input
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
self._input_check(element_indices, [list, 0, int])
|
||
|
# get element block information
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
# if not deleting any indices, we're done
|
||
|
if not element_indices:
|
||
|
return
|
||
|
# error if values are outside valid range
|
||
|
max_element_index = max(element_indices)
|
||
|
if max_element_index >= element_count:
|
||
|
self._error(
|
||
|
'Invalid element indices',
|
||
|
'The element index list contains invalid entries. '
|
||
|
'There are only %d elements in this element block and '
|
||
|
'the list contains a reference to index %d.' %
|
||
|
(max_element_index))
|
||
|
# select valid elements
|
||
|
indices_to_delete = set()
|
||
|
invalid_indices = []
|
||
|
duplicate_indices = []
|
||
|
for x in element_indices:
|
||
|
if 0 <= x < element_count:
|
||
|
if x not in indices_to_delete:
|
||
|
indices_to_delete.add(x)
|
||
|
else:
|
||
|
duplicate_indices.append(x)
|
||
|
else:
|
||
|
invalid_indices.append(x)
|
||
|
# warn about duplicate indices
|
||
|
if duplicate_indices:
|
||
|
self._warning(
|
||
|
'Duplicate element indices',
|
||
|
'The element index list contains duplicate '
|
||
|
'indices. Elements can only be deleted once so '
|
||
|
'duplicates will be ignored. There were %d '
|
||
|
'duplicates.' % (len(duplicate_indices)))
|
||
|
del duplicate_indices
|
||
|
# error if invalid indices encountered
|
||
|
if invalid_indices:
|
||
|
self._error(
|
||
|
'Invalid element indices',
|
||
|
'The element index list contains invalid entries. '
|
||
|
'There were a total of %d invalid indices.' %
|
||
|
len(invalid_indices))
|
||
|
del invalid_indices
|
||
|
# create list of nodes used by elements to delete
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
used_nodes = []
|
||
|
for element_index in indices_to_delete:
|
||
|
used_nodes.extend(
|
||
|
connectivity[element_index *
|
||
|
nodes_per_element:(element_index + 1) *
|
||
|
nodes_per_element])
|
||
|
used_nodes = set(used_nodes)
|
||
|
# remove references to these elements within each side set
|
||
|
for id_ in self.get_side_set_ids():
|
||
|
# look for members to delete
|
||
|
members = self.get_side_set_members(id_)
|
||
|
members_to_delete = [
|
||
|
x for x in members
|
||
|
if (x[0] == element_block_id and x[1] in indices_to_delete)
|
||
|
]
|
||
|
# delete those members (if any exist)
|
||
|
if members_to_delete:
|
||
|
self._delete_side_set_members(id_, members_to_delete)
|
||
|
# create translation list
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
remaining_indices = sorted(
|
||
|
set(range(element_count)) - set(indices_to_delete))
|
||
|
# create element map
|
||
|
# old element i is new element new_index[i]
|
||
|
new_index = [False] * element_count
|
||
|
for i, x in enumerate(remaining_indices):
|
||
|
new_index[x] = i
|
||
|
# delete elements from element fields
|
||
|
del indices_to_delete
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
for this_field in list(fields.values()):
|
||
|
this_field[:] = [[values[x] for x in remaining_indices]
|
||
|
for values in this_field]
|
||
|
# delete elements from the block
|
||
|
new_connectivity = []
|
||
|
for element_index in remaining_indices:
|
||
|
new_connectivity.extend(
|
||
|
connectivity[element_index *
|
||
|
nodes_per_element:(element_index + 1) *
|
||
|
nodes_per_element])
|
||
|
# set the new connectivity
|
||
|
self.get_connectivity(element_block_id)[:] = new_connectivity
|
||
|
# set the number of elements
|
||
|
self.element_blocks[element_block_id][1][1] = len(remaining_indices)
|
||
|
# change side set element numbering
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
new_members = []
|
||
|
for member in members:
|
||
|
if member[0] == element_block_id:
|
||
|
new_members.append(
|
||
|
(member[0], new_index[member[1]], member[2]))
|
||
|
else:
|
||
|
new_members.append(member)
|
||
|
members[:] = new_members
|
||
|
# make sure we didn't mess anything up
|
||
|
# find nodes which are not used by any element
|
||
|
abandoned_nodes = set(self._get_unreferenced_nodes())
|
||
|
# find nodes to delete
|
||
|
nodes_to_delete = sorted(abandoned_nodes & used_nodes)
|
||
|
# delete those nodes
|
||
|
self._delete_nodes(nodes_to_delete)
|
||
|
|
||
|
def threshold_element_blocks(self,
|
||
|
expression,
|
||
|
element_block_ids='all',
|
||
|
timestep='last',
|
||
|
new_element_block_id=None):
|
||
|
"""
|
||
|
Delete elements which do not satisfy a condition.
|
||
|
|
||
|
The expression can contain element field values.
|
||
|
|
||
|
If 'new_element_block_id' is given, all elements which don't pass
|
||
|
the threshold will be moved into that element block. Else, they are
|
||
|
deleted.
|
||
|
|
||
|
Example:
|
||
|
>>> model.threshold_element_block('all', 'eqps >= 0.01')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
if new_element_block_id is not None:
|
||
|
if self.element_block_exists(new_element_block_id):
|
||
|
self._exists_error(new_element_block_id, 'element block')
|
||
|
# create a temporary field name on these elements
|
||
|
name = self._new_element_field_name()
|
||
|
# evaluate the expression
|
||
|
self.calculate_element_field('%s = %s' % (name, expression),
|
||
|
element_block_ids=element_block_ids)
|
||
|
# go through each element block and delete elements
|
||
|
for id_ in element_block_ids:
|
||
|
values = self.get_element_field_values(name,
|
||
|
element_block_id=id_,
|
||
|
timestep=timestep)
|
||
|
# if we're saving elements, duplicate the block, and delete the
|
||
|
# ones we're keeping
|
||
|
if new_element_block_id is not None:
|
||
|
indices_to_keep = [i for i, x in enumerate(values) if x]
|
||
|
temporary_element_block_id = self._new_element_block_id()
|
||
|
self.duplicate_element_block(id_,
|
||
|
temporary_element_block_id,
|
||
|
duplicate_nodes=False)
|
||
|
self._delete_elements(temporary_element_block_id,
|
||
|
indices_to_keep)
|
||
|
if temporary_element_block_id != new_element_block_id:
|
||
|
if self.element_block_exists(new_element_block_id):
|
||
|
self.combine_element_blocks(
|
||
|
[temporary_element_block_id, new_element_block_id],
|
||
|
new_element_block_id)
|
||
|
else:
|
||
|
self.rename_element_block(temporary_element_block_id,
|
||
|
new_element_block_id)
|
||
|
# delete elements which don't meet the threshold
|
||
|
indices_to_delete = [i for i, x in enumerate(values) if not x]
|
||
|
self._delete_elements(id_, indices_to_delete)
|
||
|
# delete the temporary element field
|
||
|
self.delete_element_field(name, element_block_ids=element_block_ids)
|
||
|
if new_element_block_id is not None:
|
||
|
self.delete_element_field(name,
|
||
|
element_block_ids=new_element_block_id)
|
||
|
|
||
|
def _get_list_ids(self, thing):
|
||
|
"""
|
||
|
Return a list of list ids found within a variable.
|
||
|
|
||
|
This handles lists, dictionaries, and nested combinations of these.
|
||
|
Other objects are ignored.
|
||
|
|
||
|
This method is used to check aliasing of lists.
|
||
|
|
||
|
"""
|
||
|
ids = []
|
||
|
if isinstance(thing, list):
|
||
|
ids.append(id(thing))
|
||
|
if isinstance(thing, list) or isinstance(thing[0], dict):
|
||
|
for other_thing in thing:
|
||
|
ids.extend(self._get_list_ids(other_thing))
|
||
|
return ids
|
||
|
|
||
|
if isinstance(thing, dict):
|
||
|
for other_thing in list(thing.values()):
|
||
|
ids.extend(self._get_list_ids(other_thing))
|
||
|
return ids
|
||
|
|
||
|
def _verify(self, allow_aliased_lists=True):
|
||
|
"""
|
||
|
Verify model information is valid and arrays are appropriately sized.
|
||
|
|
||
|
If there is a problem somewhere, this method will throw an error
|
||
|
message and exit.
|
||
|
|
||
|
"""
|
||
|
timestep_count = len(self.timesteps)
|
||
|
try:
|
||
|
# verify self.node_fields
|
||
|
node_count = len(self.nodes)
|
||
|
for all_values in list(self.node_fields.values()):
|
||
|
self._assert(len(all_values) == timestep_count)
|
||
|
for values in all_values:
|
||
|
self._assert(len(values) == node_count)
|
||
|
# verify self.element_blocks
|
||
|
for _, info, connectivity, fields in list(
|
||
|
self.element_blocks.values()):
|
||
|
self._assert(len(info) == 4)
|
||
|
self._assert(len(connectivity) == info[1] * info[2])
|
||
|
if connectivity:
|
||
|
self._assert(min(connectivity) >= 0)
|
||
|
self._assert(max(connectivity) < node_count)
|
||
|
for _, all_values in list(fields.items()):
|
||
|
self._assert(len(all_values) == timestep_count)
|
||
|
for values in all_values:
|
||
|
self._assert(len(values) == info[1])
|
||
|
# verify self.node_sets
|
||
|
for _, members, fields in list(self.node_sets.values()):
|
||
|
member_count = len(members)
|
||
|
if members:
|
||
|
self._assert(min(members) >= 0)
|
||
|
self._assert(max(members) < node_count)
|
||
|
for _, all_values in list(fields.items()):
|
||
|
self._assert(len(all_values) == timestep_count)
|
||
|
for values in all_values:
|
||
|
self._assert(len(values) == member_count)
|
||
|
# verify self.side_sets
|
||
|
element_count = dict(
|
||
|
(id_, info[1])
|
||
|
for id_, (_, info, _, _) in list(self.element_blocks.items()))
|
||
|
for _, members, fields in list(self.side_sets.values()):
|
||
|
member_count = len(members)
|
||
|
if members:
|
||
|
self._assert(min(x[1] for x in members) >= 0)
|
||
|
for id_, element_index, _ in members:
|
||
|
self._assert(element_index < element_count[id_])
|
||
|
for _, all_values in list(fields.items()):
|
||
|
self._assert(len(all_values) == timestep_count)
|
||
|
for values in all_values:
|
||
|
self._assert(len(values) == member_count)
|
||
|
# verify self.global_variables
|
||
|
for _, values in list(self.global_variables.items()):
|
||
|
self._assert(len(values) == timestep_count)
|
||
|
# optionally verify that no lists are aliases of one another
|
||
|
if not allow_aliased_lists:
|
||
|
# hold list ids for later use
|
||
|
list_ids = []
|
||
|
for name, thing in inspect.getmembers(self):
|
||
|
if not name[0].isalpha() or not name[0].islower():
|
||
|
continue
|
||
|
if isinstance(thing, (dict, list)):
|
||
|
list_ids.extend(self._get_list_ids(thing))
|
||
|
# ensure none are copies
|
||
|
unique_list_ids = list(set(list_ids))
|
||
|
self._assert(len(unique_list_ids) == len(list_ids))
|
||
|
except ValueError:
|
||
|
# get the code line the assertion failed on
|
||
|
_, _, trace = sys.exc_info()
|
||
|
error_line = traceback.extract_tb(trace)[0][3]
|
||
|
if error_line.startswith('self._assert('):
|
||
|
error_line = error_line[13:-1]
|
||
|
error_line_number = traceback.extract_tb(trace)[0][1]
|
||
|
# write out the error/warning message
|
||
|
self._error(
|
||
|
'Corrupted database information.',
|
||
|
'The finite element model information failed a validity '
|
||
|
'check. The following assertion failed:\n'
|
||
|
'\n'
|
||
|
'[%d] %s\n'
|
||
|
'\n'
|
||
|
'This resulted from one of the two scenarios:\n'
|
||
|
'* You changed information in the database '
|
||
|
'inconsistently.\n'
|
||
|
'* There is a bug within the code.\n'
|
||
|
'\n'
|
||
|
'If you believe it is the latter, please contact support '
|
||
|
'with as much information as necessary.\n'
|
||
|
'* Support contact: %s' %
|
||
|
(error_line_number, error_line, CONTACT))
|
||
|
exit(1)
|
||
|
|
||
|
@staticmethod
|
||
|
def _print_message(messages, width=79):
|
||
|
"""
|
||
|
Print out a nicely formatted message which fits in a specified width.
|
||
|
|
||
|
Example:
|
||
|
>>> exomerge.ExodusModel._print_message(
|
||
|
... [("ERROR: ", "I'm sorry, Dave"),
|
||
|
... ("Message: ", "I'm afraid I can't let you do that.")],
|
||
|
... width=30)
|
||
|
|
||
|
***********************************
|
||
|
* *
|
||
|
* ERROR: I'm sorry, Dave. *
|
||
|
* *
|
||
|
* Message: I'm afraid I can't *
|
||
|
* let you do that. *
|
||
|
* *
|
||
|
***********************************
|
||
|
|
||
|
"""
|
||
|
# format messages
|
||
|
if not isinstance(messages, list):
|
||
|
messages = [messages]
|
||
|
for i, message in enumerate(messages):
|
||
|
if not isinstance(message, tuple):
|
||
|
messages[i] = ('', message)
|
||
|
# store the message width
|
||
|
max_header_width = max(len(x[0]) for x in messages)
|
||
|
width = max(width, max_header_width + 20 + 6)
|
||
|
# print the top frame after a blank line
|
||
|
print('')
|
||
|
print(('*' * width))
|
||
|
print(('*' + (' ' * (width - 2)) + '*'))
|
||
|
# process each message
|
||
|
# * Header: Text *
|
||
|
for message in messages:
|
||
|
header = message[0]
|
||
|
# format text into broken lines
|
||
|
text = ''
|
||
|
for line in message[1].split('\n'):
|
||
|
text += textwrap.fill(line, width=width - 6 - len(header))
|
||
|
text += '\n'
|
||
|
# remove trailing newlines
|
||
|
while text and text[-1] == '\n':
|
||
|
text = text[:-1]
|
||
|
# break into a list
|
||
|
text = text.split('\n')
|
||
|
# process each line
|
||
|
for i, txt in enumerate(text):
|
||
|
if i == 0:
|
||
|
line = '%s%s' % (header, txt)
|
||
|
else:
|
||
|
line = '%s%s' % (' ' * len(header), txt)
|
||
|
if len(line) < width - 6:
|
||
|
line += ' ' * (width - 6 - len(line))
|
||
|
print(('* %s *' % (line)))
|
||
|
print(('*' + (' ' * (width - 2)) + '*'))
|
||
|
# print the bottom frame
|
||
|
print(('*' * width))
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_trace(omitted_frames=1):
|
||
|
"""Return a compressed stack trace."""
|
||
|
last_file = ''
|
||
|
message = ''
|
||
|
for frame in inspect.stack()[omitted_frames:]:
|
||
|
this_file = frame[1].split('/')[-1]
|
||
|
if this_file != last_file:
|
||
|
message += this_file + ':'
|
||
|
last_file = this_file
|
||
|
else:
|
||
|
message += ' ' * len(last_file) + ' '
|
||
|
message += frame[3] + ':' + str(frame[2])
|
||
|
message += '\n'
|
||
|
return message
|
||
|
|
||
|
def _bug(self, short='Possible bug', detailed='', exit_code=1):
|
||
|
"""
|
||
|
Print out an error message possibly caused by a bug and exit.
|
||
|
|
||
|
As opposed to the _error function, this also outputs support contact
|
||
|
information.
|
||
|
|
||
|
"""
|
||
|
messages = [(' ERROR: ', short)]
|
||
|
if detailed:
|
||
|
messages.append(('Message: ', detailed))
|
||
|
messages.append(
|
||
|
('Support: ', 'This error may have been caused by a bug in the '
|
||
|
'code. If you believe this is the case, please '
|
||
|
'contact support and provide enough information '
|
||
|
'to duplicate the issue.\n\nSupport contact: %s' % CONTACT))
|
||
|
messages.append((' Trace: ', self._get_trace()))
|
||
|
self._print_message(messages)
|
||
|
exit(exit_code)
|
||
|
|
||
|
def _error(self, short='Unspecified error', detailed='', exit_code=1):
|
||
|
"""Print out an error message and exit."""
|
||
|
messages = [(' ERROR: ', short)]
|
||
|
if detailed:
|
||
|
messages.append(('Message: ', detailed))
|
||
|
messages.append((' Trace: ', self._get_trace()))
|
||
|
self._print_message(messages)
|
||
|
exit(exit_code)
|
||
|
|
||
|
def _warning(self, short='Unspecified warning', detailed=''):
|
||
|
"""Print out a warning message, but don't exit."""
|
||
|
messages = [('WARNING: ', short)]
|
||
|
if detailed:
|
||
|
messages.append(('Message: ', detailed))
|
||
|
if EXIT_ON_WARNING:
|
||
|
messages.append((' Trace: ', self._get_trace()))
|
||
|
self._print_message(messages)
|
||
|
if EXIT_ON_WARNING:
|
||
|
exit(1)
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_qa_record():
|
||
|
"""
|
||
|
Return a new qa record for this program.
|
||
|
|
||
|
QA records are stored within the ExodusII file.
|
||
|
|
||
|
"""
|
||
|
now = datetime.datetime.today()
|
||
|
return ('exomerge', VERSION, now.strftime('%Y/%m/%d'),
|
||
|
now.strftime('%H:%M:%S'))
|
||
|
|
||
|
def _ensure_acceptable_id(self, this_id, acceptable_ids, entity=''):
|
||
|
"""Ensure ID is acceptable, or error out."""
|
||
|
if this_id not in acceptable_ids:
|
||
|
if entity == "":
|
||
|
self._error("Reference to undefined entity.")
|
||
|
else:
|
||
|
id_list = string.join([str(x) + ','
|
||
|
for x in acceptable_ids])[:-1]
|
||
|
self._error(
|
||
|
"Reference to undefined %s." % (entity),
|
||
|
"A reference to %s \"%s\" was encountered but is "
|
||
|
"not defined. There are %d defined %ss: %s" %
|
||
|
(entity, str(this_id), len(acceptable_ids), entity,
|
||
|
id_list))
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_matching_strings(string_pattern, list_of_strings):
|
||
|
"""
|
||
|
Return list of string which match the pattern.
|
||
|
|
||
|
The pattern can contain a single wildcard character (*) which
|
||
|
represents any number of characters. No other special regex
|
||
|
characters are allowed.
|
||
|
|
||
|
Example:
|
||
|
>>> model._get_matching_strings('f*x',
|
||
|
... ['fox', 'faux', 'far', 'fx', 'foxes'])
|
||
|
['fox', 'faux', 'fx']
|
||
|
|
||
|
"""
|
||
|
# convert pattern to a regex expression
|
||
|
regex_pattern = '^' + string_pattern + '$'
|
||
|
regex_pattern = regex_pattern.replace('.', r'\.').replace('*', '.*')
|
||
|
matches = [x for x in list_of_strings if re.match(regex_pattern, x)]
|
||
|
return matches
|
||
|
|
||
|
@staticmethod
|
||
|
def _plural(name):
|
||
|
"""
|
||
|
Return the likely plural form of the given word.
|
||
|
|
||
|
Example:
|
||
|
>>> model._plural('rose')
|
||
|
roses
|
||
|
>>> model._plural('bunny')
|
||
|
bunnies
|
||
|
|
||
|
"""
|
||
|
if name[-1] == 'y':
|
||
|
return name[:-1] + 'ies'
|
||
|
return name + 's'
|
||
|
|
||
|
def _format_element_block_id_list(self, id_list, *args, **kwargs):
|
||
|
"""Return a validated list of element block IDs."""
|
||
|
blocks = self.element_blocks
|
||
|
entity = 'element block'
|
||
|
# get element block translations
|
||
|
tuples = [(value[0], key) for key, value in list(blocks.items())
|
||
|
if value[0]]
|
||
|
# ensure element block names are unique
|
||
|
if len(set(x[0] for x in tuples)) != len(tuples):
|
||
|
self._warning(
|
||
|
'Duplicate %s names' % entity,
|
||
|
'At least two %s have the same name. '
|
||
|
'This may cause problems.' % self._plural(entity))
|
||
|
return self._format_id_list(id_list,
|
||
|
sorted(blocks.keys()),
|
||
|
entity=entity,
|
||
|
*args,
|
||
|
translator=dict(tuples),
|
||
|
**kwargs)
|
||
|
|
||
|
def _format_side_set_id_list(self, id_list, *args, **kwargs):
|
||
|
"""Return a validated list of side set IDs."""
|
||
|
ssets = self.side_sets
|
||
|
entity = 'side set'
|
||
|
# get element block translations
|
||
|
tuples = [(value[0], key) for key, value in list(ssets.items())
|
||
|
if value[0]]
|
||
|
# ensure element block names are unique
|
||
|
if len(set(x[0] for x in tuples)) != len(tuples):
|
||
|
self._warning(
|
||
|
'Duplicate %s names' % entity,
|
||
|
'At least two %s have the same name. '
|
||
|
'This may cause problems.' % self._plural(entity))
|
||
|
return self._format_id_list(id_list,
|
||
|
sorted(ssets.keys()),
|
||
|
entity=entity,
|
||
|
*args,
|
||
|
translator=dict(tuples),
|
||
|
**kwargs)
|
||
|
|
||
|
def _format_node_set_id_list(self, id_list, *args, **kwargs):
|
||
|
"""Return a validated list of node set IDs."""
|
||
|
nsets = self.node_sets
|
||
|
entity = 'node set'
|
||
|
# get element block translations
|
||
|
tuples = [(value[0], key) for key, value in list(nsets.items())
|
||
|
if value[0]]
|
||
|
# ensure element block names are unique
|
||
|
if len(set(x[0] for x in tuples)) != len(tuples):
|
||
|
self._warning(
|
||
|
'Duplicate %s names' % entity,
|
||
|
'At least two %s have the same name. '
|
||
|
'This may cause problems.' % self._plural(entity))
|
||
|
return self._format_id_list(id_list,
|
||
|
sorted(nsets.keys()),
|
||
|
entity=entity,
|
||
|
*args,
|
||
|
translator=dict(tuples),
|
||
|
**kwargs)
|
||
|
|
||
|
def _format_id_list(self,
|
||
|
id_list,
|
||
|
acceptable_ids,
|
||
|
entity='',
|
||
|
single=False,
|
||
|
empty_list_okay=True,
|
||
|
translator={}):
|
||
|
"""
|
||
|
Return a validated list of IDs.
|
||
|
|
||
|
If 'single' is 'True', this will error out if zero or more than one id
|
||
|
is returned.
|
||
|
|
||
|
If 'empty_list_okay=False', this will error out if the list is
|
||
|
empty.
|
||
|
|
||
|
If 'translator' is given, it must be a dictionary which converts
|
||
|
strings to ids.
|
||
|
|
||
|
"""
|
||
|
new_id_list = []
|
||
|
if not isinstance(id_list, list):
|
||
|
id_list = [id_list]
|
||
|
for id_ in id_list:
|
||
|
if id_ == 'none':
|
||
|
pass
|
||
|
elif id_ == 'all':
|
||
|
new_id_list.extend(acceptable_ids)
|
||
|
elif id_ == 'first':
|
||
|
if not acceptable_ids:
|
||
|
self._error(
|
||
|
'Empty entity list.',
|
||
|
'The "first" identifier was used to refer to a %s '
|
||
|
'but no %ss exist.' % (entity, entity))
|
||
|
new_id_list.append(acceptable_ids[0])
|
||
|
elif id_ == 'last':
|
||
|
if not acceptable_ids:
|
||
|
self._error(
|
||
|
'Empty entity list.',
|
||
|
'The "last" identifier was used to refer to a %s '
|
||
|
'but no %ss exist.' % (entity, entity))
|
||
|
new_id_list.append(acceptable_ids[-1])
|
||
|
elif id_ == 'auto':
|
||
|
if len(acceptable_ids) != 1:
|
||
|
self._error(
|
||
|
'Ambiguous reference.',
|
||
|
'The "auto" identifier was used to refer to a %s '
|
||
|
'but exactly one %s does not exist. There are %d '
|
||
|
'defined %ss.' %
|
||
|
(entity, entity, len(acceptable_ids), entity))
|
||
|
new_id_list.append(acceptable_ids[0])
|
||
|
elif id_ == 'last_if_any':
|
||
|
if acceptable_ids:
|
||
|
new_id_list.append(acceptable_ids[-1])
|
||
|
elif id_ == 'first_if_any':
|
||
|
if acceptable_ids:
|
||
|
new_id_list.append(acceptable_ids[-1])
|
||
|
else:
|
||
|
new_id_list.append(id_)
|
||
|
id_list = new_id_list
|
||
|
# now apply the translation list
|
||
|
if translator:
|
||
|
id_list = [
|
||
|
translator[x] if x in translator else x for x in id_list
|
||
|
]
|
||
|
# for ids with a wildcard, find all that match
|
||
|
if id_list and isinstance(id_list[0], str):
|
||
|
for i in reversed(range(len(id_list))):
|
||
|
this_id = id_list[i]
|
||
|
if '*' in this_id:
|
||
|
del id_list[i]
|
||
|
for new_id in reversed(
|
||
|
self._get_matching_strings(this_id,
|
||
|
acceptable_ids)):
|
||
|
id_list.insert(i, new_id)
|
||
|
# do not allow duplicates
|
||
|
# and ensure all given ids are valid
|
||
|
found = set()
|
||
|
unique_ids = list()
|
||
|
for id_ in id_list:
|
||
|
if id_ not in found:
|
||
|
found.add(id_)
|
||
|
if id_ in acceptable_ids:
|
||
|
unique_ids.append(id_)
|
||
|
else:
|
||
|
id_list = ', '.join([str(x) for x in acceptable_ids])
|
||
|
self._warning(
|
||
|
'Reference to undefined %s.' % (entity),
|
||
|
'A reference to %s \"%s\" was encountered but is '
|
||
|
'not defined. This %s will not be included in the '
|
||
|
'operation.\n\nThere are %d defined %s: %s' %
|
||
|
(entity, id_, entity, len(acceptable_ids),
|
||
|
self._plural(entity), id_list))
|
||
|
# return the unique list
|
||
|
id_list = unique_ids
|
||
|
# if only a single item was requested, ensure it exists
|
||
|
if single and len(id_list) != 1:
|
||
|
if not id_list:
|
||
|
self._error(
|
||
|
'No %s specified.' % entity,
|
||
|
'A single %s is required but none '
|
||
|
'were specified.' % entity)
|
||
|
self._error(
|
||
|
'Multiple %ss specified.' % entity,
|
||
|
'A single %s is required but %d %ss were specified: %s' %
|
||
|
(entity, len(id_list), entity, ', '.join(
|
||
|
[str(x) for x in id_list])))
|
||
|
# if list is empty and that's a problem, error out
|
||
|
if not empty_list_okay and not id_list:
|
||
|
self._error(
|
||
|
'No %ss specified.' % entity,
|
||
|
'We require at least one %s to be specified for this '
|
||
|
'opration but none were specified.\n\nThere are %d '
|
||
|
'defined %s: %s' %
|
||
|
(entity, len(acceptable_ids), self._plural(entity), ', '.join(
|
||
|
[str(x) for x in acceptable_ids])))
|
||
|
return id_list
|
||
|
|
||
|
def _get_standard_element_type(self, element_type, warning=True):
|
||
|
"""
|
||
|
Return a standardized string for the given element type.
|
||
|
|
||
|
Within an ExodusII file, the element type is stored as a string.
|
||
|
Because of this, there is no standardized way to store this field
|
||
|
and different codes can and do use different strings for the same
|
||
|
element type. This translation function interprets this field and
|
||
|
returns a standardized name if possible.
|
||
|
|
||
|
If an element type is not recognized and 'warning=True', a warning
|
||
|
message will be issued.
|
||
|
|
||
|
Example:
|
||
|
>>> model._get_standard_element_type('HEX')
|
||
|
'hex8'
|
||
|
|
||
|
"""
|
||
|
if isinstance(element_type, bytes):
|
||
|
element_type = element_type.decode('utf8').lower()
|
||
|
else:
|
||
|
element_type = element_type.lower()
|
||
|
|
||
|
translation_list = {
|
||
|
'hex': 'hex8',
|
||
|
'tet': 'tet4',
|
||
|
'tetra': 'tet4',
|
||
|
'tetra10': 'tet10',
|
||
|
'tri': 'tri3',
|
||
|
'triangle': 'tri3',
|
||
|
'quad': 'quad4',
|
||
|
'shell4': 'quad4',
|
||
|
'wedge': 'wedge6'
|
||
|
}
|
||
|
if element_type in translation_list:
|
||
|
element_type = translation_list[element_type]
|
||
|
if warning and element_type not in self.STANDARD_ELEMENT_TYPES:
|
||
|
self._warning(
|
||
|
'Nonstandard element type',
|
||
|
'The element type "%s" is not a standard '
|
||
|
'element type. This may cause issues with '
|
||
|
'handling element blocks with elements of '
|
||
|
'this type.' % (element_type))
|
||
|
return element_type
|
||
|
|
||
|
def _get_default_field_value(self, name):
|
||
|
"""Return the default value for a field with the given name."""
|
||
|
if self._is_displacement_field_prefix(name[:-2]):
|
||
|
return 0.0
|
||
|
return float('nan')
|
||
|
|
||
|
@staticmethod
|
||
|
def _is_displacement_field_prefix(name):
|
||
|
"""Return 'True' if name is a displacement field prefix."""
|
||
|
default_name = 'displacement'
|
||
|
if len(name) >= 3 and name.lower() == default_name[:len(name)]:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def _get_displacement_field_prefix(self):
|
||
|
"""
|
||
|
Return the prefix of the displacement field.
|
||
|
|
||
|
If no displacement field exists, this will return the default prefix.
|
||
|
|
||
|
"""
|
||
|
prefix = 'disp'
|
||
|
for node_field_name in self.get_node_field_names():
|
||
|
# if it doesn't end in "_x", it's not a prefix
|
||
|
if len(node_field_name) < 3 or node_field_name[-2] != '_':
|
||
|
continue
|
||
|
# check against acceptable names
|
||
|
this_prefix = node_field_name[:-2]
|
||
|
if self._is_displacement_field_prefix(this_prefix):
|
||
|
prefix = this_prefix
|
||
|
return prefix
|
||
|
|
||
|
def _get_element_faces(self, element_block_ids):
|
||
|
"""
|
||
|
Return a list of all element faces.
|
||
|
|
||
|
Note that internal element faces will be duplicated in the list.
|
||
|
|
||
|
A list is returned with members of the form:
|
||
|
* '(element_block_id, element_index, face_index)'
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
members = []
|
||
|
for id_ in element_block_ids:
|
||
|
mapping = self._get_face_mapping_from_id(id_)
|
||
|
element_count = self.get_element_count(id_)
|
||
|
for i in range(element_count):
|
||
|
members.extend([(id_, i, f) for f in range(len(mapping))])
|
||
|
return members
|
||
|
|
||
|
def _get_block_info(self, element_block_id):
|
||
|
"""Return the info of an element block."""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
return self.element_blocks[element_block_id][1]
|
||
|
|
||
|
def _get_external_element_faces(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return a list of external element faces.
|
||
|
|
||
|
External faces are element faces which are shared by no other elements.
|
||
|
|
||
|
A list is returned with members of the form:
|
||
|
* '(element_block_id, element_index, face_index)'
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
external_faces = dict()
|
||
|
for id_ in element_block_ids:
|
||
|
info = self._get_block_info(id_)
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
face_mapping = self._get_face_mapping_from_id(id_)
|
||
|
for element_index in range(info[1]):
|
||
|
for face_index, (_, face) in enumerate(face_mapping):
|
||
|
sorted_nodes = tuple(
|
||
|
sorted([
|
||
|
connectivity[element_index * info[2] + x]
|
||
|
for x in face
|
||
|
]))
|
||
|
if sorted_nodes in external_faces:
|
||
|
external_faces[sorted_nodes] = None
|
||
|
else:
|
||
|
this_face = (id_, element_index, face_index)
|
||
|
external_faces[sorted_nodes] = this_face
|
||
|
return [
|
||
|
value for value in list(external_faces.values())
|
||
|
if value is not None
|
||
|
]
|
||
|
|
||
|
@staticmethod
|
||
|
def _mean(values):
|
||
|
"""Return the mean of a list of values."""
|
||
|
return sum(values) / float(len(values))
|
||
|
|
||
|
@staticmethod
|
||
|
def _order_element_faces_by_block(members):
|
||
|
"""
|
||
|
Sort element faces by element block id.
|
||
|
|
||
|
This takes in a list of members of the form:
|
||
|
* '(element_block_id, element_index, face_index)'
|
||
|
|
||
|
This outputs a dictionary with the form
|
||
|
* 'output[element_block_id] = list of (element_index, face_index)'
|
||
|
|
||
|
"""
|
||
|
members_by_block = dict()
|
||
|
for element_block_id, element_index, face_index in members:
|
||
|
if element_block_id not in members_by_block:
|
||
|
members_by_block[element_block_id] = []
|
||
|
members_by_block[element_block_id].append(
|
||
|
(element_index, face_index))
|
||
|
return members_by_block
|
||
|
|
||
|
def _get_dimension(self, element_type):
|
||
|
"""Return the dimension of the given element type."""
|
||
|
element_type = self._get_standard_element_type(element_type)
|
||
|
return self.DIMENSION[element_type]
|
||
|
|
||
|
def _get_triangulated_faces(self, element_type):
|
||
|
"""Return the rule for how to triangulate the given 2D face."""
|
||
|
element_type = self._get_standard_element_type(element_type)
|
||
|
if element_type not in self.TRIANGULATED_FACES:
|
||
|
self._bug(
|
||
|
'Invalid element type.',
|
||
|
'We do not know how to subdivide an element of type '
|
||
|
'"%s" into triangles.' % element_type)
|
||
|
return self.TRIANGULATED_FACES[element_type]
|
||
|
|
||
|
def _convert_side_set_to_triangle_block(self,
|
||
|
side_set_ids,
|
||
|
new_element_block_id='auto'):
|
||
|
"""
|
||
|
Create a new 'tri3' element block composed of the given side set.
|
||
|
|
||
|
The side set must contain faces of three dimensional elements.
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
if new_element_block_id == 'auto':
|
||
|
new_element_block_id = self._new_element_block_id()
|
||
|
new_nodes = []
|
||
|
# find members of all the given side sets by block
|
||
|
members_by_block = self._order_element_faces_by_block(
|
||
|
itertools.chain(
|
||
|
*[self.get_side_set_members(x) for x in side_set_ids]))
|
||
|
# ensure all blocks are 3D
|
||
|
for id_ in list(members_by_block.keys()):
|
||
|
element_type = self._get_element_type(id_)
|
||
|
dimension = self._get_dimension(element_type)
|
||
|
if dimension != 3:
|
||
|
self._error(
|
||
|
'Incompatible element dimension.',
|
||
|
'We expect the side set to contain faces of 3D '
|
||
|
'elements. However, the side set contains at '
|
||
|
'least one face from element block %d which '
|
||
|
'contains %dD elements of type "%s".' %
|
||
|
(id_, dimension, element_type))
|
||
|
# Now we need to create the triangles from each member of the side set.
|
||
|
faces = dict()
|
||
|
for element_block_id, members in list(members_by_block.items()):
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
face_mapping = self._get_face_mapping_from_id(element_block_id)
|
||
|
for element_index, face_index in members:
|
||
|
face_element_map = face_mapping[face_index]
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
local_node = tuple(local_node[x] for x in face_element_map[1])
|
||
|
if not face_element_map[0] in faces:
|
||
|
faces[face_element_map[0]] = [local_node]
|
||
|
else:
|
||
|
faces[face_element_map[0]].append(local_node)
|
||
|
# now triangulate each face
|
||
|
connectivity = []
|
||
|
next_node_index = len(self.nodes)
|
||
|
new_nodes = []
|
||
|
for element_type, connectivity_list in list(faces.items()):
|
||
|
triangulated_faces = self._get_triangulated_faces(element_type)
|
||
|
for local_face_indices in connectivity_list:
|
||
|
for face_indices in triangulated_faces:
|
||
|
for index in face_indices:
|
||
|
if isinstance(index, int):
|
||
|
connectivity.append(local_face_indices[index])
|
||
|
continue
|
||
|
averaged_nodes = tuple(local_face_indices[x]
|
||
|
for x in index)
|
||
|
if not new_nodes or new_nodes[-1] != averaged_nodes:
|
||
|
new_nodes.append(averaged_nodes)
|
||
|
next_node_index += 1
|
||
|
connectivity.append(next_node_index - 1)
|
||
|
# create the new nodes
|
||
|
self._create_averaged_nodes(new_nodes, [])
|
||
|
# create the new element block
|
||
|
self.create_element_block(new_element_block_id,
|
||
|
['tri3', len(connectivity) // 3, 3, 0],
|
||
|
connectivity)
|
||
|
|
||
|
@staticmethod
|
||
|
def _sign(x):
|
||
|
"""
|
||
|
Return the sign of a value.
|
||
|
|
||
|
This returns 1 if the value positive, -1 if negative, and 0 otherwise.
|
||
|
|
||
|
"""
|
||
|
if x > 0:
|
||
|
return 1
|
||
|
if x < 0:
|
||
|
return -1
|
||
|
return 0
|
||
|
|
||
|
def _partition_triangle_block_from_node_field(self, element_block_id,
|
||
|
new_element_block_id,
|
||
|
node_field_name, timestep,
|
||
|
interval_list):
|
||
|
"""
|
||
|
Subdivide a 'tri3' element block based on the given field intervals.
|
||
|
|
||
|
The target element block will have a element field named 'interval'
|
||
|
corresponding to the given interval the element falls into.
|
||
|
|
||
|
This is a helper function for generating nice looking WRL files.
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
[node_field_name] = self._format_id_list([node_field_name],
|
||
|
self.get_node_field_names(),
|
||
|
'node field',
|
||
|
single=True)
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
# verify block is actually a tri3 block
|
||
|
if self._get_element_type(element_block_id) != 'tri3':
|
||
|
self._error(
|
||
|
'Invalid element type',
|
||
|
'This function can only be used to divide element '
|
||
|
'blocks composed of "tri3" elements.')
|
||
|
timestep_index = self.timesteps.index(timestep)
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
new_connectivity = []
|
||
|
element_interval_values = []
|
||
|
triangles = [
|
||
|
tuple(connectivity[x * 3:(x + 1) * 3])
|
||
|
for x in range(element_count)
|
||
|
]
|
||
|
for index, upper_bound in enumerate(interval_list):
|
||
|
# hold new vertices we have to create
|
||
|
# edge = (x, y)
|
||
|
# new_vertex[edge] = mid_edge_vertex_index
|
||
|
# we do this to keep the mesh watertight
|
||
|
new_vertex = dict()
|
||
|
new_nodes = []
|
||
|
next_node_index = len(self.nodes)
|
||
|
values = self.node_fields[node_field_name][timestep_index]
|
||
|
for tri in triangles:
|
||
|
for d in range(3):
|
||
|
d2 = (d + 1) % 3
|
||
|
edge = (tri[d], tri[d2])
|
||
|
if (self._sign(values[tri[d]] - upper_bound) *
|
||
|
self._sign(values[tri[d2]] - upper_bound) == -1):
|
||
|
if edge not in new_vertex:
|
||
|
opposite_edge = tuple(reversed(edge))
|
||
|
new_vertex[edge] = next_node_index
|
||
|
new_vertex[opposite_edge] = next_node_index
|
||
|
next_node_index += 1
|
||
|
phi = (upper_bound - values[tri[d]]) / (
|
||
|
values[tri[d2]] - values[tri[d]])
|
||
|
new_node = ((tri[d], 1.0 - phi), (tri[d2], phi))
|
||
|
new_nodes.append(new_node)
|
||
|
self._create_averaged_nodes(new_nodes, [])
|
||
|
# now form new triangles
|
||
|
new_triangles = []
|
||
|
this_partition = []
|
||
|
values = self.node_fields[node_field_name][timestep_index]
|
||
|
for tri in triangles:
|
||
|
if (values[tri[0]] <= upper_bound
|
||
|
and values[tri[1]] <= upper_bound
|
||
|
and values[tri[2]] <= upper_bound):
|
||
|
this_partition.append(tri)
|
||
|
elif (values[tri[0]] >= upper_bound
|
||
|
and values[tri[1]] >= upper_bound
|
||
|
and values[tri[2]] >= upper_bound):
|
||
|
new_triangles.append(tri)
|
||
|
else:
|
||
|
# find vertex below the bound
|
||
|
d = 0
|
||
|
while (values[tri[d]] >= upper_bound
|
||
|
or values[tri[(d + 1) % 3]] < upper_bound):
|
||
|
d += 1
|
||
|
tri = tuple(tri[(d + x) % 3] for x in range(3))
|
||
|
case = tuple(
|
||
|
self._sign(values[tri[x]] - upper_bound)
|
||
|
for x in range(3))
|
||
|
if case == (-1, 1, -1):
|
||
|
m1 = new_vertex[(tri[0], tri[1])]
|
||
|
m2 = new_vertex[(tri[1], tri[2])]
|
||
|
# below triangles
|
||
|
this_partition.append((tri[0], m1, tri[2]))
|
||
|
this_partition.append((m1, m2, tri[2]))
|
||
|
# above triangles
|
||
|
new_triangles.append((m1, tri[1], m2))
|
||
|
elif case == (-1, 1, 1):
|
||
|
m1 = new_vertex[(tri[0], tri[1])]
|
||
|
m2 = new_vertex[(tri[2], tri[0])]
|
||
|
# below triangles
|
||
|
this_partition.append((tri[0], m1, m2))
|
||
|
# above triangles
|
||
|
new_triangles.append((m1, tri[1], m2))
|
||
|
new_triangles.append((tri[1], tri[2], m2))
|
||
|
elif case == (-1, 0, 1):
|
||
|
m1 = new_vertex[(tri[2], tri[0])]
|
||
|
# below triangles
|
||
|
this_partition.append((tri[0], tri[1], m1))
|
||
|
# above triangles
|
||
|
new_triangles.append((tri[1], tri[2], m1))
|
||
|
elif case == (-1, 1, 0):
|
||
|
m1 = new_vertex[(tri[0], tri[1])]
|
||
|
# below triangles
|
||
|
this_partition.append((tri[0], m1, tri[2]))
|
||
|
# above triangles
|
||
|
new_triangles.append((m1, tri[1], tri[2]))
|
||
|
else:
|
||
|
self._bug('Unknown case')
|
||
|
triangles = new_triangles
|
||
|
new_connectivity.extend(itertools.chain(*this_partition))
|
||
|
element_interval_values.extend([float(index)] *
|
||
|
len(this_partition))
|
||
|
# add rest of triangle to last partition
|
||
|
new_connectivity.extend(itertools.chain(*triangles))
|
||
|
element_interval_values.extend([float(len(interval_list))] *
|
||
|
len(triangles))
|
||
|
self.create_element_block(
|
||
|
new_element_block_id,
|
||
|
['tri3', len(new_connectivity) // 3, 3, 0], new_connectivity)
|
||
|
self.create_element_field('interval', new_element_block_id, 0.0)
|
||
|
fields = self._get_element_block_fields(new_element_block_id)
|
||
|
field = fields['interval']
|
||
|
for index in range(len(self.timesteps)):
|
||
|
field[index] = list(element_interval_values)
|
||
|
|
||
|
def _get_coordinates_at_time(self, timestep):
|
||
|
"""
|
||
|
Return the node coordinates list at the given timestep.
|
||
|
|
||
|
This includes the displacement if it exists.
|
||
|
|
||
|
"""
|
||
|
timestep = self._format_id_list(timestep, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
if len(timestep) > 1:
|
||
|
self._error(
|
||
|
'Ambiguous timestep.',
|
||
|
'More than one timestep was specified. We expected '
|
||
|
'one or zero timesteps.')
|
||
|
if not timestep:
|
||
|
return [tuple(x) for x in self.nodes]
|
||
|
timestep_index = self.timesteps.index(timestep[0])
|
||
|
displacement_values = [
|
||
|
x[timestep_index] for x in self._get_displacement_field_values()
|
||
|
]
|
||
|
return [(x + dx, y + dy, z + dz)
|
||
|
for (x, y,
|
||
|
z), dx, dy, dz in zip(self.nodes, *displacement_values)]
|
||
|
|
||
|
def export_wrl_model(self,
|
||
|
filename,
|
||
|
node_field_name,
|
||
|
element_block_ids='all',
|
||
|
timestep='last',
|
||
|
field_range='auto',
|
||
|
intervals=9,
|
||
|
colorspace='rgb',
|
||
|
displacement_timestep='auto',
|
||
|
export_exodus_copy=True):
|
||
|
"""
|
||
|
Export the exterior of the model to a colored WRL file.
|
||
|
|
||
|
The WRL file format is used by 3D printing software.
|
||
|
|
||
|
Example:
|
||
|
>>> model.export_wrl_model('colored_eqps_model.wrl', 'eqps')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
[node_field_name] = self._format_id_list([node_field_name],
|
||
|
self.get_node_field_names(),
|
||
|
'node field',
|
||
|
single=True)
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
timestep_index = self.timesteps.index(timestep)
|
||
|
if displacement_timestep == 'auto':
|
||
|
if self.displacement_field_exists():
|
||
|
displacement_timestep = timestep
|
||
|
else:
|
||
|
displacement_timestep = 'none'
|
||
|
else:
|
||
|
[displacement_timestep] = self._format_id_list(
|
||
|
[displacement_timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
# all element blocks should either be 3D elements, or 'tri3' elements
|
||
|
element_types = set(
|
||
|
self._get_element_type(x) for x in element_block_ids)
|
||
|
element_dimensions = set(self._get_dimension(x) for x in element_types)
|
||
|
# if we have all 3D elements, create a skin
|
||
|
if element_dimensions == set([3]):
|
||
|
triangle_element_block_id = self._new_element_block_id()
|
||
|
external_side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(
|
||
|
external_side_set_id,
|
||
|
self._get_external_element_faces(element_block_ids))
|
||
|
self._convert_side_set_to_triangle_block(
|
||
|
external_side_set_id, triangle_element_block_id)
|
||
|
self.delete_side_set(external_side_set_id)
|
||
|
elif element_dimensions == set([2]) and element_types == set(['tri3']):
|
||
|
# merge all blocks into another block
|
||
|
ids_to_merge = []
|
||
|
for id_ in element_block_ids:
|
||
|
ids_to_merge.append(self._new_element_block_id())
|
||
|
self.duplicate_element_block(id_, ids_to_merge[-1])
|
||
|
triangle_element_block_id = self._new_element_block_id()
|
||
|
self.combine_element_blocks(ids_to_merge,
|
||
|
triangle_element_block_id)
|
||
|
# these blocks better form a watertight enclosure
|
||
|
external_sides = self._get_external_element_faces(
|
||
|
triangle_element_block_id)
|
||
|
if external_sides:
|
||
|
self._warning(
|
||
|
'Enclosure is not watertight.',
|
||
|
'The "tri3" element blocks passed to this function do not '
|
||
|
'form a watertight enclosure. An output file will still '
|
||
|
'be created, but it is unlikely to be useful as a WRL '
|
||
|
'model. We found %d unmatched sides.' %
|
||
|
len(external_sides))
|
||
|
else:
|
||
|
self._error(
|
||
|
'Invalid input.',
|
||
|
'We expect a list of 3D element blocks or a list of '
|
||
|
'2D element blocks composed of triangles.')
|
||
|
# now that we have a triangle block, subdivide it
|
||
|
subdivided_element_block_id = self._new_element_block_id()
|
||
|
# set up intervals
|
||
|
if field_range == 'auto':
|
||
|
indices = self.get_nodes_in_element_block(
|
||
|
triangle_element_block_id)
|
||
|
values = self.node_fields[node_field_name][timestep_index]
|
||
|
this_values = [values[x] for x in indices]
|
||
|
field_range = [min(this_values), max(this_values)]
|
||
|
if field_range[0] >= field_range[1]:
|
||
|
self._error(
|
||
|
'Invalid field range.',
|
||
|
'The given field range [%g - %g] was not valid.' %
|
||
|
(field_range[0], field_range[1]))
|
||
|
if isinstance(intervals, int):
|
||
|
intervals = [
|
||
|
(1 - phi) * field_range[0] + phi * field_range[1]
|
||
|
for phi in [x / float(intervals) for x in range(1, intervals)]
|
||
|
]
|
||
|
# partition that block based on the node field
|
||
|
self._partition_triangle_block_from_node_field(
|
||
|
triangle_element_block_id, subdivided_element_block_id,
|
||
|
node_field_name, timestep, intervals)
|
||
|
# save model as an ExodusII file so we can view it
|
||
|
if export_exodus_copy:
|
||
|
temp_nodes = self.nodes
|
||
|
self.nodes = self._get_coordinates_at_time(displacement_timestep)
|
||
|
exodus_filename = filename.rsplit('.', 1)[0] + '_wrl.e'
|
||
|
self.export_model(exodus_filename,
|
||
|
element_block_ids=subdivided_element_block_id,
|
||
|
element_field_names='interval',
|
||
|
node_field_names='none',
|
||
|
node_set_ids='none',
|
||
|
side_set_ids='none',
|
||
|
global_variable_names='none',
|
||
|
timesteps=[timestep])
|
||
|
self.nodes = temp_nodes
|
||
|
# save triangles to the WRL file
|
||
|
with open(filename, 'w') as output:
|
||
|
output.write('#VRML V2.0 utf8\n')
|
||
|
output.write('Transform {\n')
|
||
|
output.write(' translation 0.0 0.0 0.0\n')
|
||
|
output.write(' children Shape {\n')
|
||
|
output.write(' appearance Appearance {material Material {} }\n')
|
||
|
output.write(' geometry IndexedFaceSet {\n')
|
||
|
output.write(' coord Coordinate {\n')
|
||
|
output.write(' point [\n')
|
||
|
for position in self._get_coordinates_at_time(
|
||
|
displacement_timestep):
|
||
|
output.write(','.join(str(x) for x in position))
|
||
|
output.write(',')
|
||
|
output.write('\n ]\n')
|
||
|
output.write(' }\n')
|
||
|
output.write(' coordIndex [\n')
|
||
|
connectivity = self.get_connectivity(subdivided_element_block_id)
|
||
|
element_count = self.get_element_count(subdivided_element_block_id)
|
||
|
for element_index in range(element_count):
|
||
|
output.write('%d,%d,%d,-1,' %
|
||
|
(connectivity[element_index * 3 + 0],
|
||
|
connectivity[element_index * 3 + 1],
|
||
|
connectivity[element_index * 3 + 2]))
|
||
|
output.write('\n ]\n')
|
||
|
output.write(' color Color {\n')
|
||
|
output.write(' color [\n')
|
||
|
if colorspace == 'rgb':
|
||
|
colorspace = [
|
||
|
colorsys.hls_to_rgb(2.0 / 3 - 2.0 / 3 * x / len(intervals),
|
||
|
0.5, 1)
|
||
|
for x in range(len(intervals) + 1)
|
||
|
]
|
||
|
if len(colorspace) != len(intervals) + 1:
|
||
|
self._error(
|
||
|
'Unrecognized colorspace.',
|
||
|
'The given colorspace was not recognized. We '
|
||
|
'expected a string such as "rgb" or a list of '
|
||
|
'length %d with RGB triplets. Instead, we found '
|
||
|
'"%s".' % (len(intervals) + 1, str(colorspace)))
|
||
|
for color in colorspace:
|
||
|
output.write(' %s,\n' %
|
||
|
' '.join(str(x) for x in color))
|
||
|
output.write(' ]\n')
|
||
|
output.write(' }\n')
|
||
|
output.write(' colorIndex [\n')
|
||
|
output.write(' ')
|
||
|
values = self.get_element_field_values(
|
||
|
'interval', subdivided_element_block_id,
|
||
|
self.timesteps[timestep_index])
|
||
|
output.write(','.join(str(int(x)) for x in values))
|
||
|
output.write('\n')
|
||
|
output.write(' ]\n')
|
||
|
output.write(' colorPerVertex FALSE\n')
|
||
|
output.write(' }\n')
|
||
|
output.write(' }\n')
|
||
|
output.write('}\n')
|
||
|
# delete the temporary element blocks
|
||
|
self.delete_element_block(
|
||
|
[triangle_element_block_id, subdivided_element_block_id])
|
||
|
|
||
|
def export_stl_file(self,
|
||
|
filename,
|
||
|
element_block_ids='all',
|
||
|
displacement_timestep='auto'):
|
||
|
"""
|
||
|
Export the exterior of the model to an STL file.
|
||
|
|
||
|
By default, if timesteps exist and a displacement field exists, the
|
||
|
displacements at the last timestep will be applied.
|
||
|
|
||
|
Example:
|
||
|
>>> model.export_stl_file('mesh_surface.stl')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if displacement_timestep == 'auto':
|
||
|
if self.timesteps and self.displacement_field_exists():
|
||
|
displacement_timestep = 'last'
|
||
|
else:
|
||
|
displacement_timestep = 'none'
|
||
|
# create a new element block composed of triangles
|
||
|
triangle_element_block_id = self._new_element_block_id()
|
||
|
external_side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(
|
||
|
external_side_set_id,
|
||
|
self._get_external_element_faces(element_block_ids))
|
||
|
self._convert_side_set_to_triangle_block(external_side_set_id,
|
||
|
triangle_element_block_id)
|
||
|
# export that set
|
||
|
displacement_timestep = self._format_id_list(displacement_timestep,
|
||
|
self.get_timesteps(),
|
||
|
'timestep')
|
||
|
# get coordinates at the time we care about
|
||
|
c = self._get_coordinates_at_time(displacement_timestep)
|
||
|
# create the STL file
|
||
|
connectivity = self.get_connectivity(triangle_element_block_id)
|
||
|
with open(filename, 'wb') as output:
|
||
|
output.write(b' ' * 80)
|
||
|
output.write(struct.pack('<l', len(connectivity) // 3))
|
||
|
for element_index in range(len(connectivity) // 3):
|
||
|
tri = connectivity[element_index * 3:(element_index + 1) * 3]
|
||
|
normal = (c[tri[1]][1] * c[tri[2]][2] -
|
||
|
c[tri[2]][1] * c[tri[1]][2],
|
||
|
c[tri[1]][2] * c[tri[2]][0] -
|
||
|
c[tri[2]][2] * c[tri[1]][0],
|
||
|
c[tri[1]][0] * c[tri[2]][1] -
|
||
|
c[tri[2]][0] * c[tri[1]][1])
|
||
|
scale = (normal[0]**2 + normal[1]**2 + normal[2]**2)**0.5
|
||
|
if scale > 0.0:
|
||
|
normal = tuple(x / scale for x in normal)
|
||
|
output.write(struct.pack('<3f', *normal))
|
||
|
for vertex in tri:
|
||
|
output.write(struct.pack('<3f', *c[vertex]))
|
||
|
output.write(struct.pack('<h', 0))
|
||
|
# delete the temporary block we created
|
||
|
self.delete_element_block(triangle_element_block_id)
|
||
|
|
||
|
def export(self, filename, *args, **kwargs):
|
||
|
"""
|
||
|
Export a model based on the filename extension.
|
||
|
|
||
|
The following extensions will call the appropriate functions:
|
||
|
* 'WRL --> export_wrl_model'
|
||
|
* 'STL --> export_stl_file'
|
||
|
* 'E, G, EXO --> export_model'
|
||
|
|
||
|
Arguments are passed through to the export function.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.export('result.e')
|
||
|
>>> model.export('result.stl')
|
||
|
>>> model.export('result.wrl', 'eqps')
|
||
|
|
||
|
"""
|
||
|
exporters = dict()
|
||
|
exporters['wrl'] = self.export_wrl_model
|
||
|
exporters['stl'] = self.export_stl_file
|
||
|
exporters['e'] = self.export_model
|
||
|
exporters['g'] = self.export_model
|
||
|
exporters['exo'] = self.export_model
|
||
|
extension = filename.rsplit('.', 1)[-1].lower()
|
||
|
if extension not in exporters:
|
||
|
self._error(
|
||
|
'Unrecognized file extension.',
|
||
|
'The filename extension "%s" was not recognized. The '
|
||
|
'list of recognized extensions is : %s' %
|
||
|
(extension, ', '.join(list(exporters.keys()))))
|
||
|
exporters[extension](filename, *args, **kwargs)
|
||
|
|
||
|
def _error_evaluating_expression(self, expression, var):
|
||
|
"""Throw an error saying we could not evaluate the given expression."""
|
||
|
self._error(
|
||
|
'Invalid expression',
|
||
|
'An error occurred while trying to evaluate the given '
|
||
|
'expression. It is likely that this expression is '
|
||
|
'ill-formed, or that the variables it uses do not '
|
||
|
'exist:\n\n%s\n\nDefined variables: %s' %
|
||
|
(expression, ', '.join(sorted(var.keys()))))
|
||
|
|
||
|
@staticmethod
|
||
|
def _transform_eval_expression(expression, variable_names):
|
||
|
"""Transform a string expression into one usable by eval."""
|
||
|
eval_expression = ' %s ' % expression
|
||
|
for name in variable_names:
|
||
|
eval_expression = re.sub('([^a-zA-Z0-9_])%s([^a-zA-Z0-9_])' % name,
|
||
|
r"\1var['%s']\2" % name, eval_expression)
|
||
|
# add "math." to supported mathematical functions
|
||
|
# Note: abs is a built-in function, it is not math.abs
|
||
|
for name in [
|
||
|
'atan', 'sinh', 'cosh', 'tanh', 'exp', 'sqrt', 'sin', 'cos',
|
||
|
'tan'
|
||
|
]:
|
||
|
eval_expression = re.sub('([^a-zA-Z0-9_])%s([^a-zA-Z0-9_])' % name,
|
||
|
r'\1math.%s\2' % name, eval_expression)
|
||
|
# convert powers to from "^" to "**" format
|
||
|
eval_expression = re.sub(r'\^', '**', eval_expression)
|
||
|
return eval_expression.strip()
|
||
|
|
||
|
def get_element_block_dimension(self, element_block_id):
|
||
|
"""
|
||
|
Return the dimension of elements within this element block.
|
||
|
|
||
|
If this cannot be determined, return -1.
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
element_type = self._get_element_type(element_block_id)
|
||
|
element_type = self._get_standard_element_type(element_type,
|
||
|
warning=False)
|
||
|
if element_type in self.DIMENSION:
|
||
|
return self.DIMENSION[element_type]
|
||
|
return -1
|
||
|
|
||
|
def get_nodes_per_element(self, element_block_id):
|
||
|
"""Return the nodes per element in the given element block."""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
return self.element_blocks[element_block_id][1][2]
|
||
|
|
||
|
def _get_element_type(self, element_block_id):
|
||
|
"""
|
||
|
Return the element type for the given element block.
|
||
|
|
||
|
This is a string value stored within the ExodusII file. There is no
|
||
|
standard naming convention and different programs can call the same
|
||
|
element type by different names.
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
e_type = self.element_blocks[element_block_id][1][0]
|
||
|
if isinstance(e_type, bytes):
|
||
|
return e_type.decode('utf8')
|
||
|
return e_type
|
||
|
|
||
|
def _calculate_element_field_extreme(self,
|
||
|
element_field_names,
|
||
|
element_block_ids,
|
||
|
function,
|
||
|
function_name,
|
||
|
calculate_block_id=False,
|
||
|
calculate_location=False):
|
||
|
"""Store an element field extreme as a global variable."""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
element_field_names = self._format_id_list(
|
||
|
element_field_names,
|
||
|
self.get_element_field_names(element_block_ids), 'element field')
|
||
|
for element_field_name in element_field_names:
|
||
|
for element_block_id in element_block_ids:
|
||
|
if not self.element_field_exists(element_field_name,
|
||
|
element_block_ids):
|
||
|
self._missing_on_entity_error(element_field_name,
|
||
|
'element field',
|
||
|
element_block_id,
|
||
|
'element block')
|
||
|
for element_field_name in element_field_names:
|
||
|
name = element_field_name + '_' + function_name
|
||
|
self.create_global_variable(name)
|
||
|
if calculate_location:
|
||
|
self.create_global_variable(name + '_x')
|
||
|
self.create_global_variable(name + '_y')
|
||
|
self.create_global_variable(name + '_z')
|
||
|
if calculate_block_id:
|
||
|
self.create_global_variable(name + '_block_id')
|
||
|
for index in range(len(self.timesteps)):
|
||
|
extreme = None
|
||
|
extreme_block_id = None
|
||
|
extreme_node_indices = None
|
||
|
for element_block_id in element_block_ids:
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
values = fields[element_field_name][index]
|
||
|
if not values:
|
||
|
continue
|
||
|
this_extreme = function(values)
|
||
|
if (extreme is None or function([extreme, this_extreme
|
||
|
]) == this_extreme):
|
||
|
extreme = this_extreme
|
||
|
extreme_block_id = element_block_id
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(
|
||
|
element_block_id)
|
||
|
element_index = values.index(extreme)
|
||
|
extreme_node_indices = connectivity[element_index *
|
||
|
nodes_per_element:
|
||
|
(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
if extreme is None:
|
||
|
self._warning(
|
||
|
'No values encountered',
|
||
|
'The element field extreme cannot be '
|
||
|
'calculated because no values are present.')
|
||
|
continue
|
||
|
self.global_variables[name][index] = extreme
|
||
|
if calculate_block_id:
|
||
|
self.global_variables[name + '_block_id'][index] = float(
|
||
|
extreme_block_id)
|
||
|
if calculate_location:
|
||
|
coords = self._get_coordinates_at_time(
|
||
|
self.timesteps[index])
|
||
|
locations = [coords[x] for x in extreme_node_indices]
|
||
|
centroid = [
|
||
|
self._mean([x[i] for x in locations]) for i in range(3)
|
||
|
]
|
||
|
self.global_variables[name + '_x'][index] = centroid[0]
|
||
|
self.global_variables[name + '_y'][index] = centroid[1]
|
||
|
self.global_variables[name + '_z'][index] = centroid[2]
|
||
|
|
||
|
def calculate_element_field_maximum(self,
|
||
|
element_field_names,
|
||
|
element_block_ids='all',
|
||
|
calculate_location=False,
|
||
|
calculate_block_id=False):
|
||
|
"""
|
||
|
Store an element field maximum as a global variable.
|
||
|
|
||
|
If 'calculate_block_id=True', also store the element block id
|
||
|
containing the element with this value.
|
||
|
|
||
|
If 'calculate_location=True', also store the location of the centroid
|
||
|
of the element with this value.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.calculate_element_field_maximum('eqps')
|
||
|
>>> model.global_variables['eqps_max']
|
||
|
|
||
|
"""
|
||
|
self._calculate_element_field_extreme(
|
||
|
element_field_names,
|
||
|
element_block_ids,
|
||
|
max,
|
||
|
'max',
|
||
|
calculate_location=calculate_location,
|
||
|
calculate_block_id=calculate_block_id)
|
||
|
|
||
|
def calculate_element_field_minimum(self,
|
||
|
element_field_names,
|
||
|
element_block_ids='all',
|
||
|
calculate_location=False,
|
||
|
calculate_block_id=False):
|
||
|
"""
|
||
|
Store an element field minimum as a global variable.
|
||
|
|
||
|
If 'calculate_block_id=True', also store the element block id
|
||
|
containing the element with this value.
|
||
|
|
||
|
If 'calculate_location=True', also store the location of the centroid
|
||
|
of the element with this value.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.calculate_element_field_minimum('eqps')
|
||
|
>>> model.global_variables['eqps_min']
|
||
|
|
||
|
"""
|
||
|
self._calculate_element_field_extreme(
|
||
|
element_field_names,
|
||
|
element_block_ids,
|
||
|
min,
|
||
|
'min',
|
||
|
calculate_location=calculate_location,
|
||
|
calculate_block_id=calculate_block_id)
|
||
|
|
||
|
def _calculate_node_field_extreme(self,
|
||
|
node_field_names,
|
||
|
function,
|
||
|
function_name,
|
||
|
element_block_ids='auto',
|
||
|
calculate_location=False):
|
||
|
"""Store a node field extreme as a global variable."""
|
||
|
node_field_names = self._format_id_list(node_field_names,
|
||
|
self.get_node_field_names(),
|
||
|
'node field')
|
||
|
if element_block_ids == 'auto':
|
||
|
node_indices = list(range(len(self.nodes)))
|
||
|
else:
|
||
|
node_indices = self.get_nodes_in_element_block(element_block_ids)
|
||
|
if not node_indices:
|
||
|
self._error(
|
||
|
'No node values',
|
||
|
'No nodes were specified, so we cannot calculate the '
|
||
|
'extreme value.')
|
||
|
for node_field_name in node_field_names:
|
||
|
name = node_field_name + '_' + function_name
|
||
|
self.create_global_variable(name)
|
||
|
if calculate_location:
|
||
|
self.create_global_variable(name + '_x')
|
||
|
self.create_global_variable(name + '_y')
|
||
|
self.create_global_variable(name + '_z')
|
||
|
for index in range(len(self.timesteps)):
|
||
|
values = self.node_fields[node_field_name][index]
|
||
|
values = [values[x] for x in node_indices]
|
||
|
extreme = function(values)
|
||
|
self.global_variables[name][index] = extreme
|
||
|
if calculate_location:
|
||
|
coords = self._get_coordinates_at_time(
|
||
|
self.timesteps[index])
|
||
|
node_index = node_indices[values.index(extreme)]
|
||
|
location = coords[node_index]
|
||
|
self.global_variables[name + '_x'][index] = location[0]
|
||
|
self.global_variables[name + '_y'][index] = location[1]
|
||
|
self.global_variables[name + '_z'][index] = location[2]
|
||
|
|
||
|
def calculate_node_field_maximum(self,
|
||
|
node_field_names,
|
||
|
element_block_ids='auto',
|
||
|
calculate_location=False):
|
||
|
"""
|
||
|
Store a node field maximum as a global variable.
|
||
|
|
||
|
If 'calculate_location=True', also store the location of the node with
|
||
|
this value.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.calculate_node_field_maximum('temp')
|
||
|
>>> model.global_variables['temp_max']
|
||
|
|
||
|
"""
|
||
|
self._calculate_node_field_extreme(
|
||
|
node_field_names,
|
||
|
max,
|
||
|
'max',
|
||
|
element_block_ids=element_block_ids,
|
||
|
calculate_location=calculate_location)
|
||
|
|
||
|
def calculate_node_field_minimum(self,
|
||
|
node_field_names,
|
||
|
element_block_ids='auto',
|
||
|
calculate_location=False):
|
||
|
"""
|
||
|
Store a node field minimum as a global variable.
|
||
|
|
||
|
If 'calculate_location=True', also store the location of the node with
|
||
|
this value.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.calculate_node_field_maximum('temp')
|
||
|
>>> model.global_variables['temp_min']
|
||
|
|
||
|
"""
|
||
|
self._calculate_node_field_extreme(
|
||
|
node_field_names,
|
||
|
min,
|
||
|
'min',
|
||
|
element_block_ids=element_block_ids,
|
||
|
calculate_location=calculate_location)
|
||
|
|
||
|
def output_global_variables(self,
|
||
|
filename=None,
|
||
|
global_variable_names='all',
|
||
|
timesteps='all'):
|
||
|
"""
|
||
|
Output global variables in CSV format.
|
||
|
|
||
|
If 'filename=None', output will be sent to stdout. Else, it will be
|
||
|
output to the given filename.
|
||
|
|
||
|
By default, information is output for all timesteps and all global
|
||
|
variables. This may be changed by specifying which timesteps and
|
||
|
variables are output.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.output_global_variables('variables.csv')
|
||
|
>>> model.output_global_variables('variables.csv', timesteps='last')
|
||
|
|
||
|
"""
|
||
|
global_variable_names = self._format_id_list(
|
||
|
global_variable_names, self.get_global_variable_names(),
|
||
|
'global variable')
|
||
|
timesteps = self._format_id_list(timesteps, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
# output header
|
||
|
names = ['time']
|
||
|
names.extend(global_variable_names)
|
||
|
output = ', '.join(names) + '\n'
|
||
|
# output values
|
||
|
for timestep in timesteps:
|
||
|
index = self.timesteps.index(timestep)
|
||
|
values = [timestep]
|
||
|
for name in global_variable_names:
|
||
|
values.append(self.global_variables[name][index])
|
||
|
output += ', '.join(str(x) for x in values) + '\n'
|
||
|
# output to file or screen
|
||
|
if filename:
|
||
|
open(filename, 'w').write(output)
|
||
|
else:
|
||
|
if output:
|
||
|
output = output[:-1]
|
||
|
print(output)
|
||
|
|
||
|
def calculate_global_variable(self, expression):
|
||
|
"""
|
||
|
Store a global variable calculated from the given expression.
|
||
|
|
||
|
The expression may include the following variables:
|
||
|
* 'time' to refer to the current time
|
||
|
* global variables (by name)
|
||
|
|
||
|
Examples:
|
||
|
>>> model.calculate_global_variable('time_squared = time ^ 2')
|
||
|
>>> model.calculate_global_variable('total = potential + kinetic')
|
||
|
|
||
|
"""
|
||
|
if '=' not in expression:
|
||
|
self._error(
|
||
|
'Invalid expression',
|
||
|
'A "=" sign must be present in the expression but '
|
||
|
'was not found.\n\nExpression: %s' % expression)
|
||
|
(name, expression) = expression.split('=', 1)
|
||
|
new_name = name.strip()
|
||
|
self.create_global_variable(new_name)
|
||
|
# create list of variable names and modify them in the expression
|
||
|
variable_names = set(['time'])
|
||
|
variable_names.update(self.get_global_variable_names())
|
||
|
eval_expression = self._transform_eval_expression(
|
||
|
expression, variable_names)
|
||
|
function = eval('lambda var: ' + eval_expression)
|
||
|
var = dict()
|
||
|
try:
|
||
|
for index, time in enumerate(self.timesteps):
|
||
|
var['time'] = time
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
var[name] = values[index]
|
||
|
value = float(function(var))
|
||
|
self.global_variables[new_name][index] = value
|
||
|
except (SyntaxError, NameError):
|
||
|
self._error_evaluating_expression(
|
||
|
"%s = %s" % (new_name, eval_expression), var)
|
||
|
|
||
|
def calculate_node_field(self, expression):
|
||
|
"""
|
||
|
Store a node field calculated from the given expression.
|
||
|
|
||
|
The expression may include the following variables:
|
||
|
* 'time' to refer to the current time
|
||
|
* global variables (by name)
|
||
|
* node fields (by name)
|
||
|
* model coordinates ('X', 'Y', and 'Z')
|
||
|
|
||
|
Example:
|
||
|
>>> model.calculate_node_field('temp_C = temp_K - 273.15')
|
||
|
|
||
|
"""
|
||
|
if '=' not in expression:
|
||
|
self._error(
|
||
|
'Invalid expression',
|
||
|
'A "=" sign must be present in the expression but '
|
||
|
'was not found.\n\nExpression: %s' % expression)
|
||
|
(name, expression) = expression.split('=', 1)
|
||
|
new_name = name.strip()
|
||
|
self.create_node_field(new_name)
|
||
|
new_values = self.node_fields[new_name]
|
||
|
# create list of variable names and modify them in the expression
|
||
|
variable_names = set(['time'])
|
||
|
variable_names.update(['X', 'Y', 'Z'])
|
||
|
variable_names.update(self.get_global_variable_names())
|
||
|
variable_names.update(self.get_node_field_names())
|
||
|
eval_expression = self._transform_eval_expression(
|
||
|
expression, variable_names)
|
||
|
var = dict()
|
||
|
function = eval('lambda var: ' + eval_expression)
|
||
|
try:
|
||
|
for time_index, time in enumerate(self.timesteps):
|
||
|
# set time
|
||
|
var['time'] = time
|
||
|
# set global variables
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
var[name] = values[time_index]
|
||
|
# go through each node
|
||
|
for node_index in range(len(self.nodes)):
|
||
|
# set coordinate values
|
||
|
var['X'] = self.nodes[node_index][0]
|
||
|
var['Y'] = self.nodes[node_index][1]
|
||
|
var['Z'] = self.nodes[node_index][2]
|
||
|
# set node field values
|
||
|
for name, values in list(self.node_fields.items()):
|
||
|
var[name] = values[time_index][node_index]
|
||
|
value = float(function(var))
|
||
|
new_values[time_index][node_index] = value
|
||
|
except (SyntaxError, NameError):
|
||
|
self._error_evaluating_expression(
|
||
|
"%s = %s" % (new_name, eval_expression), var)
|
||
|
|
||
|
def calculate_node_set_field(self, expression, node_set_ids='all'):
|
||
|
"""
|
||
|
Store a node set field calculated from the given expression.
|
||
|
|
||
|
The expression may include the following variables:
|
||
|
* 'time' to refer to the current time
|
||
|
* global variables (by name)
|
||
|
* node fields (by name)
|
||
|
* node set fields (by name)
|
||
|
* model coordinates ('X', 'Y', and 'Z')
|
||
|
|
||
|
Example:
|
||
|
>>> model.calculate_node_set_field('temp_C = temp_K - 273.15')
|
||
|
|
||
|
"""
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
if '=' not in expression:
|
||
|
self._error(
|
||
|
'Invalid expression',
|
||
|
'A "=" sign must be present in the expression but '
|
||
|
'was not found.\n\nExpression: %s' % expression)
|
||
|
(name, expression) = expression.split('=', 1)
|
||
|
new_name = name.strip()
|
||
|
# for each node set
|
||
|
for node_set_id in node_set_ids:
|
||
|
self.create_node_set_field(new_name, node_set_id)
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
new_values = fields[new_name]
|
||
|
# create list of variable names and modify them in the expression
|
||
|
variable_names = set(['time'])
|
||
|
variable_names.update(['X', 'Y', 'Z'])
|
||
|
variable_names.update(self.get_global_variable_names())
|
||
|
variable_names.update(self.get_node_field_names())
|
||
|
variable_names.update(list(fields.keys()))
|
||
|
eval_expression = self._transform_eval_expression(
|
||
|
expression, variable_names)
|
||
|
function = eval('lambda var: ' + eval_expression)
|
||
|
var = dict()
|
||
|
try:
|
||
|
for time_index, time in enumerate(self.timesteps):
|
||
|
# set time
|
||
|
var['time'] = time
|
||
|
# set global variables
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
var[name] = values[time_index]
|
||
|
# go through each node
|
||
|
for member_index, node_index in enumerate(members):
|
||
|
# set coordinate values
|
||
|
var['X'] = self.nodes[node_index][0]
|
||
|
var['Y'] = self.nodes[node_index][1]
|
||
|
var['Z'] = self.nodes[node_index][2]
|
||
|
# set node fields
|
||
|
for name, values in list(self.node_fields.items()):
|
||
|
var[name] = values[time_index][node_index]
|
||
|
# set node set fields
|
||
|
for name, values in list(fields.items()):
|
||
|
var[name] = values[time_index][member_index]
|
||
|
value = float(function(var))
|
||
|
new_values[time_index][member_index] = value
|
||
|
except (SyntaxError, NameError):
|
||
|
self._error_evaluating_expression(
|
||
|
"%s = %s" % (new_name, eval_expression), var)
|
||
|
|
||
|
def calculate_side_set_field(self, expression, side_set_ids='all'):
|
||
|
"""
|
||
|
Store a side set field calculated from the given expression.
|
||
|
|
||
|
The expression may include the following variables:
|
||
|
* 'time' to refer to the current time
|
||
|
* global variables (by name)
|
||
|
|
||
|
Example:
|
||
|
>>> model.calculate_side_set_field('temp_C = temp_K - 273.15')
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
if '=' not in expression:
|
||
|
self._error(
|
||
|
'Invalid expression',
|
||
|
'A "=" sign must be present in the expression but '
|
||
|
'was not found.\n\nExpression: %s' % expression)
|
||
|
(name, expression) = expression.split('=', 1)
|
||
|
new_name = name.strip()
|
||
|
# for each side set
|
||
|
for side_set_id in side_set_ids:
|
||
|
self.create_side_set_field(new_name, side_set_id)
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
new_values = fields[new_name]
|
||
|
# create list of variable names and modify them in the expression
|
||
|
variable_names = set(['time'])
|
||
|
variable_names.update(self.get_global_variable_names())
|
||
|
variable_names.update(list(fields.keys()))
|
||
|
eval_expression = self._transform_eval_expression(
|
||
|
expression, variable_names)
|
||
|
function = eval('lambda var: ' + eval_expression)
|
||
|
var = dict()
|
||
|
try:
|
||
|
for time_index, time in enumerate(self.timesteps):
|
||
|
# set time
|
||
|
var['time'] = time
|
||
|
# set global variables
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
var[name] = values[time_index]
|
||
|
# go through each face
|
||
|
for member_index in range(len(members)):
|
||
|
# set side set fields
|
||
|
for name, values in list(fields.items()):
|
||
|
var[name] = values[time_index][member_index]
|
||
|
value = float(function(var))
|
||
|
new_values[time_index][member_index] = value
|
||
|
except (SyntaxError, NameError):
|
||
|
self._error_evaluating_expression(
|
||
|
"%s = %s" % (new_name, eval_expression), var)
|
||
|
|
||
|
def calculate_element_field(self, expression, element_block_ids='all'):
|
||
|
"""
|
||
|
Store an element field calculated from the given expression.
|
||
|
|
||
|
The expression may include the following variables:
|
||
|
* 'time' to refer to the current time
|
||
|
* global variables (by name)
|
||
|
* element fields (by name)
|
||
|
|
||
|
Example:
|
||
|
>>> model.calculate_element_field('pressure = (stress_xx + '
|
||
|
... 'stress_yy + stress_zz) / -3')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if '=' not in expression:
|
||
|
self._error(
|
||
|
'Invalid expression',
|
||
|
'A "=" sign must be present in the expression but '
|
||
|
'was not found.\n\nExpression: %s' % expression)
|
||
|
(name, expression) = expression.split('=', 1)
|
||
|
new_name = name.strip()
|
||
|
# for each element block
|
||
|
for element_block_id in element_block_ids:
|
||
|
self.create_element_field(new_name, element_block_id)
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
new_values = fields[new_name]
|
||
|
# create list of variable names and modify them in the expression
|
||
|
variable_names = set(['time'])
|
||
|
variable_names.update(self.get_global_variable_names())
|
||
|
variable_names.update(list(fields.keys()))
|
||
|
eval_expression = self._transform_eval_expression(
|
||
|
expression, variable_names)
|
||
|
function = eval('lambda var: ' + eval_expression)
|
||
|
var = dict()
|
||
|
try:
|
||
|
for time_index, time in enumerate(self.timesteps):
|
||
|
# set time
|
||
|
var['time'] = time
|
||
|
# set global variables
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
var[name] = values[time_index]
|
||
|
# go through element
|
||
|
for index in range(element_count):
|
||
|
# set element fields
|
||
|
for name, values in list(fields.items()):
|
||
|
var[name] = values[time_index][index]
|
||
|
value = float(function(var))
|
||
|
new_values[time_index][index] = value
|
||
|
except (SyntaxError, NameError):
|
||
|
self._error_evaluating_expression(
|
||
|
"%s = %s" % (new_name, eval_expression), var)
|
||
|
|
||
|
def to_lowercase(self):
|
||
|
"""
|
||
|
Convert the names of all entities within the model to lowercase.
|
||
|
|
||
|
This affects global variables, node fields, element fields, side set
|
||
|
fields, and node set fields.
|
||
|
|
||
|
Example:
|
||
|
>>> model.to_lowercase()
|
||
|
|
||
|
"""
|
||
|
for name in self.get_global_variable_names():
|
||
|
if name != name.lower():
|
||
|
self.rename_global_variable(name, name.lower())
|
||
|
for name in self.get_node_field_names():
|
||
|
if name != name.lower():
|
||
|
self.rename_node_field(name, name.lower())
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
for name in self.get_element_field_names(id_):
|
||
|
if name != name.lower():
|
||
|
self.rename_element_field(name, name.lower(), id_)
|
||
|
for id_ in self.get_side_set_ids():
|
||
|
for name in self.get_side_set_field_names(id_):
|
||
|
if name != name.lower():
|
||
|
self.rename_side_set_field(name, name.lower(), id_)
|
||
|
for id_ in self.get_node_set_ids():
|
||
|
for name in self.get_node_set_field_names(id_):
|
||
|
if name != name.lower():
|
||
|
self.rename_node_set_field(name, name.lower(), id_)
|
||
|
|
||
|
def _translate_element_blocks(self, element_block_translation_list):
|
||
|
"""
|
||
|
Convert element blocks to a new element type.
|
||
|
|
||
|
>>> model._translate_element_blocks([(1, 'hex20'), (2, 'tet10')])
|
||
|
|
||
|
"""
|
||
|
# validity check the inputs
|
||
|
valid_element_block_translation_list = []
|
||
|
for (id_, new_element_type) in element_block_translation_list:
|
||
|
# check if element block exists
|
||
|
if not self.element_block_exists(id_):
|
||
|
self._missing_warning(id_, 'element block')
|
||
|
continue
|
||
|
old_element_type = self._get_standard_element_type(
|
||
|
self._get_element_type(id_))
|
||
|
# check if any schemes exist for this element block
|
||
|
if old_element_type not in self.ELEMENT_CONVERSIONS:
|
||
|
self._error(
|
||
|
'No element conversion schemes',
|
||
|
'There are no element conversion schemes to '
|
||
|
'convert elements of type "%s" into any other '
|
||
|
'element type.' % (old_element_type))
|
||
|
continue
|
||
|
# check if the given scheme exists for this element block
|
||
|
conversions = self.ELEMENT_CONVERSIONS[old_element_type]
|
||
|
if new_element_type not in conversions:
|
||
|
self._error(
|
||
|
'Invalid target element type',
|
||
|
'There are no element conversion schemes to '
|
||
|
'convert elements of type "%s" into elements of '
|
||
|
'type "%s". Conversions are available to the '
|
||
|
'following element types: "%s".' %
|
||
|
(old_element_type, new_element_type, '", "'.join(
|
||
|
list(conversions.keys()))))
|
||
|
continue
|
||
|
# all is good, keep this one on the list
|
||
|
valid_element_block_translation_list.append(
|
||
|
[id_, new_element_type])
|
||
|
# if we have nothing to do, just exit
|
||
|
if not valid_element_block_translation_list:
|
||
|
return
|
||
|
# now we need to find the new nodes to create, if any
|
||
|
averaged_nodes = set()
|
||
|
for (id_, new_element_type) in valid_element_block_translation_list:
|
||
|
old_element_type = self._get_standard_element_type(
|
||
|
self._get_element_type(id_))
|
||
|
scheme = self.ELEMENT_CONVERSIONS[old_element_type]
|
||
|
scheme = scheme[new_element_type]
|
||
|
# find new nodes which need created
|
||
|
averaged_node_list = set()
|
||
|
for formula in scheme:
|
||
|
for new_node in formula:
|
||
|
# if it's just an integer, we don't need a new node created
|
||
|
if isinstance(new_node, int):
|
||
|
continue
|
||
|
# new node
|
||
|
averaged_node_list.add(new_node)
|
||
|
# now loop through all elements and create a list of averaged
|
||
|
# nodes to create
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
# iterate over each element
|
||
|
for local_node in [
|
||
|
connectivity[x:x + nodes_per_element]
|
||
|
for x in range(0, len(connectivity), nodes_per_element)
|
||
|
]:
|
||
|
# iterate over each new averaged node formula
|
||
|
for formula in averaged_node_list:
|
||
|
node_list = tuple(sorted(local_node[x] for x in formula))
|
||
|
averaged_nodes.add(node_list)
|
||
|
# create the averaged node mapping
|
||
|
averaged_nodes = sorted(averaged_nodes)
|
||
|
averaged_node_map = dict((x, len(self.nodes) + index)
|
||
|
for index, x in enumerate(averaged_nodes))
|
||
|
# create new averaged nodes
|
||
|
self._create_averaged_nodes(averaged_nodes, [])
|
||
|
# now create the new element blocks
|
||
|
for (element_block_id,
|
||
|
new_element_type) in valid_element_block_translation_list:
|
||
|
old_element_type = self._get_standard_element_type(
|
||
|
self._get_element_type(element_block_id))
|
||
|
scheme = self.ELEMENT_CONVERSIONS[old_element_type]
|
||
|
scheme = scheme[new_element_type]
|
||
|
# rename some things
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
# create the connectivity for the new element block
|
||
|
new_connectivity = []
|
||
|
for local_node in [
|
||
|
connectivity[x:x + nodes_per_element]
|
||
|
for x in range(0, len(connectivity), nodes_per_element)
|
||
|
]:
|
||
|
for new_element in scheme:
|
||
|
for new_node in new_element:
|
||
|
if isinstance(new_node, int):
|
||
|
new_connectivity.append(local_node[new_node])
|
||
|
else:
|
||
|
node_list = tuple(
|
||
|
sorted(local_node[x] for x in new_node))
|
||
|
new_connectivity.append(
|
||
|
averaged_node_map[node_list])
|
||
|
# create a temporary element block
|
||
|
temporary_element_block_id = self._new_element_block_id()
|
||
|
new_nodes_per_element = self.NODES_PER_ELEMENT[new_element_type]
|
||
|
self.create_element_block(temporary_element_block_id, [
|
||
|
new_element_type,
|
||
|
len(new_connectivity) // new_nodes_per_element,
|
||
|
new_nodes_per_element, 0
|
||
|
], new_connectivity)
|
||
|
temporary_fields = self._get_element_block_fields(
|
||
|
temporary_element_block_id)
|
||
|
element_multiplier = len(scheme)
|
||
|
# transfer element values
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
for field_name in self.get_element_field_names(element_block_id):
|
||
|
values = fields[field_name]
|
||
|
new_values = [list(x) for x in values]
|
||
|
new_values = [[
|
||
|
x for x in these_values for _ in range(element_multiplier)
|
||
|
] for these_values in new_values]
|
||
|
temporary_fields[field_name] = new_values
|
||
|
# for each face in the old scheme, find all of its nodes
|
||
|
old_face_mapping = self._get_face_mapping(old_element_type)
|
||
|
old_face_nodes = [set(x) for _, x in old_face_mapping]
|
||
|
new_face_mapping = self._get_face_mapping(new_element_type)
|
||
|
face_translation = [[] for _ in range(len(old_face_nodes))]
|
||
|
for new_element_index, new_element in enumerate(scheme):
|
||
|
for new_face_index, (
|
||
|
_, face_members) in enumerate(new_face_mapping):
|
||
|
# find all nodes used by this new face
|
||
|
used_nodes = set()
|
||
|
for face_member in face_members:
|
||
|
if isinstance(new_element[face_member], int):
|
||
|
used_nodes.add(new_element[face_member])
|
||
|
else:
|
||
|
used_nodes.update(new_element[face_member])
|
||
|
# see if these are a subset of nodes in the old set
|
||
|
for (old_face_index,
|
||
|
old_members) in enumerate(old_face_nodes):
|
||
|
if used_nodes <= old_members:
|
||
|
face_translation[old_face_index].append(
|
||
|
(new_element_index, new_face_index))
|
||
|
# update self.side_sets
|
||
|
for id_ in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(id_)
|
||
|
fields = self._get_side_set_fields(id_)
|
||
|
old_members = list(members)
|
||
|
for member_index, (block_id, element_index,
|
||
|
face_index) in enumerate(old_members):
|
||
|
if block_id == element_block_id:
|
||
|
# add some new faces
|
||
|
for new_faces in face_translation[face_index]:
|
||
|
members.append(
|
||
|
(temporary_element_block_id,
|
||
|
element_index * len(scheme) + new_faces[0],
|
||
|
new_faces[1]))
|
||
|
# add values for the new faces
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
for _ in face_translation[face_index]:
|
||
|
values.append(values[member_index])
|
||
|
# delete the old block
|
||
|
self.delete_element_block(element_block_id)
|
||
|
# rename the temporary element block
|
||
|
self.rename_element_block(temporary_element_block_id,
|
||
|
element_block_id)
|
||
|
|
||
|
def convert_element_blocks(self, element_block_ids, new_element_type):
|
||
|
"""Convert elements within a block to a new element type."""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# form the conversion list
|
||
|
conversion_list = [(id_, new_element_type)
|
||
|
for id_ in element_block_ids]
|
||
|
# convert each block
|
||
|
self._translate_element_blocks(conversion_list)
|
||
|
|
||
|
def _change_element_order(self, element_block_ids, target_order):
|
||
|
"""
|
||
|
Convert elements to the given order.
|
||
|
|
||
|
This will attempt to find the best element to convert to given the
|
||
|
conversion schemes involved. If more than one element option exists
|
||
|
for a given element type, this will choose the option that produces
|
||
|
the fewest nodes.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# find all element types we need to convert
|
||
|
element_types = set()
|
||
|
for element_block_id in element_block_ids:
|
||
|
element_types.add(self._get_element_type(element_block_id))
|
||
|
element_types = sorted(
|
||
|
self._get_standard_element_type(x) for x in element_types)
|
||
|
# ignore non-standard element types
|
||
|
element_types = [
|
||
|
x for x in element_types if x in self.STANDARD_ELEMENT_TYPES
|
||
|
]
|
||
|
# find all elements of the target order
|
||
|
target_element_types = [
|
||
|
x for x in self.STANDARD_ELEMENT_TYPES
|
||
|
if self.ELEMENT_ORDER[x] == target_order
|
||
|
]
|
||
|
# for each element type, find the optimal target type
|
||
|
element_type_map = dict()
|
||
|
for element_type in element_types:
|
||
|
# if no conversion schemes, error out
|
||
|
if element_type not in self.ELEMENT_CONVERSIONS:
|
||
|
# if element type is already the desired order, no warning
|
||
|
if self.ELEMENT_ORDER[element_type] == target_order:
|
||
|
continue
|
||
|
self._error(
|
||
|
'Unable to convert',
|
||
|
'There is no valid scheme to convert elements '
|
||
|
'of type "%s" to an element type of order '
|
||
|
'%d.' % (element_type, target_order))
|
||
|
scheme = self.ELEMENT_CONVERSIONS[element_type]
|
||
|
possible_types = set(
|
||
|
scheme.keys()).intersection(target_element_types)
|
||
|
if not possible_types:
|
||
|
# if element type is already the desired order, no warning
|
||
|
if self.ELEMENT_ORDER[element_type] == target_order:
|
||
|
continue
|
||
|
self._error(
|
||
|
'Unable to convert',
|
||
|
'There is no valid scheme to convert elements '
|
||
|
'of type "%s" to an element type of order '
|
||
|
'%d.' % (element_type, target_order))
|
||
|
# out of the given options, choose the one that first creates the
|
||
|
# least number of elements, and secondly the least number of nodes
|
||
|
ranking = [(len(scheme[x]), len(scheme[x]) * len(scheme[x][0]), x)
|
||
|
for x in possible_types]
|
||
|
ranking = sorted(ranking)
|
||
|
element_type_map[element_type] = ranking[0][-1]
|
||
|
# create the conversion list
|
||
|
conversion_list = []
|
||
|
for element_block_id in element_block_ids:
|
||
|
element_type = self._get_element_type(element_block_id)
|
||
|
element_type = self._get_standard_element_type(element_type)
|
||
|
if element_type in element_type_map:
|
||
|
conversion_list.append(
|
||
|
(element_block_id, element_type_map[element_type]))
|
||
|
# now translate the elements
|
||
|
self._translate_element_blocks(conversion_list)
|
||
|
|
||
|
def make_elements_linear(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Convert elements in one or more element blocks to a linear type.
|
||
|
|
||
|
This will attempt to find the best element to convert to given the
|
||
|
conversion schemes involved. If more than one element option exists
|
||
|
for a given element type, this will choose the option that produces
|
||
|
the fewest nodes.
|
||
|
|
||
|
"""
|
||
|
self._change_element_order(element_block_ids, 1)
|
||
|
|
||
|
def make_elements_quadratic(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Convert elements in one or more element blocks to a quadratic type.
|
||
|
|
||
|
This will attempt to find the best element to convert to given the
|
||
|
conversion schemes involved. If more than one element option exists
|
||
|
for a given element type, this will choose the option that produces
|
||
|
the fewest nodes.
|
||
|
|
||
|
"""
|
||
|
self._change_element_order(element_block_ids, 2)
|
||
|
|
||
|
def _translate_element_type(self, element_block_id, new_element_type,
|
||
|
scheme):
|
||
|
"""Convert elements within a block to a new type."""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
old_element_type = self._get_standard_element_type(
|
||
|
self._get_element_type(element_block_id))
|
||
|
new_element_type = self._get_standard_element_type(new_element_type)
|
||
|
old_nodes_per_element = self.NODES_PER_ELEMENT[old_element_type]
|
||
|
new_nodes_per_element = self.NODES_PER_ELEMENT[new_element_type]
|
||
|
element_multiplier = len(scheme)
|
||
|
# Make a list of nodes which need duplicated and nodes which need
|
||
|
# averaged. We will assign indices to these after we determine how
|
||
|
# many there are of each.
|
||
|
duplicate_nodes = set()
|
||
|
averaged_nodes = set()
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
for element_index in range(element_count):
|
||
|
local_node = connectivity[element_index *
|
||
|
old_nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
old_nodes_per_element]
|
||
|
for new_element in scheme:
|
||
|
for new_node in new_element:
|
||
|
if isinstance(new_node, int):
|
||
|
duplicate_nodes.add(local_node[new_node])
|
||
|
else:
|
||
|
averaged_nodes.add(
|
||
|
tuple(local_node[x] for x in new_node))
|
||
|
# create new nodes
|
||
|
next_node_index = len(self.nodes)
|
||
|
duplicate_nodes = sorted(duplicate_nodes)
|
||
|
averaged_nodes = sorted(averaged_nodes)
|
||
|
self._duplicate_nodes(duplicate_nodes, [])
|
||
|
self._create_averaged_nodes(averaged_nodes, [])
|
||
|
# assign node indices
|
||
|
duplicate_nodes = dict((x, next_node_index + index)
|
||
|
for index, x in enumerate(duplicate_nodes))
|
||
|
next_node_index += len(duplicate_nodes)
|
||
|
averaged_nodes = dict((x, next_node_index + index)
|
||
|
for index, x in enumerate(averaged_nodes))
|
||
|
# create the connectivity for the new element block
|
||
|
new_connectivity = []
|
||
|
for element_index in range(element_count):
|
||
|
local_node = connectivity[element_index *
|
||
|
old_nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
old_nodes_per_element]
|
||
|
for new_element in scheme:
|
||
|
for new_node in new_element:
|
||
|
if isinstance(new_node, int):
|
||
|
new_connectivity.append(
|
||
|
duplicate_nodes[local_node[new_node]])
|
||
|
else:
|
||
|
new_connectivity.append(averaged_nodes[tuple(
|
||
|
local_node[x] for x in new_node)])
|
||
|
# create the new block
|
||
|
temporary_element_block_id = self._new_element_block_id()
|
||
|
self.create_element_block(temporary_element_block_id, [
|
||
|
new_element_type,
|
||
|
len(new_connectivity) // new_nodes_per_element,
|
||
|
new_nodes_per_element, 0
|
||
|
], new_connectivity)
|
||
|
temporary_element_fields = self._get_element_block_fields(
|
||
|
temporary_element_block_id)
|
||
|
# transfer element values
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
for field_name in self.get_element_field_names(element_block_id):
|
||
|
values = fields[field_name]
|
||
|
new_values = [list(x) for x in values]
|
||
|
new_values = [[
|
||
|
x for x in these_values for _ in range(element_multiplier)
|
||
|
] for these_values in new_values]
|
||
|
temporary_element_fields[field_name] = new_values
|
||
|
# for each face in the old scheme, find all of its nodes
|
||
|
old_face_mapping = self._get_face_mapping(old_element_type)
|
||
|
old_face_nodes = [set(x) for _, x in old_face_mapping]
|
||
|
new_face_mapping = self._get_face_mapping(new_element_type)
|
||
|
face_translation = [[] for _ in range(len(old_face_nodes))]
|
||
|
for new_element_index, new_element in enumerate(scheme):
|
||
|
for new_face_index, (_,
|
||
|
face_members) in enumerate(new_face_mapping):
|
||
|
# find all nodes used by this new face
|
||
|
used_nodes = set()
|
||
|
for face_member in face_members:
|
||
|
if isinstance(new_element[face_member], int):
|
||
|
used_nodes.add(new_element[face_member])
|
||
|
else:
|
||
|
used_nodes.update(new_element[face_member])
|
||
|
# see if these are a subset of nodes in the old set
|
||
|
for old_face_index, old_members in enumerate(old_face_nodes):
|
||
|
if used_nodes <= old_members:
|
||
|
face_translation[old_face_index].append(
|
||
|
(new_element_index, new_face_index))
|
||
|
# update self.side_sets
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
old_members = list(members)
|
||
|
for member_index, (block_id, element_index,
|
||
|
face_index) in enumerate(old_members):
|
||
|
if block_id == element_block_id:
|
||
|
# add some new faces
|
||
|
for new_faces in face_translation[face_index]:
|
||
|
members.append(
|
||
|
(temporary_element_block_id,
|
||
|
element_index * len(scheme) + new_faces[0],
|
||
|
new_faces[1]))
|
||
|
# add values for the new faces
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
for _ in face_translation[face_index]:
|
||
|
values.append(values[member_index])
|
||
|
# delete the old block and rename the new one
|
||
|
self.delete_element_block(element_block_id)
|
||
|
self.rename_element_block(temporary_element_block_id, element_block_id)
|
||
|
|
||
|
def convert_hex8_block_to_tet4_block(self,
|
||
|
element_block_id,
|
||
|
scheme='hex24tet'):
|
||
|
"""
|
||
|
Convert a block of 'hex8' elements to a block of 'tet4' elements.
|
||
|
|
||
|
Side sets are updated accordingly.
|
||
|
|
||
|
Node sets are not modified.
|
||
|
|
||
|
Currently, only the 'hex24tet' scheme is implemented, which creates 24
|
||
|
'tet4' element for eeach 'hex8' element in the original mesh.
|
||
|
|
||
|
Example:
|
||
|
>>> model.convert_hex8_block_to_tet4_block(1)
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
# ensure source block is actually hex8
|
||
|
source_element_type = self._get_standard_element_type(
|
||
|
self._get_element_type(element_block_id))
|
||
|
if source_element_type != 'hex8':
|
||
|
self._error(
|
||
|
'Incompatible element type.',
|
||
|
'We were expecting an element block composed of '
|
||
|
'"hex8" element but instead encountered one with "%s" '
|
||
|
'elements.' % source_element_type)
|
||
|
# get the chosen scheme
|
||
|
if scheme != 'hex24tet':
|
||
|
self._error('Unsupported scheme.',
|
||
|
'The scheme "%s" was not recognized.' % scheme)
|
||
|
# create the scheme
|
||
|
scheme = []
|
||
|
for _, face in self._get_face_mapping('hex8'):
|
||
|
for index in range(4):
|
||
|
scheme.append(
|
||
|
tuple([
|
||
|
face[(index + 1) % 4], face[index],
|
||
|
tuple(sorted(face)),
|
||
|
tuple(range(8))
|
||
|
]))
|
||
|
# apply it
|
||
|
self._translate_element_type(element_block_id, 'tet4', scheme)
|
||
|
|
||
|
def displacement_field_exists(self):
|
||
|
"""
|
||
|
Return 'True' if the displacement field exists.
|
||
|
|
||
|
Example:
|
||
|
>>> model.displacement_field_exists()
|
||
|
|
||
|
"""
|
||
|
prefix = self._get_displacement_field_prefix()
|
||
|
return (self.node_field_exists(prefix + '_x')
|
||
|
and self.node_field_exists(prefix + '_y')
|
||
|
and self.node_field_exists(prefix + '_z'))
|
||
|
|
||
|
def create_displacement_field(self):
|
||
|
"""
|
||
|
Create the displacement field if it doesn't already exist.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_displacement_field()
|
||
|
|
||
|
"""
|
||
|
prefix = self._get_displacement_field_prefix()
|
||
|
for component in ['x', 'y', 'z']:
|
||
|
this_name = prefix + '_' + component
|
||
|
if not self.node_field_exists(this_name):
|
||
|
self.create_node_field(this_name)
|
||
|
|
||
|
def _get_displacement_field_values(self):
|
||
|
"""
|
||
|
Return a list of the displacement field names.
|
||
|
|
||
|
This will create them if they do not already exist.
|
||
|
|
||
|
"""
|
||
|
self.create_displacement_field()
|
||
|
prefix = self._get_displacement_field_prefix()
|
||
|
displacement_field_list = []
|
||
|
for component in ['x', 'y', 'z']:
|
||
|
this_name = prefix + '_' + component
|
||
|
displacement_field_list.append(self.node_fields[this_name])
|
||
|
return displacement_field_list
|
||
|
|
||
|
def _apply_displacements(self, timestep='last', scale_factor=1.0):
|
||
|
"""
|
||
|
Apply the displacements to each node.
|
||
|
|
||
|
If 'dx, dy, dz' are the components of the displacement fields, this
|
||
|
applies the following transformation.
|
||
|
* 'X --> X + dx'
|
||
|
* 'Y --> Y + dy'
|
||
|
* 'Z --> Z + dz'
|
||
|
|
||
|
The nodal coordinates are modified but the displacement field itself
|
||
|
is unchanged.
|
||
|
|
||
|
"""
|
||
|
timestep = self._format_id_list(timestep, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
if len(timestep) > 1:
|
||
|
self._error(
|
||
|
'Ambiguous timestep.',
|
||
|
'More than one timestep was specified. We expected '
|
||
|
'one or zero timesteps.')
|
||
|
if not timestep:
|
||
|
return [tuple(x) for x in self.nodes]
|
||
|
timestep_index = self.timesteps.index(timestep[0])
|
||
|
displacement_values = [
|
||
|
x[timestep_index] for x in self._get_displacement_field_values()
|
||
|
]
|
||
|
new_nodes = [
|
||
|
(x + dx * scale_factor, y + dy * scale_factor,
|
||
|
z + dz * scale_factor)
|
||
|
for (x, y, z), dx, dy, dz in zip(self.nodes, *displacement_values)
|
||
|
]
|
||
|
self.nodes = new_nodes
|
||
|
|
||
|
def _get_local_index(self, this_id, id_list, entity='entity'):
|
||
|
"""
|
||
|
Return the local index corresponding to the given id.
|
||
|
|
||
|
If an entity is not present, throw an error.
|
||
|
|
||
|
Example:
|
||
|
>>> model._get_local_index(10, [10, 20, 30])
|
||
|
0
|
||
|
>>> model._get_local_index('first', [10, 20, 30])
|
||
|
10
|
||
|
|
||
|
"""
|
||
|
if this_id == 'first':
|
||
|
if not id_list:
|
||
|
self._error(
|
||
|
'Undefined %s reference.' % entity,
|
||
|
'A reference to the first %s was encountered but '
|
||
|
'no %ss are defined.' % (entity, entity))
|
||
|
return 0
|
||
|
if this_id == 'last':
|
||
|
if not id_list:
|
||
|
self._error(
|
||
|
'Undefined %s reference.' % entity,
|
||
|
'A reference to the last %s was encountered but '
|
||
|
'no %ss are defined.' % (entity, entity))
|
||
|
return len(id_list) - 1
|
||
|
if this_id not in id_list:
|
||
|
entity_list = ', '.join([str(x) for x in id_list])
|
||
|
self._error(
|
||
|
'Reference to undefined %s.' % entity,
|
||
|
'A reference to %s "%s" was encountered but is not '
|
||
|
'defined in the model. There are %d defined %ss: %s' %
|
||
|
(entity, str(this_id), len(id_list), entity, entity_list))
|
||
|
return id_list.index(this_id)
|
||
|
|
||
|
def get_element_field_values(self,
|
||
|
element_field_name,
|
||
|
element_block_id='auto',
|
||
|
timestep='last'):
|
||
|
"""
|
||
|
Return the list of element field values.
|
||
|
|
||
|
The actual list of values is returned, so any modifications to it will
|
||
|
be stored in the model.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_element_field_values('strain', element_block_id=1)
|
||
|
>>> model.get_element_field_values('strain', timestep=2.0)
|
||
|
>>> model.get_element_field_values('strain',
|
||
|
... element_block_id=5,
|
||
|
... timestep='last')
|
||
|
|
||
|
"""
|
||
|
[element_field_name] = self._format_id_list(
|
||
|
[element_field_name],
|
||
|
self.get_element_field_names(),
|
||
|
'element field',
|
||
|
single=True)
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
timestep_index = self._get_internal_timestep_index(timestep)
|
||
|
if not self.element_field_exists(element_field_name, element_block_id):
|
||
|
self._missing_on_entity_error(element_field_name, 'element field',
|
||
|
element_block_id, 'element block')
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
return fields[element_field_name][timestep_index]
|
||
|
|
||
|
def get_side_set_field_values(self,
|
||
|
side_set_field_name,
|
||
|
side_set_id='auto',
|
||
|
timestep='last'):
|
||
|
"""
|
||
|
Return the list of side set field values.
|
||
|
|
||
|
The actual list of values is returned, so any modifications to it will
|
||
|
be stored in the model.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_side_set_field_values('contact_pressure', side_set_id=1)
|
||
|
>>> model.get_side_set_field_values('contact_pressure', timestep=2.0)
|
||
|
>>> model.get_side_set_field_values('contact_pressure',
|
||
|
... side_set_id=5,
|
||
|
... timestep='last')
|
||
|
|
||
|
"""
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
[side_set_field_name] = self._format_id_list(
|
||
|
[side_set_field_name],
|
||
|
self.get_side_set_field_names(),
|
||
|
'side set field',
|
||
|
single=True)
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
timestep_index = self._get_internal_timestep_index(timestep)
|
||
|
if not self.side_set_field_exists(side_set_field_name, side_set_id):
|
||
|
self._missing_on_entity_error(side_set_field_name,
|
||
|
'side set field', side_set_id,
|
||
|
'side set')
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
return fields[side_set_field_name][timestep_index]
|
||
|
|
||
|
def get_node_set_field_values(self,
|
||
|
node_set_field_name,
|
||
|
node_set_id='auto',
|
||
|
timestep='last'):
|
||
|
"""
|
||
|
Return the list of node set field values.
|
||
|
|
||
|
The actual list of values is returned, so any modifications to it will
|
||
|
be stored in the model.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_node_set_field_values('contact_pressure', node_set_id=1)
|
||
|
>>> model.get_node_set_field_values('contact_pressure', timestep=2.0)
|
||
|
>>> model.get_node_set_field_values('contact_pressure',
|
||
|
... node_set_id=5,
|
||
|
... timestep='last')
|
||
|
|
||
|
"""
|
||
|
[node_set_id] = self._format_node_set_id_list([node_set_id], single=True)
|
||
|
[node_set_field_name] = self._format_id_list(
|
||
|
[node_set_field_name],
|
||
|
self.get_node_set_field_names(),
|
||
|
'node set field',
|
||
|
single=True)
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
timestep_index = self._get_internal_timestep_index(timestep)
|
||
|
if not self.node_set_field_exists(node_set_field_name, node_set_id):
|
||
|
self._missing_on_entity_error(node_set_field_name,
|
||
|
'node set field', node_set_id,
|
||
|
'node set')
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
return fields[node_set_field_name][timestep_index]
|
||
|
|
||
|
def _is_standard_element_type(self, element_type):
|
||
|
"""Return 'True' if 'element_type' is a standard element type."""
|
||
|
name = self._get_standard_element_type(element_type, warning=False)
|
||
|
return name in self.STANDARD_ELEMENT_TYPES
|
||
|
|
||
|
def get_element_block_ids(self):
|
||
|
"""
|
||
|
Return a list of all element block ids.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_element_block_ids()
|
||
|
|
||
|
"""
|
||
|
return sorted(self.element_blocks.keys())
|
||
|
|
||
|
def get_element_block_name(self, element_block_id):
|
||
|
"""
|
||
|
Return the given element block name.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_element_block_names()
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
return self.element_blocks[element_block_id][0]
|
||
|
|
||
|
def get_all_element_block_names(self):
|
||
|
"""
|
||
|
Return a list of all element block names.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_all_element_block_names()
|
||
|
|
||
|
"""
|
||
|
return sorted(x[0] for x in list(self.element_blocks.values()) if x[0])
|
||
|
|
||
|
def _get_standard_element_block_ids(self):
|
||
|
"""
|
||
|
Return a list of element block ids which use standard element types.
|
||
|
|
||
|
Example:
|
||
|
>>> model._get_standard_element_block_ids()
|
||
|
|
||
|
"""
|
||
|
return [
|
||
|
x for x in self.get_element_block_ids()
|
||
|
if self._is_standard_element_type(self._get_element_type(x))
|
||
|
]
|
||
|
|
||
|
def get_node_set_ids(self):
|
||
|
"""
|
||
|
Return a list of all node set ids.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_node_set_ids()
|
||
|
|
||
|
"""
|
||
|
return sorted(self.node_sets.keys())
|
||
|
|
||
|
def get_side_set_ids(self):
|
||
|
"""
|
||
|
Return a list of all side set ids.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_side_set_ids()
|
||
|
|
||
|
"""
|
||
|
return sorted(self.side_sets.keys())
|
||
|
|
||
|
def get_node_field_names(self):
|
||
|
"""
|
||
|
Return a list of all node field names.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_node_field_names()
|
||
|
|
||
|
"""
|
||
|
return self._sort_field_names(list(self.node_fields.keys()))
|
||
|
|
||
|
def get_node_set_field_names(self, node_set_ids='all'):
|
||
|
"""
|
||
|
Return a list of all node set field names.
|
||
|
|
||
|
By default, this returns node set fields which are defined on any
|
||
|
node set. To return fields which are defined on a particular
|
||
|
node set, pass a node set id.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_node_set_field_names()
|
||
|
>>> model.get_node_set_field_names(1)
|
||
|
|
||
|
"""
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
names = set()
|
||
|
for id_ in node_set_ids:
|
||
|
names.update(list(self.node_sets[id_][-1].keys()))
|
||
|
return self._sort_field_names(list(names))
|
||
|
|
||
|
def get_side_set_field_names(self, side_set_ids='all'):
|
||
|
"""
|
||
|
Return a list of all side set field names.
|
||
|
|
||
|
By default, this returns side set fields which are defined on any
|
||
|
side set. To return fields which are defined on a particular
|
||
|
side set, pass a side set id.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_side_set_field_names()
|
||
|
>>> model.get_side_set_field_names(1)
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
names = set()
|
||
|
for id_ in side_set_ids:
|
||
|
names.update(list(self._get_side_set_fields(id_).keys()))
|
||
|
return self._sort_field_names(list(names))
|
||
|
|
||
|
def get_element_field_names(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return a list of all element field names.
|
||
|
|
||
|
By default, this returns element fields which are defined on any
|
||
|
element block. To return fields which are defined on a particular
|
||
|
element block, pass an element block id.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_element_field_names()
|
||
|
>>> model.get_element_field_names(1)
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
names = set()
|
||
|
for id_ in element_block_ids:
|
||
|
names.update(list(self.element_blocks[id_][-1].keys()))
|
||
|
return self._sort_field_names(list(names))
|
||
|
|
||
|
def get_global_variable_names(self):
|
||
|
"""
|
||
|
Return a list of all global variable names.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_global_variable_names()
|
||
|
|
||
|
"""
|
||
|
return self._sort_field_names(list(self.global_variables.keys()))
|
||
|
|
||
|
def get_timesteps(self):
|
||
|
"""
|
||
|
Return a list of all timesteps.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_timesteps()
|
||
|
|
||
|
"""
|
||
|
return sorted(self.timesteps)
|
||
|
|
||
|
def _get_internal_timestep_index(self, timestep):
|
||
|
"""Return the local timestep index."""
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
return self.timesteps.index(timestep)
|
||
|
|
||
|
def _create_element_field_truth_table(self, element_block_ids,
|
||
|
field_names):
|
||
|
"""
|
||
|
Return the element field truth table.
|
||
|
|
||
|
The element field truth table defines which element fields are defined
|
||
|
for which element blocks according to the given order of element block
|
||
|
ids and field names passed in. This is needed within the ExodusII
|
||
|
file format.
|
||
|
|
||
|
"""
|
||
|
# go through each case and set values to True if they exist
|
||
|
truth_table = [
|
||
|
field_name in self._get_element_block_fields(element_block_id)
|
||
|
for element_block_id in element_block_ids
|
||
|
for field_name in field_names
|
||
|
]
|
||
|
return truth_table
|
||
|
|
||
|
def _create_side_set_field_truth_table(self, side_set_ids, field_names):
|
||
|
"""
|
||
|
Return the side set field truth table.
|
||
|
|
||
|
The side set field truth table defines which side set fields are
|
||
|
defined for which side sets according to the given order of side set
|
||
|
ids and field names passed in. This is needed within the ExodusII
|
||
|
file format.
|
||
|
|
||
|
"""
|
||
|
# go through each case and set values to True if they exist
|
||
|
truth_table = [
|
||
|
self.side_set_field_exists(field_name, side_set_id)
|
||
|
for side_set_id in side_set_ids for field_name in field_names
|
||
|
]
|
||
|
return truth_table
|
||
|
|
||
|
def _create_node_set_field_truth_table(self, node_set_ids, field_names):
|
||
|
"""
|
||
|
Return the node set field truth table.
|
||
|
|
||
|
The node set field truth table defines which node set fields are
|
||
|
defined for which node sets according to the given order of node set
|
||
|
ids and field names passed in. This is needed within the ExodusII
|
||
|
file format.
|
||
|
|
||
|
"""
|
||
|
# go through each case and set values to True if they exist
|
||
|
truth_table = [
|
||
|
self.node_set_field_exists(field_name, node_set_id)
|
||
|
for node_set_id in node_set_ids for field_name in field_names
|
||
|
]
|
||
|
return truth_table
|
||
|
|
||
|
def get_connectivity(self, element_block_id='auto'):
|
||
|
"""
|
||
|
Return the connectivity list of an element block.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_connectivity(1)
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
return self.element_blocks[element_block_id][2]
|
||
|
|
||
|
def get_element_block_connectivity(self, element_block_id='auto'):
|
||
|
"""Alias for 'get_connectivity()'."""
|
||
|
return self.get_connectivity(element_block_id)
|
||
|
|
||
|
def get_nodes_in_element_block(self, element_block_ids):
|
||
|
"""
|
||
|
Return a list of all node indices used in the given element blocks.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_nodes_in_element_block(1)
|
||
|
>>> model.get_nodes_in_element_block([1, 3])
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
node_list = set()
|
||
|
for id_ in element_block_ids:
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
node_list.update(connectivity)
|
||
|
return sorted(node_list)
|
||
|
|
||
|
def _rotate_nodes(self,
|
||
|
axis,
|
||
|
angle_in_degrees,
|
||
|
node_indices='all',
|
||
|
adjust_displacement_field='auto'):
|
||
|
"""Rotate nodes about an axis by the given angle."""
|
||
|
if adjust_displacement_field == 'auto':
|
||
|
adjust_displacement_field = self.displacement_field_exists()
|
||
|
# Create rotation matrix.
|
||
|
# x --> R * x
|
||
|
scale = math.sqrt(sum(x * x for x in axis))
|
||
|
(ux, uy, uz) = [float(x) / scale for x in axis]
|
||
|
theta = float(angle_in_degrees) * math.pi / 180
|
||
|
cost = math.cos(theta)
|
||
|
sint = math.sin(theta)
|
||
|
# if angle_in_degrees is a multiple of 90 degrees, then make sin and
|
||
|
# cos exactly 0, 1, or -1 to avoid roundoff errors.
|
||
|
if angle_in_degrees % 90 == 0:
|
||
|
sint = math.floor(sint + 0.5)
|
||
|
cost = math.floor(cost + 0.5)
|
||
|
rxx = cost + ux * ux * (1 - cost)
|
||
|
rxy = ux * uy * (1 - cost) - uz * sint
|
||
|
rxz = ux * uz * (1 - cost) + uy * sint
|
||
|
ryx = uy * ux * (1 - cost) + uz * sint
|
||
|
ryy = cost + uy * uy * (1 - cost)
|
||
|
ryz = uy * uz * (1 - cost) - ux * sint
|
||
|
rzx = uz * ux * (1 - cost) - uy * sint
|
||
|
rzy = uz * uy * (1 - cost) + ux * sint
|
||
|
rzz = cost + uz * uz * (1 - cost)
|
||
|
# Rotate nodes.
|
||
|
if node_indices == 'all':
|
||
|
self.nodes = [[
|
||
|
rxx * x + rxy * y + rxz * z, ryx * x + ryy * y + ryz * z,
|
||
|
rzx * x + rzy * y + rzz * z
|
||
|
] for x, y, z in self.nodes]
|
||
|
else:
|
||
|
for index in node_indices:
|
||
|
n = self.nodes[index]
|
||
|
self.nodes[index] = [
|
||
|
rxx * n[0] + rxy * n[1] + rxz * n[2],
|
||
|
ryx * n[0] + ryy * n[1] + ryz * n[2],
|
||
|
rzx * n[0] + rzy * n[1] + rzz * n[2]
|
||
|
]
|
||
|
# Rotate the displacement field.
|
||
|
if adjust_displacement_field:
|
||
|
(disp_x, disp_y, disp_z) = self._get_displacement_field_values()
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
if node_indices == 'all':
|
||
|
new_disp_x = [
|
||
|
rxx * x + rxy * y + rxz * z for x, y, z in zip(
|
||
|
disp_x[timestep_index], disp_y[timestep_index],
|
||
|
disp_z[timestep_index])
|
||
|
]
|
||
|
new_disp_y = [
|
||
|
ryx * x + ryy * y + ryz * z for x, y, z in zip(
|
||
|
disp_x[timestep_index], disp_y[timestep_index],
|
||
|
disp_z[timestep_index])
|
||
|
]
|
||
|
new_disp_z = [
|
||
|
rzx * x + rzy * y + rzz * z for x, y, z in zip(
|
||
|
disp_x[timestep_index], disp_y[timestep_index],
|
||
|
disp_z[timestep_index])
|
||
|
]
|
||
|
disp_x[timestep_index] = new_disp_x
|
||
|
disp_y[timestep_index] = new_disp_y
|
||
|
disp_z[timestep_index] = new_disp_z
|
||
|
else:
|
||
|
for index in node_indices:
|
||
|
x = disp_x[timestep_index][index]
|
||
|
y = disp_y[timestep_index][index]
|
||
|
z = disp_z[timestep_index][index]
|
||
|
(x, y, z) = [
|
||
|
rxx * x + rxy * y + rxz * z,
|
||
|
ryx * x + ryy * y + ryz * z,
|
||
|
rzx * x + rzy * y + rzz * z
|
||
|
]
|
||
|
disp_x[timestep_index][index] = x
|
||
|
disp_y[timestep_index][index] = y
|
||
|
disp_z[timestep_index][index] = z
|
||
|
|
||
|
def _translate_nodes(self, offset, node_indices='all'):
|
||
|
"""Translate nodes by the given offset."""
|
||
|
(dx, dy, dz) = [float(x) for x in offset]
|
||
|
if node_indices == 'all':
|
||
|
self.nodes = [[x + dx, y + dy, z + dz] for x, y, z in self.nodes]
|
||
|
else:
|
||
|
for index in node_indices:
|
||
|
self.nodes[index][0] += dx
|
||
|
self.nodes[index][1] += dy
|
||
|
self.nodes[index][2] += dz
|
||
|
|
||
|
def _scale_nodes(self,
|
||
|
scale_factor,
|
||
|
node_indices='all',
|
||
|
adjust_displacement_field='auto'):
|
||
|
"""Scale nodes in the list by the given scale factor."""
|
||
|
scale_factor = float(scale_factor)
|
||
|
if adjust_displacement_field == 'auto':
|
||
|
adjust_displacement_field = self.displacement_field_exists()
|
||
|
# Scale the nodal coordinates.
|
||
|
if node_indices == 'all':
|
||
|
self.nodes = [[x * scale_factor for x in n] for n in self.nodes]
|
||
|
else:
|
||
|
for index in node_indices:
|
||
|
self.nodes[index] = [
|
||
|
x * scale_factor for x in self.nodes[index]
|
||
|
]
|
||
|
# Scale the displacement field.
|
||
|
if adjust_displacement_field:
|
||
|
for all_values in self._get_displacement_field_values():
|
||
|
for values in all_values:
|
||
|
if node_indices == 'all':
|
||
|
values[:] = [x * scale_factor for x in values]
|
||
|
else:
|
||
|
for index in node_indices:
|
||
|
values[index] *= scale_factor
|
||
|
|
||
|
def rotate_geometry(self,
|
||
|
axis,
|
||
|
angle_in_degrees,
|
||
|
adjust_displacement_field='auto'):
|
||
|
"""
|
||
|
Rotate the model about an axis by the given angle.
|
||
|
|
||
|
The rotation axis includes the origin and points in the direction of
|
||
|
the 'axis' parameter.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rotate_geometry([1, 0, 0], 90)
|
||
|
|
||
|
"""
|
||
|
self._rotate_nodes(axis,
|
||
|
angle_in_degrees,
|
||
|
adjust_displacement_field=adjust_displacement_field)
|
||
|
|
||
|
def translate_geometry(self, offset):
|
||
|
"""
|
||
|
Translate the model by the given offset.
|
||
|
|
||
|
Example:
|
||
|
>>> model.translate_geometry([1, 2, 3])
|
||
|
|
||
|
"""
|
||
|
self._translate_nodes(offset)
|
||
|
|
||
|
def scale_geometry(self, scale_factor, adjust_displacement_field='auto'):
|
||
|
"""
|
||
|
Scale the model by the given factor.
|
||
|
|
||
|
By default, if it exists, the displacement field will also be scaled
|
||
|
accordingly.
|
||
|
|
||
|
Example:
|
||
|
>>> model.scale_geometry(0.0254)
|
||
|
|
||
|
"""
|
||
|
self._scale_nodes(scale_factor,
|
||
|
adjust_displacement_field=adjust_displacement_field)
|
||
|
|
||
|
def _ensure_no_shared_nodes(self, element_block_ids):
|
||
|
"""
|
||
|
Ensure no nodes are shared outside the given element blocks.
|
||
|
|
||
|
If nodes are shared between the given element blocks and all
|
||
|
other element block then an error message is output.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
affected_nodes = self.get_nodes_in_element_block(element_block_ids)
|
||
|
nodes_in_other_blocks = self.get_nodes_in_element_block(
|
||
|
list(set(self.get_element_block_ids()) - set(element_block_ids)))
|
||
|
shared_nodes = sorted(
|
||
|
set.intersection(set(affected_nodes), set(nodes_in_other_blocks)))
|
||
|
if shared_nodes:
|
||
|
max_nodes_to_display = 20
|
||
|
node_list = ', '.join(
|
||
|
[str(x) for x in shared_nodes[:max_nodes_to_display]])
|
||
|
if len(shared_nodes) > max_nodes_to_display:
|
||
|
node_list += ', ...'
|
||
|
self._error(
|
||
|
'Unable to operate on merged nodes.',
|
||
|
'You are attempting to operate on some element blocks '
|
||
|
'while keeping others unaffected. Because some nodes '
|
||
|
'are shared between the two groups, this cannot be '
|
||
|
'done. Use unmerge_element_blocks() to unmerge the '
|
||
|
'blocks if that is desired.\n'
|
||
|
'\n'
|
||
|
'There are %d shared nodes: %s' %
|
||
|
(len(shared_nodes), node_list))
|
||
|
|
||
|
def translate_element_blocks(self,
|
||
|
element_block_ids,
|
||
|
offset,
|
||
|
check_for_merged_nodes=True):
|
||
|
"""
|
||
|
Translate the specified element blocks by the given offset.
|
||
|
|
||
|
Example:
|
||
|
>>> model.translate_element_blocks(1, [1.0, 2.0, 3.0])
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if check_for_merged_nodes:
|
||
|
self._ensure_no_shared_nodes(element_block_ids)
|
||
|
affected_nodes = self.get_nodes_in_element_block(element_block_ids)
|
||
|
self._translate_nodes(offset, affected_nodes)
|
||
|
|
||
|
def reflect_element_blocks(self,
|
||
|
element_block_ids,
|
||
|
point,
|
||
|
normal,
|
||
|
check_for_merged_nodes=True,
|
||
|
adjust_displacement_field='auto'):
|
||
|
"""
|
||
|
Reflect the specified element blocks about the given plane.
|
||
|
|
||
|
Since an element becomes inverted when it is reflected across a plane,
|
||
|
this function will also uninvert the elements.
|
||
|
|
||
|
Example:
|
||
|
>>> model.reflect_element_blocks(1, [0, 0, 0], [1, 0, 0])
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if adjust_displacement_field == 'auto':
|
||
|
adjust_displacement_field = self.displacement_field_exists()
|
||
|
point = [float(x) for x in point]
|
||
|
scale = math.sqrt(sum(x * x for x in normal))
|
||
|
normal = [float(x) / scale for x in normal]
|
||
|
if check_for_merged_nodes:
|
||
|
self._ensure_no_shared_nodes(element_block_ids)
|
||
|
affected_nodes = self.get_nodes_in_element_block(element_block_ids)
|
||
|
for index in affected_nodes:
|
||
|
distance = sum(
|
||
|
(a - p) * n
|
||
|
for a, p, n in zip(self.nodes[index], point, normal))
|
||
|
self.nodes[index] = [
|
||
|
a - 2 * distance * n for a, n in zip(self.nodes[index], normal)
|
||
|
]
|
||
|
# adjust displacement fields
|
||
|
if adjust_displacement_field:
|
||
|
displacement_fields = self._get_displacement_field_values()
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
for index in affected_nodes:
|
||
|
dx = displacement_fields[0][timestep_index][index]
|
||
|
dy = displacement_fields[1][timestep_index][index]
|
||
|
dz = displacement_fields[2][timestep_index][index]
|
||
|
distance = dx * normal[0] + dy * normal[1] + dz * normal[2]
|
||
|
dx -= 2 * distance * normal[0]
|
||
|
dy -= 2 * distance * normal[1]
|
||
|
dz -= 2 * distance * normal[2]
|
||
|
displacement_fields[0][timestep_index][index] = dx
|
||
|
displacement_fields[1][timestep_index][index] = dy
|
||
|
displacement_fields[2][timestep_index][index] = dz
|
||
|
# uninvert elements
|
||
|
self._invert_element_blocks(element_block_ids)
|
||
|
|
||
|
def scale_element_blocks(self,
|
||
|
element_block_ids,
|
||
|
scale_factor,
|
||
|
check_for_merged_nodes=True,
|
||
|
adjust_displacement_field='auto'):
|
||
|
"""
|
||
|
Scale all nodes in the given element blocks by the given amount.
|
||
|
|
||
|
By default, if a displacement field exists, this will also scale the
|
||
|
displacement field.
|
||
|
|
||
|
Example:
|
||
|
>>> model.scale_element_blocks(1, 0.0254)
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if adjust_displacement_field == 'auto':
|
||
|
adjust_displacement_field = self.displacement_field_exists()
|
||
|
if check_for_merged_nodes:
|
||
|
self._ensure_no_shared_nodes(element_block_ids)
|
||
|
affected_nodes = self.get_nodes_in_element_block(element_block_ids)
|
||
|
self._scale_nodes(scale_factor,
|
||
|
affected_nodes,
|
||
|
adjust_displacement_field=adjust_displacement_field)
|
||
|
|
||
|
def rotate_element_blocks(self,
|
||
|
element_block_ids,
|
||
|
axis,
|
||
|
angle_in_degrees,
|
||
|
check_for_merged_nodes=True,
|
||
|
adjust_displacement_field='auto'):
|
||
|
"""
|
||
|
Rotate all nodes in the given element blocks by the given amount.
|
||
|
|
||
|
By default, if a displacement field exists, this will also rotate the
|
||
|
displacement field.
|
||
|
|
||
|
The rotation axis includes the origin and points in the direction of
|
||
|
the 'axis' parameter.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rotate_element_blocks(1, [1, 0, 0], 90)
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if adjust_displacement_field == 'auto':
|
||
|
adjust_displacement_field = self.displacement_field_exists()
|
||
|
if check_for_merged_nodes:
|
||
|
self._ensure_no_shared_nodes(element_block_ids)
|
||
|
affected_nodes = self.get_nodes_in_element_block(element_block_ids)
|
||
|
self._rotate_nodes(axis,
|
||
|
angle_in_degrees,
|
||
|
affected_nodes,
|
||
|
adjust_displacement_field=adjust_displacement_field)
|
||
|
|
||
|
def displace_element_blocks(self,
|
||
|
element_block_ids,
|
||
|
offset,
|
||
|
timesteps='all',
|
||
|
check_for_merged_nodes=True):
|
||
|
"""
|
||
|
Displace all nodes in the given element blocks.
|
||
|
|
||
|
This function operates on the displacement field rather than the model
|
||
|
coordinate field. To operate on the model coordinates, use
|
||
|
'translate_element_blocks()'.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.displace_element_blocks(1, [1.0, 2.0, 3.0])
|
||
|
>>> model.displace_element_blocks('all', [1.0, 2.0, 3.0])
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
timesteps = self._format_id_list(timesteps, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
if check_for_merged_nodes:
|
||
|
self._ensure_no_shared_nodes(element_block_ids)
|
||
|
timestep_indices = [self.timesteps.index(x) for x in timesteps]
|
||
|
offset = [float(x) for x in offset]
|
||
|
# if no timesteps exist, issue an error
|
||
|
if not self.timesteps:
|
||
|
self._error(
|
||
|
'No timesteps defined',
|
||
|
'A displacement field cannot exist if no timesteps '
|
||
|
'are defined. To avoid this error, create a timestep '
|
||
|
'before calling this function.')
|
||
|
# get displacement field indices
|
||
|
displacement_fields = self._get_displacement_field_values()
|
||
|
# get affected nodes
|
||
|
node_list = self.get_nodes_in_element_block(element_block_ids)
|
||
|
# translate nodes
|
||
|
for timestep_index in timestep_indices:
|
||
|
for dimension in range(3):
|
||
|
values = displacement_fields[dimension][timestep_index]
|
||
|
for index in node_list:
|
||
|
values[index] += offset[dimension]
|
||
|
|
||
|
def _rename_field_on_entity(self, field_type, field_name, new_field_name,
|
||
|
field_name_list_function, entity_type,
|
||
|
entity_list, entity_list_function,
|
||
|
entity_objects):
|
||
|
"""Rename a field defined on an entity."""
|
||
|
entity_list = self._format_id_list(entity_list, entity_list_function(),
|
||
|
entity_type)
|
||
|
[field_name] = self._format_id_list(
|
||
|
[field_name],
|
||
|
field_name_list_function(entity_list),
|
||
|
field_type,
|
||
|
single=True)
|
||
|
for id_ in entity_list:
|
||
|
fields = entity_objects[id_][-1]
|
||
|
if field_name not in fields:
|
||
|
self._warning(
|
||
|
field_type + ' not defined.', 'The given %s "%s" is not '
|
||
|
'defined on %s %s. It cannot '
|
||
|
'be renamed.' %
|
||
|
(field_type, field_name, entity_type, str(id_)))
|
||
|
continue
|
||
|
if field_name == new_field_name:
|
||
|
continue
|
||
|
if new_field_name in fields:
|
||
|
self._exists_on_entity_warning(new_field_name, field_type, id_,
|
||
|
entity_type)
|
||
|
fields[new_field_name] = fields[field_name]
|
||
|
del fields[field_name]
|
||
|
|
||
|
def rename_element_field(self,
|
||
|
element_field_name,
|
||
|
new_element_field_name,
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Rename an element field.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_element_field('p', 'pressure')
|
||
|
|
||
|
"""
|
||
|
self._rename_field_on_entity('element field', element_field_name,
|
||
|
new_element_field_name,
|
||
|
self.get_element_field_names,
|
||
|
'element block', element_block_ids,
|
||
|
self.get_element_block_ids,
|
||
|
self.element_blocks)
|
||
|
|
||
|
def rename_side_set_field(self,
|
||
|
side_set_field_name,
|
||
|
new_side_set_field_name,
|
||
|
side_set_ids='all'):
|
||
|
"""
|
||
|
Rename a side set field.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_side_set_field('cp', 'contact_pressure')
|
||
|
|
||
|
"""
|
||
|
self._rename_field_on_entity('side set field', side_set_field_name,
|
||
|
new_side_set_field_name,
|
||
|
self.get_side_set_field_names, 'side set',
|
||
|
side_set_ids, self.get_side_set_ids,
|
||
|
self.side_sets)
|
||
|
|
||
|
def get_side_set_name(self, side_set_id):
|
||
|
"""Return the name of a side set."""
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
return self.side_sets[side_set_id][0]
|
||
|
|
||
|
def get_all_side_set_names(self):
|
||
|
"""Return a list of all side set names."""
|
||
|
return sorted(x[0] for x in list(self.side_sets.values()) if x[0])
|
||
|
|
||
|
def get_side_set_members(self, side_set_id):
|
||
|
"""Return the members of a side set."""
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
return self.side_sets[side_set_id][1]
|
||
|
|
||
|
def _get_side_set_fields(self, side_set_id):
|
||
|
"""Return the dictionary of side set fields."""
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
return self.side_sets[side_set_id][-1]
|
||
|
|
||
|
def rename_node_set_field(self,
|
||
|
node_set_field_name,
|
||
|
new_node_set_field_name,
|
||
|
node_set_ids='all'):
|
||
|
"""
|
||
|
Rename a node set field.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_node_set_field('cp', 'contact_pressure')
|
||
|
|
||
|
"""
|
||
|
self._rename_field_on_entity('node set field', node_set_field_name,
|
||
|
new_node_set_field_name,
|
||
|
self.get_node_set_field_names, 'node set',
|
||
|
node_set_ids, self.get_node_set_ids,
|
||
|
self.node_sets)
|
||
|
|
||
|
def _rename_entity(self, entity_type, entity_name, new_entity_name,
|
||
|
entity_name_list_function, entity_objects):
|
||
|
"""Rename an entity."""
|
||
|
[entity_name] = self._format_id_list([entity_name],
|
||
|
entity_name_list_function(),
|
||
|
entity_type,
|
||
|
single=True)
|
||
|
if new_entity_name == entity_name:
|
||
|
return
|
||
|
if new_entity_name in entity_name_list_function():
|
||
|
self._exists_warning(new_entity_name, entity_type)
|
||
|
entity_objects[new_entity_name] = entity_objects[entity_name]
|
||
|
del entity_objects[entity_name]
|
||
|
|
||
|
def rename_node_field(self, node_field_name, new_node_field_name):
|
||
|
"""
|
||
|
Rename a node field.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_node_field('temp', 'temperature')
|
||
|
|
||
|
"""
|
||
|
self._rename_entity('node field', node_field_name, new_node_field_name,
|
||
|
self.get_node_field_names, self.node_fields)
|
||
|
|
||
|
def rename_global_variable(self, global_variable_name,
|
||
|
new_global_variable_name):
|
||
|
"""
|
||
|
Rename a global variable.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_global_variable('ke', 'kinetic_energy')
|
||
|
|
||
|
"""
|
||
|
self._rename_entity('global variable', global_variable_name,
|
||
|
new_global_variable_name,
|
||
|
self.get_global_variable_names,
|
||
|
self.global_variables)
|
||
|
|
||
|
def rename_element_block(self, element_block_id, new_element_block_id):
|
||
|
"""
|
||
|
Change an element block id or name.
|
||
|
|
||
|
This function can be used to change either the element block id or
|
||
|
name. If 'new_element_block_id' is an integer, it will change the id.
|
||
|
If it is a string, it will change the name.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_element_block(1, 100)
|
||
|
>>> model.rename_element_block(1, 'block_1')
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
# if we're just changing the name
|
||
|
if isinstance(new_element_block_id, str):
|
||
|
# if the same name already, just exit
|
||
|
if (self.element_blocks[element_block_id][0] ==
|
||
|
new_element_block_id):
|
||
|
return
|
||
|
# if the name already exists, issue a warning
|
||
|
if self.element_block_exists(new_element_block_id):
|
||
|
self._exists_warning('"' + new_element_block_id + '"',
|
||
|
'element block')
|
||
|
# rename it
|
||
|
self.element_blocks[element_block_id][0] = new_element_block_id
|
||
|
return
|
||
|
assert isinstance(new_element_block_id, int)
|
||
|
# rename the block
|
||
|
self._rename_entity('element block', element_block_id,
|
||
|
new_element_block_id, self.get_element_block_ids,
|
||
|
self.element_blocks)
|
||
|
# adjust side sets
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
new_members = []
|
||
|
for member in members:
|
||
|
if member[0] == element_block_id:
|
||
|
member = list(member)
|
||
|
member[0] = new_element_block_id
|
||
|
member = tuple(member)
|
||
|
new_members.append(member)
|
||
|
members[:] = new_members
|
||
|
|
||
|
def rename_node_set(self, node_set_id, new_node_set_id):
|
||
|
"""
|
||
|
Change a node set id or name.
|
||
|
|
||
|
This function can be used to change either the node set id or name.
|
||
|
If 'new_node_set_id' is an integer, it will change the id. If it is a
|
||
|
string, it will change the name.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_node_set(1, 100)
|
||
|
>>> model.rename_node_set(1, 'node_group_1')
|
||
|
|
||
|
"""
|
||
|
[node_set_id] = self._format_node_set_id_list([node_set_id], single=True)
|
||
|
# if we're just changing the name:
|
||
|
if isinstance(new_node_set_id, str):
|
||
|
# if the same name already, just exit
|
||
|
if self.get_node_set_name(node_set_id) == new_node_set_id:
|
||
|
return
|
||
|
# if the name already exists, issue a warning
|
||
|
if self.node_set_exists(new_node_set_id):
|
||
|
self._exists_warning('"' + new_node_set_id + '"', 'node set')
|
||
|
# rename it
|
||
|
self.node_sets[node_set_id][0] = new_node_set_id
|
||
|
return
|
||
|
# rename it
|
||
|
self._rename_entity('node set', node_set_id, new_node_set_id,
|
||
|
self.get_node_set_ids, self.node_sets)
|
||
|
|
||
|
def rename_side_set(self, side_set_id, new_side_set_id):
|
||
|
"""
|
||
|
Change a side set id or name.
|
||
|
|
||
|
This function can be used to change either the side set id or name.
|
||
|
If 'new_side_set_id' is an integer, it will change the id. If it is a
|
||
|
string, it will change the name.
|
||
|
|
||
|
Example:
|
||
|
>>> model.rename_side_set(1, 100)
|
||
|
>>> model.rename_side_set(1, 'surface_1')
|
||
|
|
||
|
"""
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
# if we're just changing the name:
|
||
|
if isinstance(new_side_set_id, str):
|
||
|
# if the same name already, just exit
|
||
|
if self.get_side_set_name(side_set_id) == new_side_set_id:
|
||
|
return
|
||
|
# if the name already exists, issue a warning
|
||
|
if self.side_set_exists(new_side_set_id):
|
||
|
self._exists_warning('"' + new_side_set_id + '"', 'side set')
|
||
|
# rename it
|
||
|
self.side_sets[side_set_id][0] = new_side_set_id
|
||
|
return
|
||
|
self._rename_entity('side set', side_set_id, new_side_set_id,
|
||
|
self.get_side_set_ids, self.side_sets)
|
||
|
|
||
|
def _empty_field_warning(self):
|
||
|
"""Issue a warning if no timesteps exist."""
|
||
|
assert not self.get_timesteps()
|
||
|
self._warning(
|
||
|
'Creating an empty field',
|
||
|
'No timesteps are defined. Because fields are only '
|
||
|
'defined at each timestep, creating a field when no '
|
||
|
'timesteps are defined will allow let the field to '
|
||
|
'have any values. By default, empty fields are not '
|
||
|
'exported.\n'
|
||
|
'\n'
|
||
|
'To avoid this warning, create a timestep before '
|
||
|
'creating a field.')
|
||
|
|
||
|
def create_element_field(self,
|
||
|
element_field_name,
|
||
|
element_block_ids='all',
|
||
|
value='auto'):
|
||
|
"""
|
||
|
Create an element field on one or more element blocks.
|
||
|
|
||
|
A default value can be passed. If no default value is given, 0.0 will
|
||
|
be used if the element field appears to be a displacement field.
|
||
|
Otherwise, NaN will be used.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_element_field('eqps', value=0.0)
|
||
|
|
||
|
"""
|
||
|
# warn if no timesteps are present
|
||
|
if not self.get_timesteps():
|
||
|
self._empty_field_warning()
|
||
|
# get list of block ids
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# get the value to assign
|
||
|
if value == 'auto':
|
||
|
value = self._get_default_field_value(element_field_name)
|
||
|
# for each block
|
||
|
for element_block_id in element_block_ids:
|
||
|
# if it exists, no need to do anything
|
||
|
if self.element_field_exists(element_field_name, element_block_id):
|
||
|
self._exists_on_entity_warning(element_field_name,
|
||
|
'element field',
|
||
|
element_block_id,
|
||
|
'element block')
|
||
|
# get the element block info
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
# create an empty field
|
||
|
values = []
|
||
|
for _ in range(len(self.timesteps)):
|
||
|
values.append([value] * element_count)
|
||
|
# add the field
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
fields[element_field_name] = values
|
||
|
|
||
|
def create_node_field(self, node_field_name, value='auto'):
|
||
|
"""
|
||
|
Create a node field and assign it a default value.
|
||
|
|
||
|
A default value can be passed. If no default value is given, 0.0 will
|
||
|
be used if the element field appears to be a displacement field.
|
||
|
Otherwise, NaN will be used.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_node_field('temperature', 298.15)
|
||
|
|
||
|
"""
|
||
|
# issue warning if no timesteps exist
|
||
|
if not self.get_timesteps():
|
||
|
self._empty_field_warning()
|
||
|
# if it exists, no need to do anything
|
||
|
if self.node_field_exists(node_field_name):
|
||
|
self._exists_warning(node_field_name, 'node field')
|
||
|
return
|
||
|
# get the value
|
||
|
if value == 'auto':
|
||
|
value = self._get_default_field_value(node_field_name)
|
||
|
# create the new field
|
||
|
new_field_values = []
|
||
|
for _ in range(len(self.timesteps)):
|
||
|
new_field_values.append([value] * len(self.nodes))
|
||
|
self.node_fields[node_field_name] = new_field_values
|
||
|
|
||
|
def create_node_set_field(self,
|
||
|
node_set_field_name,
|
||
|
node_set_ids='all',
|
||
|
value='auto'):
|
||
|
"""
|
||
|
Create a node set field on the given node sets.
|
||
|
|
||
|
A default value can be passed. If no default value is given, 0.0 will
|
||
|
be used if the element field appears to be a displacement field.
|
||
|
Otherwise, NaN will be used.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_node_set_field('temperature', 13, 298.15)
|
||
|
|
||
|
"""
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
if value == 'auto':
|
||
|
value = self._get_default_field_value(node_set_field_name)
|
||
|
for node_set_id in node_set_ids:
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
member_count = len(members)
|
||
|
# if it exists, warn about overwriting
|
||
|
if self.node_set_field_exists(node_set_field_name, [node_set_id]):
|
||
|
self._exists_on_entity_warning(node_set_field_name,
|
||
|
'node set field', node_set_id,
|
||
|
'node set')
|
||
|
# create the new field
|
||
|
new_field_values = []
|
||
|
for _ in range(len(self.timesteps)):
|
||
|
new_field_values.append([value] * member_count)
|
||
|
fields[node_set_field_name] = new_field_values
|
||
|
|
||
|
def create_side_set_field(self,
|
||
|
side_set_field_name,
|
||
|
side_set_ids='all',
|
||
|
value='auto'):
|
||
|
"""
|
||
|
Create a side set field.
|
||
|
|
||
|
By default the field will be created on all side sets. To create a
|
||
|
side set field on a particular field, pass in 'side_set_ids'.
|
||
|
|
||
|
To set the value of the field, pass in 'value'. By default this is
|
||
|
0 for displacement fields and NaN for all other fields.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_side_set_field('temperature', 13, 298.15)
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
if value == 'auto':
|
||
|
value = self._get_default_field_value(side_set_field_name)
|
||
|
for side_set_id in side_set_ids:
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
member_count = len(members)
|
||
|
# if it exists, warn about overwriting
|
||
|
if self.side_set_field_exists(side_set_field_name, side_set_id):
|
||
|
self._exists_on_entity_warning(side_set_field_name,
|
||
|
'side set field', side_set_id,
|
||
|
'side set')
|
||
|
# create the new field
|
||
|
new_field_values = []
|
||
|
for _ in range(len(self.timesteps)):
|
||
|
new_field_values.append([value] * member_count)
|
||
|
fields[side_set_field_name] = new_field_values
|
||
|
|
||
|
def create_node_set_from_side_set(self, node_set_id, side_set_id):
|
||
|
"""
|
||
|
Create a node set from a side set.
|
||
|
|
||
|
The created node set will contain all nodes used by faces in the given
|
||
|
side set.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_node_set_from_side_set(1, 1)
|
||
|
|
||
|
"""
|
||
|
node_set_members = self.get_nodes_in_side_set(side_set_id)
|
||
|
self.create_node_set(node_set_id, node_set_members)
|
||
|
|
||
|
@staticmethod
|
||
|
def _remove_duplicates(item_list, other_list=None, preserve_order=False):
|
||
|
"""
|
||
|
Return a list with duplicates removed.
|
||
|
|
||
|
If 'other_list' is used, this will also remove items which appear in
|
||
|
that list.
|
||
|
|
||
|
To preserve the relative order of items, set 'preserve_order=True'.
|
||
|
|
||
|
Example:
|
||
|
>>> _remove_duplicates([1, 3, 2, 3])
|
||
|
[1, 3, 2]
|
||
|
>>> _remove_duplicates([1, 3, 2, 3], [3])
|
||
|
[1, 2]
|
||
|
|
||
|
"""
|
||
|
if not preserve_order:
|
||
|
if not other_list:
|
||
|
return list(set(item_list))
|
||
|
return list(set(item_list) - set(other_list))
|
||
|
if other_list:
|
||
|
appeared = set(other_list)
|
||
|
else:
|
||
|
appeared = set()
|
||
|
unique_list = []
|
||
|
for item in item_list:
|
||
|
if item not in appeared:
|
||
|
unique_list.append(item)
|
||
|
appeared.add(item)
|
||
|
return unique_list
|
||
|
|
||
|
def create_node_set(self, node_set_id, node_set_members=None):
|
||
|
"""
|
||
|
Create a node side from the list of node indices.
|
||
|
|
||
|
Node sets are unnamed when created. To name them, use the
|
||
|
'rename_node_set' function.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_node_set(1, [0, 1, 2, 3])
|
||
|
|
||
|
"""
|
||
|
# if it exists, warn that it will be overwritten
|
||
|
if self.node_set_exists(node_set_id):
|
||
|
self._exists_warning(node_set_id, 'node set')
|
||
|
# reformat members if necessary
|
||
|
if not node_set_members:
|
||
|
node_set_members = []
|
||
|
# do not allow duplicates
|
||
|
new_members = self._remove_duplicates(node_set_members,
|
||
|
preserve_order=True)
|
||
|
if len(new_members) != len(node_set_members):
|
||
|
self._warning(
|
||
|
'Duplicate nodes in set.',
|
||
|
'The node set member list contains multiple copies '
|
||
|
'of some nodes. These nodes will only be added '
|
||
|
'once.')
|
||
|
# add the new set
|
||
|
self.node_sets[node_set_id] = ['', new_members, {}]
|
||
|
|
||
|
def add_nodes_to_node_set(self, node_set_id, new_node_set_members):
|
||
|
"""
|
||
|
Add nodes to an existing node set.
|
||
|
|
||
|
Example:
|
||
|
>>> model.add_nodes_to_node_set(1, [4, 5, 6, 7])
|
||
|
|
||
|
"""
|
||
|
if not self.node_set_exists(node_set_id):
|
||
|
self._warning('Node set does not exist.'
|
||
|
'The specified node set "%s% does not exist. It '
|
||
|
'will be created.' % node_set_id)
|
||
|
self.create_node_set(node_set_id, new_node_set_members)
|
||
|
return
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
# Remove duplicate nodes.
|
||
|
new_nodes = self._remove_duplicates(new_node_set_members,
|
||
|
other_list=members,
|
||
|
preserve_order=True)
|
||
|
if len(new_nodes) != len(new_node_set_members):
|
||
|
self._warning(
|
||
|
'Duplicates nodes in set',
|
||
|
'The node set already contains some of the nodes '
|
||
|
'in the list. These members will not be '
|
||
|
'duplicated.')
|
||
|
# Add the members.
|
||
|
members.extend(new_nodes)
|
||
|
# Add new values to each field.
|
||
|
for name, all_values in list(fields.items()):
|
||
|
value = self._get_default_field_value(name)
|
||
|
for values in all_values:
|
||
|
values.extend([value] * len(new_nodes))
|
||
|
|
||
|
def _transform_expression(self, expression):
|
||
|
"""
|
||
|
Transform the expression into one that evaluates to 0 when true.
|
||
|
|
||
|
For example, 'x=y' becomes '(x)-(y)'.
|
||
|
|
||
|
"""
|
||
|
# create transformation dictionary by precedence order
|
||
|
transforms = []
|
||
|
transforms.append(('||', 'min(abs(L), abs(R))'))
|
||
|
transforms.append(('&&', 'max(abs(L), abs(R))'))
|
||
|
transforms.append(('>=', '((L) - (R)) - abs((L) - (R))'))
|
||
|
transforms.append(('>', '((L) - (R)) - abs((L) - (R))'))
|
||
|
transforms.append(('<=', '((R) - (L)) - abs((R) - (L))'))
|
||
|
transforms.append(('<', '((R) - (L)) - abs((R) - (L))'))
|
||
|
transforms.append(('==', 'abs((L) - (R))'))
|
||
|
transforms.append(('=', 'abs((L) - (R))'))
|
||
|
# replace occurrences of each transform
|
||
|
for separator, transform in transforms:
|
||
|
while separator in expression:
|
||
|
# ensure parenthesis count is identical
|
||
|
if expression.count('(') != expression.count(')'):
|
||
|
self._error(
|
||
|
'Unbalances parenthesis.',
|
||
|
'We cannot transform the given expression '
|
||
|
'becuase it contains unbalanced '
|
||
|
'parenthesis:\n%s' % expression)
|
||
|
# get parenthesis depth on each character
|
||
|
next_depth = 0
|
||
|
depth = []
|
||
|
for c in expression:
|
||
|
if c == '(':
|
||
|
depth.append(next_depth)
|
||
|
next_depth += 1
|
||
|
elif c == ')':
|
||
|
next_depth -= 1
|
||
|
depth.append(next_depth)
|
||
|
else:
|
||
|
depth.append(next_depth)
|
||
|
# find extents of the inner expression
|
||
|
separator_index = expression.index(separator)
|
||
|
left_index = separator_index
|
||
|
right_index = separator_index + len(separator)
|
||
|
while left_index > 0 and (
|
||
|
depth[left_index - 1] > depth[separator_index] or
|
||
|
(depth[left_index - 1] == depth[separator_index]
|
||
|
and expression[left_index - 1] != ',')):
|
||
|
left_index -= 1
|
||
|
while right_index < len(expression) and (
|
||
|
depth[right_index] > depth[separator_index] or
|
||
|
(depth[right_index] == depth[separator_index]
|
||
|
and expression[right_index] != ',')):
|
||
|
right_index += 1
|
||
|
new_expression = expression[:left_index]
|
||
|
left_expression = expression[left_index:separator_index].strip(
|
||
|
)
|
||
|
right_expression = expression[separator_index +
|
||
|
len(separator
|
||
|
):right_index].strip()
|
||
|
for c in transform:
|
||
|
if c == 'L':
|
||
|
new_expression += left_expression
|
||
|
elif c == 'R':
|
||
|
new_expression += right_expression
|
||
|
else:
|
||
|
new_expression += c
|
||
|
new_expression += expression[right_index:]
|
||
|
expression = new_expression
|
||
|
# return the result
|
||
|
return expression
|
||
|
|
||
|
def create_side_set_from_expression(self,
|
||
|
side_set_id,
|
||
|
expression,
|
||
|
element_block_ids='all',
|
||
|
tolerance='auto',
|
||
|
timesteps='last_if_any',
|
||
|
zero_member_warning=True):
|
||
|
"""
|
||
|
Create a side set from faces which satisfy an expression.
|
||
|
|
||
|
Only external element faces of the given element blocks are checked.
|
||
|
|
||
|
For example, if the model had a symmetry plane at 'Y = 0', the
|
||
|
expression 'Y == 0' would select all element faces on this plane.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_side_set_from_expression(1, 'Y == 0')
|
||
|
|
||
|
"""
|
||
|
timesteps = self._format_id_list(timesteps, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
if len(timesteps) > 2:
|
||
|
self._error(
|
||
|
'Too many timesteps specified.',
|
||
|
'We were expecting zero or one timesteps but instead '
|
||
|
'found %d specified.' % len(timesteps))
|
||
|
if tolerance == 'auto':
|
||
|
tolerance = self.get_length_scale() * 1e-6
|
||
|
if self.side_set_exists(side_set_id):
|
||
|
self._exists_warning(side_set_id, 'side set')
|
||
|
external_faces_by_block = self._order_element_faces_by_block(
|
||
|
self._get_external_element_faces(element_block_ids))
|
||
|
members = []
|
||
|
# create list of variable names and modify them in the expression
|
||
|
variable_names = set(['X', 'Y', 'Z'])
|
||
|
if timesteps:
|
||
|
timestep_index = self._get_internal_timestep_index(timesteps[0])
|
||
|
variable_names.update(['time'])
|
||
|
variable_names.update(self.get_global_variable_names())
|
||
|
variable_names.update(self.get_node_field_names())
|
||
|
expression = self._transform_expression(expression)
|
||
|
eval_expression = self._transform_eval_expression(
|
||
|
expression, variable_names)
|
||
|
var = dict()
|
||
|
function = eval('lambda var: ' + eval_expression)
|
||
|
if timesteps:
|
||
|
var['time'] = timesteps[0]
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
var[name] = values[timestep_index]
|
||
|
try:
|
||
|
for id_, external_faces in list(external_faces_by_block.items()):
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
face_mapping = self._get_face_mapping(
|
||
|
self._get_element_type(id_))
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
for element_index, face_index in external_faces:
|
||
|
node_indices = [
|
||
|
connectivity[element_index * nodes_per_element + x]
|
||
|
for x in face_mapping[face_index][1]
|
||
|
]
|
||
|
all_passed = True
|
||
|
for node_index in node_indices:
|
||
|
# set coordinate values
|
||
|
var['X'] = self.nodes[node_index][0]
|
||
|
var['Y'] = self.nodes[node_index][1]
|
||
|
var['Z'] = self.nodes[node_index][2]
|
||
|
# set node field values
|
||
|
if timesteps:
|
||
|
for name, values in list(self.node_fields.items()):
|
||
|
var[name] = values[timestep_index][node_index]
|
||
|
value = float(function(var))
|
||
|
if not abs(value) <= tolerance:
|
||
|
all_passed = False
|
||
|
break
|
||
|
if all_passed:
|
||
|
members.append((id_, element_index, face_index))
|
||
|
except (SyntaxError, NameError):
|
||
|
self._error_evaluating_expression("%s" % (eval_expression), var)
|
||
|
# create the side set
|
||
|
self.create_side_set(side_set_id, members)
|
||
|
if zero_member_warning and not members:
|
||
|
self._warning(
|
||
|
'Empty side set.',
|
||
|
'No external element faces satisfied the given '
|
||
|
'expression. An empty side set was created.')
|
||
|
|
||
|
def create_side_set(self, side_set_id, side_set_members=None):
|
||
|
"""
|
||
|
Create a side set from the given element faces.
|
||
|
|
||
|
If the side set already exists, the faces will be added.
|
||
|
|
||
|
side_set_members should be a list of tuples of the form:
|
||
|
* '(element_block_id, local_element_index, element_side_index)'
|
||
|
|
||
|
Side sets are unnamed when created. To name them, use the
|
||
|
'rename_side_set' function.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_side_set(1, [(1, 0, 1), (1, 0, 2)])
|
||
|
|
||
|
"""
|
||
|
if not side_set_members:
|
||
|
side_set_members = []
|
||
|
# ensure set set id doesn't exist
|
||
|
if self.side_set_exists(side_set_id):
|
||
|
self._exists_warning(side_set_id, 'side set')
|
||
|
unique_members = self._remove_duplicates(side_set_members,
|
||
|
preserve_order=True)
|
||
|
if len(unique_members) != len(side_set_members):
|
||
|
self._warning(
|
||
|
'Duplicate faces in set.',
|
||
|
'The face set member list contains multiple copies '
|
||
|
'of some faces. These faces will only be added '
|
||
|
'once.')
|
||
|
self.side_sets[side_set_id] = ['', unique_members, {}]
|
||
|
|
||
|
def add_faces_to_side_set(self, side_set_id, new_side_set_members):
|
||
|
"""
|
||
|
Add the given faces to the side set.
|
||
|
|
||
|
The parameter 'new_side_set_members' should be a list of tuples of the
|
||
|
form:
|
||
|
* '(element_block_id, local_element_index, element_side_index)'
|
||
|
|
||
|
Example:
|
||
|
>>> model.add_faces_to_side_set(1, [(2, 0, 3), (3, 0, 4)])
|
||
|
|
||
|
"""
|
||
|
if not self.side_set_exists(side_set_id):
|
||
|
self._warning('Side set does not exist.'
|
||
|
'The specified side set "%s% does not exist. It '
|
||
|
'will be created.' % side_set_id)
|
||
|
self.create_side_set(side_set_id, new_side_set_members)
|
||
|
return
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
# remove duplicate faces
|
||
|
new_members = self._remove_duplicates(new_side_set_members,
|
||
|
other_list=members,
|
||
|
preserve_order=True)
|
||
|
if len(new_members) != len(new_side_set_members):
|
||
|
self._warning(
|
||
|
'Duplicates faces in set',
|
||
|
'The node set already contains some nodes of the '
|
||
|
'faces in the list to add. These members will not '
|
||
|
'be duplicated.')
|
||
|
# add the members
|
||
|
members.extend(new_members)
|
||
|
# add new values to each field
|
||
|
for name, all_values in list(fields.items()):
|
||
|
value = self._get_default_field_value(name)
|
||
|
for values in all_values:
|
||
|
values.extend([value] * len(new_members))
|
||
|
|
||
|
def delete_node_field(self, node_field_names):
|
||
|
"""
|
||
|
Delete one or more node fields.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_node_field('temperature')
|
||
|
>>> model.delete_node_field('all')
|
||
|
>>> model.delete_node_field('disp_*')
|
||
|
|
||
|
"""
|
||
|
node_field_names = self._format_id_list(node_field_names,
|
||
|
self.get_node_field_names(),
|
||
|
'node field')
|
||
|
for node_field_name in node_field_names:
|
||
|
del self.node_fields[node_field_name]
|
||
|
|
||
|
def delete_timestep(self, timesteps):
|
||
|
"""
|
||
|
Delete one or more timesteps.
|
||
|
|
||
|
Because fields are defined on each timestep, this also deletes field
|
||
|
values for the corresponding timestep.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_timestep(0.0)
|
||
|
>>> model.delete_timestep('all')
|
||
|
|
||
|
"""
|
||
|
timesteps = self._format_id_list(timesteps, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
# get list of deleted indices in descending order
|
||
|
deleted_indices = reversed(
|
||
|
sorted([self.timesteps.index(x) for x in timesteps]))
|
||
|
for index in deleted_indices:
|
||
|
# process element fields
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
for values in list(fields.values()):
|
||
|
del values[index]
|
||
|
# process node fields
|
||
|
for values in list(self.node_fields.values()):
|
||
|
del values[index]
|
||
|
# process side set fields
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
for values in list(fields.values()):
|
||
|
del values[index]
|
||
|
# process node set fields
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
for values in list(fields.values()):
|
||
|
del values[index]
|
||
|
# process global variables
|
||
|
for values in list(self.global_variables.values()):
|
||
|
del values[index]
|
||
|
# delete the timestep
|
||
|
del self.timesteps[index]
|
||
|
|
||
|
def delete_element_block(self,
|
||
|
element_block_ids,
|
||
|
delete_orphaned_nodes=True):
|
||
|
"""
|
||
|
Delete one or more element blocks.
|
||
|
|
||
|
This will also delete any references to elements in that block in
|
||
|
side sets.
|
||
|
|
||
|
By default, this will delete any nodes that become unused as a result
|
||
|
of deleting the element blocks. To prevent this, set
|
||
|
'delete_orphaned_nodes=False'.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_element_block(1)
|
||
|
>>> model.delete_element_block([1, 3, 4])
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# if we're not deleting anything, skip some computations.
|
||
|
if not element_block_ids:
|
||
|
return
|
||
|
# find unreferenced nodes
|
||
|
if delete_orphaned_nodes:
|
||
|
unreferenced_nodes = self._get_unreferenced_nodes()
|
||
|
# delete the element blocks and associated data
|
||
|
for element_block_id in element_block_ids:
|
||
|
# delete the element block itself
|
||
|
del self.element_blocks[element_block_id]
|
||
|
# delete faces of that element block from side set
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
# find indices to delete
|
||
|
deleted_indices = []
|
||
|
for index, (id_, _, _) in enumerate(members):
|
||
|
if id_ == element_block_id:
|
||
|
deleted_indices.append(index)
|
||
|
# delete them from members
|
||
|
for index in reversed(deleted_indices):
|
||
|
del members[index]
|
||
|
# delete them from the fields
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
for index in reversed(deleted_indices):
|
||
|
del values[index]
|
||
|
# now find the new unreferenced nodes
|
||
|
if delete_orphaned_nodes:
|
||
|
new_unreferenced_nodes = self._get_unreferenced_nodes()
|
||
|
nodes_to_delete = sorted(
|
||
|
set(new_unreferenced_nodes) - set(unreferenced_nodes))
|
||
|
if nodes_to_delete:
|
||
|
self._delete_nodes(nodes_to_delete)
|
||
|
|
||
|
def delete_element_field(self,
|
||
|
element_field_names,
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Delete one or more element fields.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_element_field('eqps')
|
||
|
>>> model.delete_element_field('all')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
element_field_names = self._format_id_list(
|
||
|
element_field_names, self.get_element_field_names(),
|
||
|
'element field')
|
||
|
# for each field
|
||
|
for element_field_name in element_field_names:
|
||
|
any_deleted = False
|
||
|
# for each element block
|
||
|
for element_block_id in element_block_ids:
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
if element_field_name in fields:
|
||
|
any_deleted = True
|
||
|
del fields[element_field_name]
|
||
|
if not any_deleted:
|
||
|
self._warning(
|
||
|
'Element field not defined.',
|
||
|
'The element field "%s" was not defined on any '
|
||
|
'of the given element blocks. It cannot be '
|
||
|
'deleted.' % element_field_name)
|
||
|
|
||
|
def delete_node_set_field(self, node_set_field_names, node_set_ids='all'):
|
||
|
"""
|
||
|
Delete one or more node set fields.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_node_set_field('contact_pressure', 1)
|
||
|
|
||
|
"""
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
node_set_field_names = self._format_id_list(
|
||
|
node_set_field_names, self.get_node_set_field_names(),
|
||
|
'node set field')
|
||
|
# for each field
|
||
|
for name in node_set_field_names:
|
||
|
any_deleted = False
|
||
|
# for each node set
|
||
|
for node_set_id in node_set_ids:
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
if name in fields:
|
||
|
any_deleted = True
|
||
|
del fields[name]
|
||
|
if not any_deleted:
|
||
|
self._warning(
|
||
|
'Node set field not defined.',
|
||
|
'The node set field "%s" was not defined on any '
|
||
|
'of the given node sets. It cannot be '
|
||
|
'deleted.' % name)
|
||
|
|
||
|
def delete_side_set_field(self, side_set_field_names, side_set_ids='all'):
|
||
|
"""
|
||
|
Delete one or more side set fields.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_side_set_field('contact_pressure', 1)
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
side_set_field_names = self._format_id_list(
|
||
|
side_set_field_names, self.get_side_set_field_names(),
|
||
|
'side set field')
|
||
|
# for each field
|
||
|
for name in side_set_field_names:
|
||
|
any_deleted = False
|
||
|
# for each node set
|
||
|
for id_ in side_set_ids:
|
||
|
fields = self._get_side_set_fields(id_)
|
||
|
if name in fields:
|
||
|
any_deleted = True
|
||
|
del fields[name]
|
||
|
if not any_deleted:
|
||
|
self._warning(
|
||
|
'Side set field not defined.',
|
||
|
'The side set field "%s" was not defined on any '
|
||
|
'of the given side sets. It cannot be '
|
||
|
'deleted.' % name)
|
||
|
|
||
|
def global_variable_exists(self, global_variable_name):
|
||
|
"""
|
||
|
Return 'True' if the given global variable exists.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.global_variable_exists('timestep')
|
||
|
|
||
|
"""
|
||
|
return global_variable_name in self.global_variables
|
||
|
|
||
|
def side_set_field_exists(self, side_set_field_name, side_set_ids='all'):
|
||
|
"""
|
||
|
Return 'True' if the side set field exists on the side sets.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.side_set_field_exists('contact_pressure')
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
for id_ in side_set_ids:
|
||
|
fields = self._get_side_set_fields(id_)
|
||
|
if side_set_field_name not in fields:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def node_set_field_exists(self, node_set_field_name, node_set_ids='all'):
|
||
|
"""
|
||
|
Return 'True' if the node set field is defined on the node sets.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.node_set_field_exists('contact_pressure')
|
||
|
|
||
|
"""
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
for node_set_id in node_set_ids:
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
if node_set_field_name not in fields:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def element_field_exists(self, element_field_name,
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Return 'True' if the element field exists on the element blocks.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.element_field_exists('eqps')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
for id_ in element_block_ids:
|
||
|
fields = self.element_blocks[id_][-1]
|
||
|
if element_field_name not in fields:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def node_field_exists(self, node_field_name):
|
||
|
"""
|
||
|
Return 'True' if the given node field exists.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.node_field_exists('temperature')
|
||
|
|
||
|
"""
|
||
|
return node_field_name in self.node_fields
|
||
|
|
||
|
def element_block_exists(self, element_block_id):
|
||
|
"""
|
||
|
Return 'True' if the given element block exists.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.element_block_exists(1)
|
||
|
>>> model.element_block_exists('block_name')
|
||
|
|
||
|
"""
|
||
|
if isinstance(element_block_id, str):
|
||
|
return element_block_id in self.get_all_element_block_names()
|
||
|
return element_block_id in self.get_element_block_ids()
|
||
|
|
||
|
def node_set_exists(self, node_set_id):
|
||
|
"""
|
||
|
Return 'True' if the given node set exists.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.node_set_exists(1)
|
||
|
>>> model.node_set_exists('nodeset_name')
|
||
|
|
||
|
"""
|
||
|
if isinstance(node_set_id, str):
|
||
|
return node_set_id in self.get_all_node_set_names()
|
||
|
return node_set_id in self.get_node_set_ids()
|
||
|
|
||
|
def side_set_exists(self, side_set_id):
|
||
|
"""
|
||
|
Return 'True' if the given side set exists.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.side_set_exists(1)
|
||
|
>>> model.node_set_exists('sideset_name')
|
||
|
|
||
|
"""
|
||
|
if isinstance(side_set_id, str):
|
||
|
return side_set_id in self.get_all_side_set_names()
|
||
|
return side_set_id in self.get_side_set_ids()
|
||
|
|
||
|
def timestep_exists(self, timestep):
|
||
|
"""
|
||
|
Return 'True' if the given timestep exists.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.timestep_exists(0.0)
|
||
|
|
||
|
"""
|
||
|
return timestep in self.timesteps
|
||
|
|
||
|
def get_node_field_values(self, node_field_name, timestep='last'):
|
||
|
"""
|
||
|
Return the list of node field values for the given field and timestep.
|
||
|
|
||
|
This returns the actual list of values, so any modifications to the
|
||
|
list will be retained in the model.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.get_node_field_values('disp_x')
|
||
|
>>> model.get_node_field_values('disp_x', 0.0)
|
||
|
|
||
|
"""
|
||
|
[node_field_name] = self._format_id_list([node_field_name],
|
||
|
self.get_node_field_names(),
|
||
|
'node field',
|
||
|
single=True)
|
||
|
timestep_index = self._get_internal_timestep_index(timestep)
|
||
|
return self.node_fields[node_field_name][timestep_index]
|
||
|
|
||
|
def get_node_set_name(self, node_set_id):
|
||
|
"""Return the name of the given node set."""
|
||
|
[node_set_id] = self._format_node_set_id_list([node_set_id], single=True)
|
||
|
return self.node_sets[node_set_id][0]
|
||
|
|
||
|
def get_all_node_set_names(self):
|
||
|
"""Return a list of all node set names."""
|
||
|
return sorted(x[0] for x in list(self.node_sets.values()) if x[0])
|
||
|
|
||
|
def get_node_set_members(self, node_set_id):
|
||
|
"""
|
||
|
Return the list of node indices that belong to the given node set.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_node_set_members(1)
|
||
|
|
||
|
"""
|
||
|
[node_set_id] = self._format_node_set_id_list([node_set_id], single=True)
|
||
|
return self.node_sets[node_set_id][1]
|
||
|
|
||
|
def _get_face_mapping(self, element_name):
|
||
|
"""
|
||
|
Return the mapping from face number to node number for an element.
|
||
|
|
||
|
A list is returned with members of the form '(type, numbering)' where
|
||
|
'type' is a string giving the face element type and 'numbering' is a
|
||
|
list if element indices.
|
||
|
|
||
|
"""
|
||
|
element_type = self._get_standard_element_type(element_name)
|
||
|
if element_type not in self.FACE_MAPPING:
|
||
|
self._error(
|
||
|
'Undefined face mapping.',
|
||
|
'The mapping defining which nodes belong to a given '
|
||
|
'face has not been defined for the \"%s\" element '
|
||
|
'type. This could be because it has not been '
|
||
|
'implemented or because the element type is '
|
||
|
'invalid. Mappings for the following element types '
|
||
|
'are defined: %s' %
|
||
|
(element_name, ', '.join(list(self.FACE_MAPPING.keys()))))
|
||
|
return self.FACE_MAPPING[element_type]
|
||
|
|
||
|
def _get_face_mapping_from_id(self, element_block_id):
|
||
|
"""Return the face mapping from the element block id."""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
element_type = self._get_element_type(element_block_id)
|
||
|
element_type = self._get_standard_element_type(element_type)
|
||
|
return self._get_face_mapping(element_type)
|
||
|
|
||
|
def get_nodes_in_side_set(self, side_set_id):
|
||
|
"""
|
||
|
Return a list of node indices which belong to the given side set.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_nodes_in_side_set(1)
|
||
|
|
||
|
"""
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
included_nodes = list()
|
||
|
# store for element number in each element block
|
||
|
last_element_block_id = 0
|
||
|
nodes_per_element = 0
|
||
|
face_mapping = None
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
for block_id, element_index, face_index in members:
|
||
|
if block_id != last_element_block_id or face_mapping is None:
|
||
|
last_element_block_id = block_id
|
||
|
nodes_per_element = self.get_nodes_per_element(block_id)
|
||
|
face_mapping = self._get_face_mapping(
|
||
|
self._get_element_type(block_id))
|
||
|
connectivity = self.get_connectivity(block_id)
|
||
|
for local_index in face_mapping[face_index][1]:
|
||
|
included_nodes.append(
|
||
|
connectivity[element_index * nodes_per_element +
|
||
|
local_index])
|
||
|
# return list after deleting duplicates
|
||
|
included_nodes = sorted(set(included_nodes))
|
||
|
return included_nodes
|
||
|
|
||
|
def _invert_face(self, face, face_type):
|
||
|
"""
|
||
|
Return the connectivity of the inverted 2D face.
|
||
|
|
||
|
Example:
|
||
|
>>> model._invert_face(tuple(7, 2, 1, 3), 'quad4')
|
||
|
|
||
|
"""
|
||
|
return tuple(face[x] for x in self.INVERTED_CONNECTIVITY[face_type])
|
||
|
|
||
|
def _rotate_face(self, face, face_type):
|
||
|
"""
|
||
|
Return a rotation of the given 2D face.
|
||
|
|
||
|
Example:
|
||
|
>>> model._invert_face(tuple(7, 2, 1, 3), 'quad4')
|
||
|
|
||
|
"""
|
||
|
return tuple(face[x] for x in self.ROTATED_CONNECTIVITY[face_type])
|
||
|
|
||
|
def _minimum_face(self, face, face_type):
|
||
|
"""
|
||
|
Find a unique identifier for the given face.
|
||
|
|
||
|
This finds the local face connectivity list and rotates it until the
|
||
|
minimum such list is found.
|
||
|
|
||
|
"""
|
||
|
original = face
|
||
|
best = face
|
||
|
best_count = 0
|
||
|
face = self._rotate_face(face, face_type)
|
||
|
rotate_count = 0
|
||
|
while face != original:
|
||
|
rotate_count += 1
|
||
|
if face < best:
|
||
|
best = face
|
||
|
best_count = rotate_count
|
||
|
face = self._rotate_face(face, face_type)
|
||
|
return best, best_count
|
||
|
|
||
|
def _get_internal_faces(self, element_block_ids='all', set_of_nodes=None):
|
||
|
"""
|
||
|
Return a list of internal element faces.
|
||
|
|
||
|
If 'element_block_ids' is specified, only faces within those elements
|
||
|
will be considered. If 'set_of_nodes' is given, only faces which
|
||
|
contain one or more nodes in that set will be returned.
|
||
|
TODO: Not sure this handles 'element_block_ids' correctly.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# hold internal faces
|
||
|
internal_faces = dict()
|
||
|
# hold faces which need matched
|
||
|
faces_to_match = dict()
|
||
|
# form node mask if possible
|
||
|
if set_of_nodes is not None:
|
||
|
node_mask = [x in set_of_nodes for x in range(len(self.nodes))]
|
||
|
# go through each element block
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
# if we have a node mask, skip this block if no nodes in the block
|
||
|
# are part of the set of nodes we care about
|
||
|
if set_of_nodes is not None:
|
||
|
if set(connectivity).isdisjoint(set_of_nodes):
|
||
|
continue
|
||
|
face_mappings = self._get_face_mapping_from_id(id_)
|
||
|
element_count = self.get_element_count(id_)
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
for element_index in range(element_count):
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
for face_index, (face_type,
|
||
|
face_mapping) in enumerate(face_mappings):
|
||
|
this_member = (id_, element_index, face_index)
|
||
|
this_face = tuple(local_node[x] for x in face_mapping)
|
||
|
# skip the face if no nodes are relevant
|
||
|
if set_of_nodes is not None:
|
||
|
if not [True for x in this_face if node_mask[x]]:
|
||
|
continue
|
||
|
# transform into lowest form
|
||
|
unique_face, unique_offset = self._minimum_face(
|
||
|
this_face, face_type)
|
||
|
# match if possible
|
||
|
match = faces_to_match.pop((face_type, unique_face), None)
|
||
|
# if no match, add it
|
||
|
if match is None:
|
||
|
# invert and transform into lowest form
|
||
|
inverted_face = self._invert_face(this_face, face_type)
|
||
|
inverted_face, inverted_offset = self._minimum_face(
|
||
|
inverted_face, face_type)
|
||
|
# add it and continue
|
||
|
faces_to_match[(face_type,
|
||
|
inverted_face)] = (this_member,
|
||
|
inverted_offset)
|
||
|
continue
|
||
|
# set first mapping
|
||
|
transform = (match[1] - unique_offset) % len(this_face)
|
||
|
internal_faces[this_member] = (match[0], transform)
|
||
|
# set opposite mapping
|
||
|
internal_faces[match[0]] = (this_member, transform)
|
||
|
return internal_faces
|
||
|
|
||
|
def _get_face_indices(self, member):
|
||
|
"""
|
||
|
Return the indices of the given face.
|
||
|
|
||
|
'member = (element_block_id, element_index, face_index)'
|
||
|
|
||
|
This function is slow.
|
||
|
|
||
|
"""
|
||
|
face_mapping = self._get_face_mapping(self._get_element_type(
|
||
|
member[0]))
|
||
|
nodes_per_element = self.get_nodes_per_element(member[0])
|
||
|
connectivity = self.get_connectivity(member[0])
|
||
|
return [
|
||
|
connectivity[nodes_per_element * member[1] + x]
|
||
|
for x in face_mapping[member[2]][1]
|
||
|
]
|
||
|
|
||
|
def convert_side_set_to_cohesive_zone(self, side_set_ids,
|
||
|
new_element_block_id):
|
||
|
"""
|
||
|
Convert the given side sets into a block of cohesive zone elements.
|
||
|
|
||
|
The given side set must contain faces which are internal (i.e. shared
|
||
|
by exactly two elements). Support for the following face types is
|
||
|
implemented:
|
||
|
* 'quad4' creates 'hex8' cohesive elements
|
||
|
* 'tri3' creates 'wedge6' cohesive elements
|
||
|
* 'tri6' creates 'wedge12' cohesive elements
|
||
|
|
||
|
Example:
|
||
|
>>> model.convert_side_set_to_cohesive_zone(1, 2)
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
# if nothing to operate on, just return
|
||
|
if not side_set_ids:
|
||
|
return
|
||
|
# order all faces by element block
|
||
|
members_by_block = self._order_element_faces_by_block(
|
||
|
itertools.chain(
|
||
|
*[self.get_side_set_members(x) for x in side_set_ids]))
|
||
|
# find face shape
|
||
|
face_type = set()
|
||
|
element_types = set()
|
||
|
for id_, members in list(members_by_block.items()):
|
||
|
element_types.add(self._get_element_type(id_))
|
||
|
face_mapping = self._get_face_mapping_from_id(id_)
|
||
|
numbers = set(x for _, x in members)
|
||
|
face_type.update(face_mapping[x][0] for x in numbers)
|
||
|
# ensure single face type
|
||
|
if len(face_type) != 1:
|
||
|
self._error(
|
||
|
'Multiple face types',
|
||
|
'The side set contains multiple types of faces and '
|
||
|
'cannot be converted into a cohesive zone.')
|
||
|
# ensure face type is supported
|
||
|
face_type = sorted(face_type)[0]
|
||
|
if face_type not in self.COHESIVE_FORMULA:
|
||
|
self._bug(
|
||
|
'Unsupported face type',
|
||
|
'The side set chosen has elements of face type "%s". '
|
||
|
'At this time, only the following face types are '
|
||
|
'supported:\n* %s' %
|
||
|
(face_type, '\n* '.join(sorted(self.COHESIVE_FORMULA.keys()))))
|
||
|
# for each element type, find adjacent faces
|
||
|
# (adjacent faces in this context are faces which share one or more
|
||
|
# nodes)
|
||
|
adjacent_faces = dict()
|
||
|
element_types = sorted(element_types)
|
||
|
for element_type in element_types:
|
||
|
face_mapping = self._get_face_mapping(element_type)
|
||
|
face_count = len(face_mapping)
|
||
|
adjacent_face = [[] for i in range(face_count)]
|
||
|
for face_one in range(face_count):
|
||
|
for face_two in range(face_count):
|
||
|
if set(face_mapping[face_one][1]).intersection(
|
||
|
face_mapping[face_two][1]):
|
||
|
adjacent_face[face_one].append(face_two)
|
||
|
adjacent_faces[element_type] = adjacent_face
|
||
|
# create a set of all nodes in the side set
|
||
|
nodes_on_surface = set()
|
||
|
for side_set_id in side_set_ids:
|
||
|
nodes_on_surface.update(self.get_nodes_in_side_set(side_set_id))
|
||
|
# find all relevant internal faces
|
||
|
internal_faces = self._get_internal_faces(
|
||
|
list(members_by_block.keys()), set_of_nodes=nodes_on_surface)
|
||
|
# create a set of all free faces
|
||
|
free_faces = set()
|
||
|
for id_, members in list(members_by_block.items()):
|
||
|
for element_index, face_index in members:
|
||
|
face = (id_, element_index, face_index)
|
||
|
free_faces.add(face)
|
||
|
matching_face = internal_faces.get(face, None)
|
||
|
if not matching_face:
|
||
|
self._error(
|
||
|
'Invalid face',
|
||
|
'One or more members of the side set are '
|
||
|
'external faces which cannot be converted '
|
||
|
'to cohesive elements.')
|
||
|
free_faces.add(matching_face[0])
|
||
|
# count the number of times each node is used
|
||
|
node_use_count = [0] * len(self.nodes)
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
for node_index in connectivity:
|
||
|
node_use_count[node_index] += 1
|
||
|
# hold a list of all nodes to duplicate
|
||
|
nodes_to_duplicate = []
|
||
|
for node_index in nodes_on_surface:
|
||
|
nodes_to_duplicate.extend([node_index] *
|
||
|
node_use_count[node_index])
|
||
|
nodes_to_duplicate.sort()
|
||
|
# duplicate nodes
|
||
|
first_new_node_index = len(self.nodes)
|
||
|
new_indices = []
|
||
|
self._duplicate_nodes(nodes_to_duplicate, new_indices)
|
||
|
# for each duplicated node, find the first new node index
|
||
|
next_index = [None] * first_new_node_index
|
||
|
for old_index, new_index in zip(nodes_to_duplicate, new_indices):
|
||
|
if next_index[old_index] is None:
|
||
|
next_index[old_index] = new_index
|
||
|
# for faster lookup, create a vector to see if a given node
|
||
|
# is on the cohesive zone
|
||
|
node_in_zone = [
|
||
|
x in nodes_on_surface for x in range(first_new_node_index)
|
||
|
]
|
||
|
# for each element, unmerge nodes on the cohesive zone
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
for index, value in enumerate(connectivity):
|
||
|
if node_in_zone[value]:
|
||
|
connectivity[index] = next_index[value]
|
||
|
next_index[value] += 1
|
||
|
# find pairs of nodes which need merged
|
||
|
node_pairs = set()
|
||
|
for old_face, (new_face,
|
||
|
transform_count) in list(internal_faces.items()):
|
||
|
# since the list is duplicated, only operate on one
|
||
|
if old_face > new_face:
|
||
|
continue
|
||
|
# skip faces on the free surface
|
||
|
if old_face in free_faces:
|
||
|
continue
|
||
|
# merge these nodes
|
||
|
original = tuple(self._get_face_indices(old_face))
|
||
|
target = tuple(self._get_face_indices(new_face))
|
||
|
original = self._invert_face(original, face_type)
|
||
|
for _ in range(transform_count):
|
||
|
original = self._rotate_face(original, face_type)
|
||
|
node_pairs.update((min(x, y), max(x, y))
|
||
|
for x, y in zip(original, target) if x != y)
|
||
|
# merge nodes
|
||
|
self._merge_node_pairs(node_pairs)
|
||
|
# delete nodes we created that are no longer referenced as well as the
|
||
|
# original nodes which are no longer used
|
||
|
nodes_to_delete = self._get_unreferenced_nodes()
|
||
|
nodes_to_delete = [
|
||
|
x for x in nodes_to_delete if x >= first_new_node_index
|
||
|
]
|
||
|
nodes_to_delete.extend(nodes_on_surface)
|
||
|
self._delete_nodes(nodes_to_delete)
|
||
|
# get the formula for converting two faces into a cohesive element
|
||
|
formula = self.COHESIVE_FORMULA[face_type]
|
||
|
nodes_per_element = self.NODES_PER_ELEMENT[formula[0]]
|
||
|
# create cohesive zone element block connectivity by
|
||
|
connectivity = []
|
||
|
for face in free_faces:
|
||
|
other_face, transform_count = internal_faces[face]
|
||
|
if other_face < face:
|
||
|
continue
|
||
|
bottom = self._get_face_indices(face)
|
||
|
top = self._get_face_indices(other_face)
|
||
|
bottom = self._invert_face(bottom, face_type)
|
||
|
for _ in range(transform_count):
|
||
|
bottom = self._rotate_face(bottom, face_type)
|
||
|
top.extend(bottom)
|
||
|
# apply formula to convert two faces into an element
|
||
|
this_connectivity = [top[x] for x in formula[1]]
|
||
|
# add this element to the connectivity list
|
||
|
connectivity.extend(this_connectivity)
|
||
|
# create the actual element block
|
||
|
self.create_element_block(new_element_block_id, [
|
||
|
formula[0],
|
||
|
len(connectivity) // nodes_per_element, nodes_per_element, 0
|
||
|
], connectivity)
|
||
|
|
||
|
def create_averaged_element_field(self,
|
||
|
from_element_field_names,
|
||
|
new_element_field_name,
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Create an element field by averaging the given field values.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.create_averaged_element_field(['temp_1', 'temp_2'],
|
||
|
... 'temp_avg')
|
||
|
>>> model.create_averaged_element_field('temp_*', 'temp_avg')
|
||
|
|
||
|
"""
|
||
|
# format the arguments
|
||
|
from_element_field_names = self._format_id_list(
|
||
|
from_element_field_names, self.get_element_field_names(),
|
||
|
'element field')
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# for each timestep
|
||
|
for element_block_id in element_block_ids:
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
# see if all fields are defined on this block
|
||
|
all_defined = True
|
||
|
for name in from_element_field_names:
|
||
|
if not self.element_field_exists(name, element_block_id):
|
||
|
all_defined = False
|
||
|
break
|
||
|
if not all_defined:
|
||
|
self._warning(
|
||
|
'Fields not defined.',
|
||
|
'Not all of the requested element fields are '
|
||
|
'defined on element block %s. The averaged '
|
||
|
'field will not be created.' % element_block_id)
|
||
|
continue
|
||
|
# create the field if it doesn't exist
|
||
|
if self.element_field_exists(new_element_field_name,
|
||
|
element_block_id):
|
||
|
self._exists_on_entity_warning(new_element_field_name,
|
||
|
'element field',
|
||
|
element_block_id,
|
||
|
'element block')
|
||
|
else:
|
||
|
self.create_element_field(new_element_field_name,
|
||
|
element_block_id)
|
||
|
# get lists to average
|
||
|
field_list = []
|
||
|
for name in from_element_field_names:
|
||
|
field_list.append(fields[name])
|
||
|
new_element_field = fields[new_element_field_name]
|
||
|
# for each timestep, average the values
|
||
|
field_count = len(from_element_field_names)
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
lists_to_average = [x[timestep_index] for x in field_list]
|
||
|
averaged_field = [
|
||
|
sum(x) / field_count for x in zip(*lists_to_average)
|
||
|
]
|
||
|
new_element_field[timestep_index] = averaged_field
|
||
|
|
||
|
def convert_element_field_to_node_field(self,
|
||
|
element_field_name,
|
||
|
node_field_name='auto'):
|
||
|
"""
|
||
|
Convert an element field to a node field by performing.
|
||
|
|
||
|
For each node, the value of the field at that node will be the average
|
||
|
of the values in every element which shares that node for elements on
|
||
|
which the field is defined.
|
||
|
|
||
|
By default, the name of the node field will be the same as the element
|
||
|
field.
|
||
|
|
||
|
Example:
|
||
|
>>> model.convert_element_field_to_node_field('temperature')
|
||
|
|
||
|
"""
|
||
|
[element_field_name] = self._format_id_list(
|
||
|
[element_field_name],
|
||
|
self.get_element_field_names(),
|
||
|
'element field',
|
||
|
single=True)
|
||
|
# format the arguments
|
||
|
if node_field_name == 'auto':
|
||
|
node_field_name = element_field_name
|
||
|
# create the field
|
||
|
self.create_node_field(node_field_name)
|
||
|
# store field indices
|
||
|
node_field = self.node_fields[node_field_name]
|
||
|
# store default value
|
||
|
default_value = self._get_default_field_value(node_field_name)
|
||
|
# process each timestep
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
# initialize node field
|
||
|
node_field_values = [0.0] * len(self.nodes)
|
||
|
node_field_elements = [0] * len(self.nodes)
|
||
|
# for each element block
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
# if field doesn't exist on this element, skip it
|
||
|
if not self.element_field_exists(element_field_name,
|
||
|
element_block_id):
|
||
|
continue
|
||
|
# store connectivity
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
# for each node within each element
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
element_field_values = fields[element_field_name][
|
||
|
timestep_index]
|
||
|
nodes_per_element = self.get_nodes_per_element(
|
||
|
element_block_id)
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
for element_index in range(element_count):
|
||
|
for node_index in range(nodes_per_element):
|
||
|
this_node = connectivity[element_index *
|
||
|
nodes_per_element +
|
||
|
node_index]
|
||
|
node_field_values[this_node] += element_field_values[
|
||
|
element_index]
|
||
|
node_field_elements[this_node] += 1
|
||
|
# average each value, or replace with default value
|
||
|
for node_index in range(len(self.nodes)):
|
||
|
if node_field_elements[node_index] == 0:
|
||
|
node_field_values[node_index] = default_value
|
||
|
else:
|
||
|
node_field_values[node_index] /= node_field_elements[
|
||
|
node_index]
|
||
|
# replace field values with generated values
|
||
|
node_field[timestep_index] = node_field_values
|
||
|
|
||
|
def convert_node_field_to_element_field(self,
|
||
|
node_field_name,
|
||
|
element_field_name='auto',
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Convert a node field to an element field.
|
||
|
|
||
|
For each element, the value of the field at that element will be the
|
||
|
average of the values in each node of that element.
|
||
|
|
||
|
Example:
|
||
|
>>> model.convert_node_field_to_element_field('temperature')
|
||
|
|
||
|
"""
|
||
|
[node_field_name] = self._format_id_list([node_field_name],
|
||
|
self.get_node_field_names(),
|
||
|
'node field',
|
||
|
single=True)
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# format the arguments
|
||
|
if element_field_name == 'auto':
|
||
|
element_field_name = node_field_name
|
||
|
# store the node field
|
||
|
node_field = self.node_fields[node_field_name]
|
||
|
# for each element block
|
||
|
for element_block_id in element_block_ids:
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
# create the field
|
||
|
self.create_element_field(element_field_name, element_block_id)
|
||
|
element_field = fields[element_field_name]
|
||
|
# for each timestep
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
# initialize element field
|
||
|
element_field_values = [0.0] * element_count
|
||
|
# for each node within each element
|
||
|
for connectivity_index, node_index in enumerate(connectivity):
|
||
|
element_index = connectivity_index // nodes_per_element
|
||
|
element_field_values[element_index] += node_field[
|
||
|
timestep_index][node_index]
|
||
|
# average each value
|
||
|
element_field_values = [
|
||
|
x / float(nodes_per_element) for x in element_field_values
|
||
|
]
|
||
|
# replace field values with generated values
|
||
|
element_field[timestep_index] = element_field_values
|
||
|
|
||
|
def _find_member_indices_to_keep(self, all_members, members_to_delete):
|
||
|
"""
|
||
|
Return a list of indices to keep from the list.
|
||
|
|
||
|
This will handle duplicate entries correctly. Duplicate entries will
|
||
|
either all be deleted or all be kept. Each index will appear in the
|
||
|
resulting list at most once.
|
||
|
|
||
|
"""
|
||
|
# find entries which are not in either list
|
||
|
all_members_set = set(all_members)
|
||
|
members_to_delete_set = set(members_to_delete)
|
||
|
invalid_entries = members_to_delete_set - all_members_set
|
||
|
if invalid_entries:
|
||
|
self._warning(
|
||
|
'Invalid members',
|
||
|
'The member list contains entries that were not '
|
||
|
'members. There were %d such occurrences. These '
|
||
|
'will be ignored.' % (len(invalid_entries)))
|
||
|
# find members to delete
|
||
|
indices_to_keep = [
|
||
|
i for i, x in enumerate(all_members)
|
||
|
if x not in members_to_delete_set
|
||
|
]
|
||
|
return indices_to_keep
|
||
|
|
||
|
def _delete_side_set_members(self, side_set_id, side_set_members):
|
||
|
"""
|
||
|
Delete the specified members from the given node set.
|
||
|
|
||
|
Node set fields are updates.
|
||
|
|
||
|
Members are given as (node_index).
|
||
|
|
||
|
"""
|
||
|
# validate input
|
||
|
[side_set_id] = self._format_side_set_id_list([side_set_id], single=True)
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
# find members to keep
|
||
|
indices_to_keep = self._find_member_indices_to_keep(
|
||
|
members, side_set_members)
|
||
|
# delete members from node set fields
|
||
|
for all_values in list(fields.values()):
|
||
|
all_values[:] = [[values[x] for x in indices_to_keep]
|
||
|
for values in all_values]
|
||
|
# delete members
|
||
|
members[:] = [members[x] for x in indices_to_keep]
|
||
|
|
||
|
def _delete_node_set_members(self, node_set_id, node_set_members):
|
||
|
"""
|
||
|
Delete the specified members from the given node set.
|
||
|
|
||
|
Node set fields are updates.
|
||
|
|
||
|
Members are given as (node_index).
|
||
|
|
||
|
"""
|
||
|
# validate input
|
||
|
[node_set_id] = self._format_node_set_id_list([node_set_id], single=True)
|
||
|
node_set = self.node_sets[node_set_id]
|
||
|
# find members to keep
|
||
|
indices_to_keep = self._find_member_indices_to_keep(
|
||
|
node_set[0], node_set_members)
|
||
|
# delete members from node set fields
|
||
|
node_set[-1] = [[these_values[x] for x in indices_to_keep]
|
||
|
for these_values in node_set[1]]
|
||
|
# delete members
|
||
|
node_set[0] = [node_set[0][x] for x in indices_to_keep]
|
||
|
|
||
|
def _delete_nodes(self, node_list):
|
||
|
"""
|
||
|
Delete the given nodes.
|
||
|
|
||
|
This will also delete all references to those nodes in node sets. If a
|
||
|
node is still being used by an element, it cannot be deleted.
|
||
|
|
||
|
Example:
|
||
|
>>> model.delete_nodes([0, 1, 2, 3])
|
||
|
|
||
|
"""
|
||
|
node_list = self._remove_duplicates(node_list, preserve_order=False)
|
||
|
# find node mapping
|
||
|
# old node i refers to new node node_map[i]
|
||
|
keep_node = [True] * len(self.nodes)
|
||
|
for node_index in node_list:
|
||
|
keep_node[node_index] = False
|
||
|
node_map = [None] * len(self.nodes)
|
||
|
reverse_node_map = []
|
||
|
next_index = 0
|
||
|
for index, keep in enumerate(keep_node):
|
||
|
if keep:
|
||
|
reverse_node_map.append(index)
|
||
|
node_map[index] = next_index
|
||
|
next_index += 1
|
||
|
# delete nodes
|
||
|
new_nodes = [self.nodes[x] for x in reverse_node_map]
|
||
|
self.nodes = new_nodes
|
||
|
# update connectivity in each element block
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
new_connectivity = [node_map[x] for x in connectivity]
|
||
|
if None in new_connectivity:
|
||
|
self._error(
|
||
|
'Node still used.',
|
||
|
'A node in the list of nodes to delete is still '
|
||
|
'used by elements in element block %d and cannot '
|
||
|
'be deleted.' % element_block_id)
|
||
|
connectivity[:] = new_connectivity
|
||
|
# update node fields
|
||
|
for field in list(self.node_fields.values()):
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
new_values = [
|
||
|
field[timestep_index][x] for x in reverse_node_map
|
||
|
]
|
||
|
field[timestep_index] = new_values
|
||
|
# delete nodes from node sets and fields
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
# find new mapping
|
||
|
value_map = []
|
||
|
new_members = []
|
||
|
for index, member in enumerate(members):
|
||
|
if node_map[member] is not None:
|
||
|
value_map.append(index)
|
||
|
new_members.append(node_map[member])
|
||
|
# update member list
|
||
|
members[:] = new_members
|
||
|
# delete these nodes from the field
|
||
|
for field in list(fields.values()):
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
new_values = [field[timestep_index][x] for x in value_map]
|
||
|
field[timestep_index] = new_values
|
||
|
|
||
|
def _get_unreferenced_nodes(self):
|
||
|
"""Return a list of node indices which are not used by any element."""
|
||
|
used_node = [False] * len(self.nodes)
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
for node_index in connectivity:
|
||
|
used_node[node_index] = True
|
||
|
unused_nodes = [
|
||
|
index for index, used in enumerate(used_node) if not used
|
||
|
]
|
||
|
return unused_nodes
|
||
|
|
||
|
def delete_unused_nodes(self):
|
||
|
"""
|
||
|
Delete nodes which are not used by any elements.
|
||
|
|
||
|
Example:
|
||
|
>>> model.delete_unused_nodes()
|
||
|
|
||
|
"""
|
||
|
nodes_to_delete = self._get_unreferenced_nodes()
|
||
|
self._delete_nodes(nodes_to_delete)
|
||
|
|
||
|
def delete_global_variable(self, global_variable_names):
|
||
|
"""
|
||
|
Delete one or more global variables.
|
||
|
|
||
|
This function may also be called by its plural form
|
||
|
'delete_global_variables()'.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_global_variable('internal_energy')
|
||
|
>>> model.delete_global_variable('all')
|
||
|
|
||
|
"""
|
||
|
global_variable_names = self._format_id_list(
|
||
|
global_variable_names, self.get_global_variable_names(),
|
||
|
'global variable')
|
||
|
for name in global_variable_names:
|
||
|
del self.global_variables[name]
|
||
|
|
||
|
def delete_side_set(self, side_set_ids):
|
||
|
"""
|
||
|
Delete one or more side sets.
|
||
|
|
||
|
This function may also be called by its plural form
|
||
|
'delete_side_sets()'.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.delete_side_set(1)
|
||
|
>>> model.delete_side_set('all')
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
for side_set_id in side_set_ids:
|
||
|
del self.side_sets[side_set_id]
|
||
|
|
||
|
def delete_empty_side_sets(self):
|
||
|
"""
|
||
|
Delete all side sets with zero members.
|
||
|
|
||
|
Example:
|
||
|
>>> model.delete_empty_side_sets()
|
||
|
|
||
|
"""
|
||
|
for id_ in self.get_side_set_ids():
|
||
|
if not self.get_side_set_members(id_):
|
||
|
self.delete_side_set(id_)
|
||
|
|
||
|
def delete_node_set(self, node_set_ids):
|
||
|
"""
|
||
|
Delete the given node set.
|
||
|
|
||
|
This function may also be called by its plural form
|
||
|
'delete_node_sets()'.
|
||
|
|
||
|
Example:
|
||
|
>>> model.delete_node_set(1)
|
||
|
|
||
|
"""
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
for node_set_id in node_set_ids:
|
||
|
del self.node_sets[node_set_id]
|
||
|
|
||
|
def delete_empty_node_sets(self):
|
||
|
"""
|
||
|
Delete all side sets with zero members.
|
||
|
|
||
|
Example:
|
||
|
>>> model.delete_empty_node_sets()
|
||
|
|
||
|
"""
|
||
|
for id_ in self.get_node_set_ids():
|
||
|
if not self.get_node_set_members(id_):
|
||
|
self.delete_node_set(id_)
|
||
|
|
||
|
def summarize(self):
|
||
|
"""
|
||
|
Print a summary of the information in the current model.
|
||
|
|
||
|
Example:
|
||
|
>>> model.summarize()
|
||
|
|
||
|
"""
|
||
|
print('The model contains the following:')
|
||
|
print((' %d elements' % (self.get_element_count())))
|
||
|
print((' %d timesteps' % (len(self.timesteps))))
|
||
|
print((' %d global variables' % (len(self.global_variables))))
|
||
|
print((' %d nodes' % (len(self.nodes))))
|
||
|
print((' %d node fields' % (len(self.node_fields))))
|
||
|
print((' %d element blocks' % (len(self.element_blocks))))
|
||
|
print((' %d element fields' % (len(self.get_element_field_names()))))
|
||
|
print((' %d node sets' % (len(self.node_sets))))
|
||
|
print(
|
||
|
(' %d node set fields' % (len(self.get_node_set_field_names()))))
|
||
|
print((' %d side sets' % (len(self.side_sets))))
|
||
|
print(
|
||
|
(' %d side set fields' % (len(self.get_side_set_field_names()))))
|
||
|
|
||
|
def _get_element_block_fields(self, element_block_id):
|
||
|
"""Return the dictionary of element block field values."""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
return self.element_blocks[element_block_id][-1]
|
||
|
|
||
|
def _get_node_set_fields(self, node_set_id):
|
||
|
"""Return the dictionary of node set field values."""
|
||
|
[node_set_id] = self._format_node_set_id_list([node_set_id], single=True)
|
||
|
return self.node_sets[node_set_id][-1]
|
||
|
|
||
|
def get_element_count(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return the total number of elements in the given element blocks.
|
||
|
|
||
|
Example:
|
||
|
>>> print((' %d elements' % (object.get_element_count())))
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
return int(sum(self.element_blocks[x][1][1] for x in element_block_ids))
|
||
|
|
||
|
def process_element_fields(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Process element field information to create node based fields.
|
||
|
|
||
|
For element fields with 8 integration points, this takes the average.
|
||
|
This is useful for fully integrated or selective deviatoric elements
|
||
|
within the SIERRA/SM code.
|
||
|
|
||
|
For element fields with 9 integration points, this takes the first one.
|
||
|
This is useful for q1p0 elements within the SIERRA/SM code.
|
||
|
|
||
|
This function is provided as a convenience for post processing hex8
|
||
|
elements with multiple integration points.
|
||
|
|
||
|
Example:
|
||
|
>>> model.process_element_fields()
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# for each element block
|
||
|
for block_id in element_block_ids:
|
||
|
element_field_names = self.get_element_field_names(block_id)
|
||
|
for element_field_name in element_field_names:
|
||
|
# if it's the first integration point, count the total number
|
||
|
# and process accordingly
|
||
|
if re.match('^.*_1$', element_field_name):
|
||
|
prefix = element_field_name[:-1]
|
||
|
points = 1
|
||
|
while self.element_field_exists(prefix + str(points + 1),
|
||
|
block_id):
|
||
|
points += 1
|
||
|
if points == 9:
|
||
|
self.rename_element_field(prefix + '1', prefix[:-1],
|
||
|
block_id)
|
||
|
for i in range(2, 10):
|
||
|
self.delete_element_field(prefix + str(i),
|
||
|
block_id)
|
||
|
elif points == 8:
|
||
|
self.create_averaged_element_field(
|
||
|
[prefix + str(x) for x in range(1, 9)],
|
||
|
prefix[:-1], block_id)
|
||
|
for i in range(1, 9):
|
||
|
self.delete_element_field(prefix + str(i),
|
||
|
block_id)
|
||
|
# nodal average all fields
|
||
|
for element_field_name in self.get_element_field_names():
|
||
|
self.convert_element_field_to_node_field(element_field_name)
|
||
|
# delete element fields
|
||
|
self.delete_element_field('all')
|
||
|
|
||
|
def combine_element_blocks(self,
|
||
|
element_block_ids,
|
||
|
target_element_block_id='auto'):
|
||
|
"""
|
||
|
Combine multiple element blocks into a single block.
|
||
|
|
||
|
By default, the target element block id will be the smallest of the
|
||
|
merged element block ids.
|
||
|
|
||
|
The element blocks to combine must have the same element type.
|
||
|
|
||
|
Example:
|
||
|
>>> model.combine_element_blocks('all', 1)
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
if target_element_block_id == 'auto':
|
||
|
target_element_block_id = min(element_block_ids)
|
||
|
if len(element_block_ids) == 1:
|
||
|
self.rename_element_block(element_block_ids[0],
|
||
|
target_element_block_id)
|
||
|
return
|
||
|
# ensure all blocks have the same number of nodes per element
|
||
|
nodes_per_element = set(
|
||
|
self.get_nodes_per_element(x) for x in element_block_ids)
|
||
|
if len(nodes_per_element) != 1:
|
||
|
self._error(
|
||
|
'Incompatible element types.',
|
||
|
'The number of nodes per element on each element '
|
||
|
'block to be merged must by the same. This is an '
|
||
|
'ExodusII file requirement.')
|
||
|
# ensure all blocks are the same type
|
||
|
element_types = set(
|
||
|
self._get_standard_element_type(self._get_element_type(x))
|
||
|
for x in element_block_ids)
|
||
|
if len(element_types) != 1:
|
||
|
self._warning(
|
||
|
'Element types vary.',
|
||
|
'The element types of the merged blocks differ. '
|
||
|
'This may cause issues.')
|
||
|
# create new connectivity
|
||
|
new_connectivity = list(
|
||
|
itertools.chain(
|
||
|
*[self.get_connectivity(x) for x in element_block_ids]))
|
||
|
# create new info
|
||
|
new_info = list(self._get_block_info(element_block_ids[0]))
|
||
|
# set element count to zero
|
||
|
new_info[1] = 0
|
||
|
# find a temporary element block id
|
||
|
temporary_element_block_id = self._new_element_block_id()
|
||
|
# create translation list for elements
|
||
|
# new_block[old_block_id] = (new_block_id, element_offset)
|
||
|
new_block_info = dict(
|
||
|
(id_, (id_, 0)) for id_ in self.get_element_block_ids())
|
||
|
for id_ in element_block_ids:
|
||
|
new_block_info[id_] = (temporary_element_block_id, new_info[1])
|
||
|
new_info[1] += self.element_blocks[id_][1][1]
|
||
|
# for fields which only exist on certain block, define them on all
|
||
|
element_field_names = self.get_element_field_names(element_block_ids)
|
||
|
give_nonexistant_field_warning = True
|
||
|
for name in element_field_names:
|
||
|
for id_ in element_block_ids:
|
||
|
if not self.element_field_exists(name, id_):
|
||
|
if give_nonexistant_field_warning:
|
||
|
self._warning(
|
||
|
'Inconsistent element fields.',
|
||
|
'The element field "%s" is not defined '
|
||
|
'on all element blocks included in the '
|
||
|
'merging operation. It will be '
|
||
|
'created. Future warnings of this type '
|
||
|
'will be suppressed.' % name)
|
||
|
give_nonexistant_field_warning = False
|
||
|
self.create_element_field(name, id_)
|
||
|
# create new field information
|
||
|
new_fields = dict()
|
||
|
for name in element_field_names:
|
||
|
new_values = [[] for _ in self.timesteps]
|
||
|
for id_ in element_block_ids:
|
||
|
for index, values in enumerate(
|
||
|
self.element_blocks[id_][-1][name]):
|
||
|
new_values[index].extend(values)
|
||
|
new_fields[name] = new_values
|
||
|
# create new block
|
||
|
self.create_element_block(temporary_element_block_id, new_info,
|
||
|
new_connectivity)
|
||
|
self.element_blocks[temporary_element_block_id][-1] = new_fields
|
||
|
# translate side set members
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
new_members = [
|
||
|
(new_block_info[element_block_id][0],
|
||
|
element_index + new_block_info[element_block_id][1],
|
||
|
face_index)
|
||
|
for element_block_id, element_index, face_index in members
|
||
|
]
|
||
|
members[:] = new_members
|
||
|
# delete old blocks
|
||
|
# (nodes can not be orphaned by this procedure)
|
||
|
for element_block_id in element_block_ids:
|
||
|
self.delete_element_block(element_block_id,
|
||
|
delete_orphaned_nodes=False)
|
||
|
# rename element block
|
||
|
self.rename_element_block(temporary_element_block_id,
|
||
|
target_element_block_id)
|
||
|
|
||
|
def duplicate_element_block(self,
|
||
|
element_block_id,
|
||
|
new_element_block_id,
|
||
|
duplicate_nodes=True):
|
||
|
"""
|
||
|
Create an duplicate of the given element block.
|
||
|
|
||
|
Nodes are duplicated by default. The new element block references
|
||
|
these duplicated nodes, not the original ones.
|
||
|
|
||
|
Example:
|
||
|
>>> model.duplicate_element_block(1, 2)
|
||
|
|
||
|
"""
|
||
|
[element_block_id] = self._format_element_block_id_list(
|
||
|
[element_block_id], single=True)
|
||
|
info = list(self._get_block_info(element_block_id))
|
||
|
old_connectivity = self.get_connectivity(element_block_id)
|
||
|
# create new nodes
|
||
|
if duplicate_nodes:
|
||
|
unique_node_indices = sorted(set(old_connectivity))
|
||
|
new_node_indices = []
|
||
|
self._duplicate_nodes(unique_node_indices, new_node_indices)
|
||
|
new_node_indices = dict(
|
||
|
(x, y) for x, y in zip(unique_node_indices, new_node_indices))
|
||
|
new_connectivity = [new_node_indices[x] for x in old_connectivity]
|
||
|
else:
|
||
|
new_connectivity = list(old_connectivity)
|
||
|
# create the block
|
||
|
self.create_element_block(new_element_block_id, info, new_connectivity)
|
||
|
# copy fields
|
||
|
fields = dict()
|
||
|
for name, all_values in list(
|
||
|
self._get_element_block_fields(element_block_id).items()):
|
||
|
fields[name] = [list(x) for x in all_values]
|
||
|
self.element_blocks[new_element_block_id][-1] = fields
|
||
|
# update side sets and side set fields
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
new_members = []
|
||
|
source_face = []
|
||
|
for index, member in enumerate(members):
|
||
|
if member[0] == element_block_id:
|
||
|
new_members.append(
|
||
|
(new_element_block_id, member[1], member[2]))
|
||
|
source_face.append(index)
|
||
|
members.extend(new_members)
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
new_values = [values[x] for x in source_face]
|
||
|
values.extend(new_values)
|
||
|
|
||
|
def _get_inverted_connectivity(self, element_type):
|
||
|
"""Return the connectivity pemutation to invert an element."""
|
||
|
element_type = self._get_standard_element_type(element_type)
|
||
|
if element_type not in self.INVERTED_CONNECTIVITY:
|
||
|
self._error(
|
||
|
'Unknown inversion algorithm.',
|
||
|
'The required numbering to invert an element of type '
|
||
|
'"%s" is not defined.' % element_type)
|
||
|
return self.INVERTED_CONNECTIVITY[element_type]
|
||
|
|
||
|
def _get_inverted_face_mapping(self, element_type):
|
||
|
"""Return the connectivity pemutation to invert an element face."""
|
||
|
face_mapping = self._get_face_mapping(element_type)
|
||
|
inversion_mapping = self._get_inverted_connectivity(element_type)
|
||
|
original_face = [set(face) for _, face in face_mapping]
|
||
|
new_face = [
|
||
|
set(inversion_mapping[x] for x in face) for _, face in face_mapping
|
||
|
]
|
||
|
try:
|
||
|
new_face = [original_face.index(x) for x in new_face]
|
||
|
except ValueError:
|
||
|
self._bug(
|
||
|
'Unable to determine face mapping.',
|
||
|
'We were unable to determine how to map faces when '
|
||
|
'inverting an element of type "%s".' % element_type)
|
||
|
return new_face
|
||
|
|
||
|
def _invert_element_blocks(self, element_block_ids):
|
||
|
"""Invert all elements within one or more element blocks."""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
for id_ in element_block_ids:
|
||
|
element_count = self.get_element_count(id_)
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
# invert the connectivity
|
||
|
inverted_mapping = self._get_inverted_connectivity(
|
||
|
self._get_element_type(id_))
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
new_connectivity = [
|
||
|
connectivity[element_index * nodes_per_element + x]
|
||
|
for element_index in range(element_count)
|
||
|
for x in inverted_mapping
|
||
|
]
|
||
|
connectivity[:] = new_connectivity
|
||
|
# adjust side set members
|
||
|
new_face_indices = self._get_inverted_face_mapping(
|
||
|
self._get_element_type(id_))
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
for index, member in enumerate(members):
|
||
|
if member[0] == id_:
|
||
|
members[index] = (id_, member[1],
|
||
|
new_face_indices[member[2]])
|
||
|
|
||
|
def create_element_block(self, element_block_id, info, connectivity=None):
|
||
|
"""
|
||
|
Create a new element block.
|
||
|
|
||
|
The nodes for the elements in the block must have already been defined.
|
||
|
|
||
|
The info list should be comprised of the following information.
|
||
|
* '[element_type, element_count, nodes_per_element, 0]'
|
||
|
|
||
|
For example, the following would be valid.
|
||
|
* '["hex8", elements, 8, 0]'
|
||
|
|
||
|
The connectivity list should be a shallow list of element connectivity
|
||
|
and must be of length 'element_count * nodes_per_element'.
|
||
|
|
||
|
Element blocks are unnamed when created. To name them, use the
|
||
|
'rename_element_block' function.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_element_block(1, ['hex8', 0, 8, 0])
|
||
|
|
||
|
"""
|
||
|
# make sure it doesn't exist already
|
||
|
if self.element_block_exists(element_block_id):
|
||
|
self._exists_error(element_block_id, 'element block')
|
||
|
# set up an empty connectivity if none is given
|
||
|
if not connectivity:
|
||
|
connectivity = []
|
||
|
# create the actual block
|
||
|
self.element_blocks[element_block_id] = ['', info, connectivity, {}]
|
||
|
|
||
|
def create_nodes(self, new_nodes):
|
||
|
"""
|
||
|
Create nodes corresponding to the given coordinate list.
|
||
|
|
||
|
The list must contain coordinate triples '[x, y, z]' defined for each
|
||
|
new node. The new nodes are assigned a local index starting with the
|
||
|
current length of the nodes list.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_nodes([[0.0, 0.0, 0.0], [1.0, 2.0, 3.0]])
|
||
|
|
||
|
"""
|
||
|
# add nodes
|
||
|
self.nodes += new_nodes
|
||
|
# adjust node_fields
|
||
|
for name, all_values in list(self.node_fields.items()):
|
||
|
default_value = self._get_default_field_value(name)
|
||
|
for values in all_values:
|
||
|
values.extend([default_value] * len(new_nodes))
|
||
|
|
||
|
def _exists_error(self, name, entity):
|
||
|
"""Warn the user something already exists and exit."""
|
||
|
self._error(
|
||
|
entity[0].upper() + entity[1:] + ' already exists.',
|
||
|
'The specified %s "%s" already exists. This operation cannot '
|
||
|
'be completed.' % (entity, str(name)))
|
||
|
|
||
|
def _exists_warning(self, name, entity):
|
||
|
"""Warn the user something already exists."""
|
||
|
self._warning(
|
||
|
entity[0].upper() + entity[1:] + ' already exists.',
|
||
|
'The specified %s "%s" already exists. Information may be '
|
||
|
'overwritten.' % (entity, str(name)))
|
||
|
|
||
|
def _exists_on_entity_warning(self, name, entity, base_name, base_entity):
|
||
|
"""Warn the user something already exists on a given entity."""
|
||
|
self._warning(
|
||
|
entity[0].upper() + entity[1:] + ' already exists.',
|
||
|
'The specified %s "%s" already exists on %s %s. Information may '
|
||
|
'be overwritten.' %
|
||
|
(entity, str(name), base_entity, str(base_name)))
|
||
|
|
||
|
def _missing_error(self, name, entity):
|
||
|
"""Tell the user something does not exist and exit."""
|
||
|
self._error(
|
||
|
entity[0].upper() + entity[1:] + ' does not exist.',
|
||
|
'The specified %s "%s" does not exist.' % (entity, str(name)))
|
||
|
|
||
|
def _missing_warning(self, name, entity):
|
||
|
"""Warn the user something does not exist."""
|
||
|
self._error(
|
||
|
entity[0].upper() + entity[1:] + ' does not exist.',
|
||
|
'The specified %s "%s" does not exist and will be ignored.' %
|
||
|
(entity, str(name)))
|
||
|
|
||
|
def _missing_on_entity_error(self, name, entity, base_name, base_entity):
|
||
|
"""Tell the user something does not exist on an entity and exit."""
|
||
|
self._error(
|
||
|
entity[0].upper() + entity[1:] + ' does not exist.',
|
||
|
'The specified %s "%s" does not exist on %s %s.' %
|
||
|
(entity, str(name), base_entity, str(base_name)))
|
||
|
|
||
|
def create_global_variable(self, global_variable_name, value='auto'):
|
||
|
"""
|
||
|
Create a new global variable.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_global_variable('gravitational_acceleration', 9.8)
|
||
|
|
||
|
"""
|
||
|
if self.global_variable_exists(global_variable_name):
|
||
|
self._exists_warning(global_variable_name, 'global variable')
|
||
|
if value == 'auto':
|
||
|
value = self._get_default_field_value(global_variable_name)
|
||
|
values = [value] * len(self.timesteps)
|
||
|
self.global_variables[global_variable_name] = values
|
||
|
|
||
|
def calculate_element_centroids(self,
|
||
|
element_field_name_prefix='centroid_',
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Calculate and store the centroid of each element.
|
||
|
|
||
|
This will approximate the element centroid as the nodal average of each
|
||
|
element and will store that value in an element field. Since a
|
||
|
timestep must be defined in order for element fields to exist, one will
|
||
|
be created if none exist.
|
||
|
|
||
|
By default, the centroid will be stored in the fields 'centroid_x',
|
||
|
'centroid_y', and 'centroid_z'. Alternatively, a prefix can be given
|
||
|
or a list of three strings can be given.
|
||
|
|
||
|
Example:
|
||
|
>>> model.calculate_element_centroids()
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if not self.timesteps:
|
||
|
self.create_timestep(0.0)
|
||
|
if isinstance(element_field_name_prefix, str):
|
||
|
centroid_field_names = [
|
||
|
element_field_name_prefix + x for x in ['x', 'y', 'z']
|
||
|
]
|
||
|
else:
|
||
|
centroid_field_names = element_field_name_prefix
|
||
|
for element_block_id in element_block_ids:
|
||
|
for name in centroid_field_names:
|
||
|
if self.element_field_exists(name, element_block_id):
|
||
|
self._exists_on_entity_warning(name, 'element field',
|
||
|
element_block_id,
|
||
|
'element block')
|
||
|
# calculate centroids
|
||
|
centroid = [[], [], []]
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
for element_index in range(element_count):
|
||
|
this_centroid = [0.0, 0.0, 0.0]
|
||
|
for connectivity_index in range(nodes_per_element):
|
||
|
node_index = connectivity[element_index * nodes_per_element
|
||
|
+ connectivity_index]
|
||
|
for i in range(3):
|
||
|
this_centroid[i] += self.nodes[node_index][i]
|
||
|
for i in range(3):
|
||
|
centroid[i].append(this_centroid[i] / nodes_per_element)
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
for index, name in enumerate(centroid_field_names):
|
||
|
values = []
|
||
|
for _ in range(len(self.timesteps)):
|
||
|
values.append(list(centroid[index]))
|
||
|
fields[name] = values
|
||
|
|
||
|
def calculate_element_volumes(self,
|
||
|
element_field_name='volume',
|
||
|
element_block_ids='all'):
|
||
|
"""
|
||
|
Calculate and store the volume of each element.
|
||
|
|
||
|
This will approximate the element volume. Since a timestep must be
|
||
|
defined in order for element fields to exist, one will be created if
|
||
|
none exist.
|
||
|
|
||
|
For two dimensional elements, this calculates the area. For one
|
||
|
dimensional elements, this calculates the length.
|
||
|
|
||
|
Example:
|
||
|
>>> model.calculate_element_volumes()
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if not self.timesteps:
|
||
|
self.create_timestep(0.0)
|
||
|
for element_block_id in element_block_ids:
|
||
|
# get the element type
|
||
|
element_type = self._get_standard_element_type(
|
||
|
self._get_element_type(element_block_id))
|
||
|
if element_type not in self.VOLUME_FORMULA:
|
||
|
self._warning(
|
||
|
'Unrecognized element type',
|
||
|
'The formula for calculating the volume of '
|
||
|
'element type "%s" is not implemented or not '
|
||
|
'known. This block will be skipped.' % (element_type))
|
||
|
continue
|
||
|
# warn if field already exists
|
||
|
if self.element_field_exists(element_field_name, element_block_id):
|
||
|
self._exists_on_entity_warning(element_field_name,
|
||
|
'element field',
|
||
|
element_block_id,
|
||
|
'element block')
|
||
|
# get the formula
|
||
|
formula = self.VOLUME_FORMULA[element_type]
|
||
|
if len(formula) == 2:
|
||
|
# distance between two points
|
||
|
equation = ('(x[0] ** 2.0 + '
|
||
|
'x[1] ** 2.0 + '
|
||
|
'x[2] ** 2.0) ** 0.5')
|
||
|
elif len(formula) == 3:
|
||
|
# magnitude of cross product
|
||
|
equation = ('((x[0] * x[4] - x[1] * x[3]) ** 2.0 + '
|
||
|
'(x[2] * x[3] - x[0] * x[5]) ** 2.0 + '
|
||
|
'(x[1] * x[5] - x[2] * x[4]) ** 2.0) ** 0.5')
|
||
|
elif len(formula) == 4:
|
||
|
# triple product
|
||
|
equation = ('(x[1] * x[5] - x[2] * x[4]) * x[6] + '
|
||
|
'(x[2] * x[3] - x[0] * x[5]) * x[7] + '
|
||
|
'(x[0] * x[4] - x[1] * x[3]) * x[8]')
|
||
|
else:
|
||
|
self._bug('Unknown case')
|
||
|
# process the formula to account for averaged nodes
|
||
|
transforms = []
|
||
|
for i, rule in enumerate(formula[1:]):
|
||
|
for d in range(3):
|
||
|
value = '('
|
||
|
node_list = rule[1]
|
||
|
if isinstance(node_list, int):
|
||
|
value += 'n[%d]' % (node_list * 3 + d)
|
||
|
else:
|
||
|
value += '('
|
||
|
value += 'n[%d]' % (node_list[0] * 3 + d)
|
||
|
for x in node_list[1:]:
|
||
|
value += ' + n[%d]' % (x * 3 + d)
|
||
|
value += ') / %d.0' % len(node_list)
|
||
|
value += ' - '
|
||
|
node_list = rule[0]
|
||
|
if isinstance(node_list, int):
|
||
|
value += 'n[%d]' % (node_list * 3 + d)
|
||
|
else:
|
||
|
value += '('
|
||
|
value += 'n[%d]' % (node_list[0] * 3 + d)
|
||
|
for x in node_list[1:]:
|
||
|
value += ' + n[%d]' % (x * 3 + d)
|
||
|
value += ') / %d.0' % len(node_list)
|
||
|
value += ')'
|
||
|
transforms.append(('x[%d]' % (i * 3 + d), value))
|
||
|
for transform in transforms:
|
||
|
equation = equation.replace(transform[0], transform[1])
|
||
|
# store coordinates of nodes into x, y, z lists
|
||
|
function = eval('lambda n: ' + equation)
|
||
|
# store some element block values
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
volumes = []
|
||
|
for element_index in range(int(element_count)):
|
||
|
# get local node values
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
# get local coordinates
|
||
|
n = [x for i in local_node for x in self.nodes[i]]
|
||
|
# add the volume
|
||
|
volumes.append(float(function(n)) * formula[0])
|
||
|
# now make the field for each timestep
|
||
|
values = [list(volumes) for _ in range(len(self.timesteps))]
|
||
|
# assign the field
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
fields[element_field_name] = values
|
||
|
|
||
|
def get_element_block_volume(self,
|
||
|
element_block_ids,
|
||
|
element_volume_field_name=None):
|
||
|
"""Return the total volume of the given element blocks.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_element_block_volume('all')
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# create a timestep if none exist
|
||
|
created_timestep = False
|
||
|
if not self.timesteps:
|
||
|
created_timestep = True
|
||
|
self.create_timestep(0.0)
|
||
|
# calculate temporary field with element volumes
|
||
|
created_volume_field = False
|
||
|
if element_volume_field_name is None:
|
||
|
created_volume_field = True
|
||
|
element_volume_field_name = self._new_element_field_name()
|
||
|
self.calculate_element_volumes(element_volume_field_name,
|
||
|
element_block_ids)
|
||
|
# add up the volumes
|
||
|
volume = 0.0
|
||
|
for id_ in element_block_ids:
|
||
|
fields = self._get_element_block_fields(id_)
|
||
|
volume += sum(fields[element_volume_field_name][0])
|
||
|
# delete the temporary timestep
|
||
|
if created_timestep:
|
||
|
self.delete_timestep(0.0)
|
||
|
# delete the temporary field
|
||
|
if created_volume_field:
|
||
|
self.delete_element_field(element_volume_field_name,
|
||
|
element_block_ids)
|
||
|
return volume
|
||
|
|
||
|
def get_element_block_centroid(self,
|
||
|
element_block_ids,
|
||
|
element_volume_field_name=None,
|
||
|
element_centroid_field_names=None):
|
||
|
"""Return the centroid of the given element blocks.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_element_block_centroid('all')
|
||
|
[1.0, 2.0, 3.0]
|
||
|
|
||
|
"""
|
||
|
# format inputs
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# create a timestep if none exist
|
||
|
created_timestep = False
|
||
|
if not self.timesteps:
|
||
|
created_timestep = True
|
||
|
self.create_timestep(0.0)
|
||
|
# calculate temporary field with element volumes
|
||
|
created_volume_field = False
|
||
|
if element_volume_field_name is None:
|
||
|
created_volume_field = True
|
||
|
element_volume_field_name = self._new_element_field_name()
|
||
|
self.calculate_element_volumes(element_volume_field_name,
|
||
|
element_block_ids)
|
||
|
# calculate temporary field with element centroids
|
||
|
created_centroid_fields = False
|
||
|
if element_centroid_field_names is None:
|
||
|
created_centroid_fields = True
|
||
|
element_centroid_field_names = self._new_element_field_name(3)
|
||
|
self.calculate_element_centroids(element_centroid_field_names,
|
||
|
element_block_ids)
|
||
|
# calculate the centroid
|
||
|
total_volume = 0.0
|
||
|
centroid = [0.0, 0.0, 0.0]
|
||
|
for id_ in element_block_ids:
|
||
|
fields = self._get_element_block_fields(id_)
|
||
|
volumes = fields[element_volume_field_name][0]
|
||
|
centroids = [
|
||
|
fields[element_centroid_field_names[x]][0] for x in range(3)
|
||
|
]
|
||
|
for x in range(3):
|
||
|
centroid[x] += sum(a * b
|
||
|
for a, b in zip(volumes, centroids[x]))
|
||
|
total_volume += sum(volumes)
|
||
|
if total_volume == 0.0:
|
||
|
centroid = [float('nan')] * 3
|
||
|
else:
|
||
|
centroid = [x / total_volume for x in centroid]
|
||
|
# delete the temporary timestep
|
||
|
if created_timestep:
|
||
|
self.delete_timestep(0.0)
|
||
|
# delete the temporary fields
|
||
|
if created_volume_field:
|
||
|
self.delete_element_field(element_volume_field_name,
|
||
|
element_block_ids)
|
||
|
if created_centroid_fields:
|
||
|
for name in element_centroid_field_names:
|
||
|
self.delete_element_field(name, element_block_ids)
|
||
|
return centroid
|
||
|
|
||
|
def create_interpolated_timestep(self, timestep, interpolation='cubic'):
|
||
|
"""
|
||
|
Create a new timestep by interpolating neighboring steps.
|
||
|
|
||
|
This does not extrapolate, so the given timestep to be interpolated
|
||
|
must lie within the range of timesteps already defined.
|
||
|
|
||
|
By default, 'cubic' interpolation is used. This can be changed to
|
||
|
'linear' interpolation if desired.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.create_interpolated_timestep(0.5)
|
||
|
>>> model.create_interpolated_timestep(0.5, interpolation='linear')
|
||
|
|
||
|
"""
|
||
|
if timestep in self.timesteps:
|
||
|
self._exists_warning(timestep, 'timestep')
|
||
|
return
|
||
|
# find neighboring timesteps and how to form the new step
|
||
|
steps = self.get_timesteps()
|
||
|
# make sure enough steps exist
|
||
|
if len(steps) < 2:
|
||
|
self._error(
|
||
|
'Invalid interpolation.',
|
||
|
'A timestep cannot be interpolated unless 2 or '
|
||
|
'more timesteps already exist. There are %d defined '
|
||
|
'timesteps: %s' % (len(self.timesteps), ', '.join(
|
||
|
[str(x) for x in self.get_timesteps()])))
|
||
|
# make sure value lies within time bounds
|
||
|
if steps[0] > timestep or steps[-1] < timestep:
|
||
|
self._error(
|
||
|
'Invalid interpolation.',
|
||
|
'The specified timestep %s does not lie within the '
|
||
|
'range of timesteps already defined: [%s, %s].' %
|
||
|
(str(timestep), str(steps[0]), str(steps[-1])))
|
||
|
if interpolation == 'linear':
|
||
|
# find two bounding timesteps
|
||
|
nearby = min(steps, key=lambda x: abs(x - timestep))
|
||
|
if nearby < timestep:
|
||
|
nearby = [nearby, steps[steps.index(nearby) + 1]]
|
||
|
else:
|
||
|
nearby = [steps[steps.index(nearby) - 1], nearby]
|
||
|
# find proportion of each one to use
|
||
|
phi = (timestep - nearby[0]) / (nearby[1] - nearby[0])
|
||
|
formula = [(nearby[0], 1.0 - phi), (nearby[1], phi)]
|
||
|
elif interpolation == 'cubic':
|
||
|
# find four bounding timesteps
|
||
|
# if step is within first or last segment, create
|
||
|
# an imaginary point to do the interpolation
|
||
|
nearby = min(steps, key=lambda x: abs(x - timestep))
|
||
|
index = steps.index(nearby)
|
||
|
four_steps = [0.0] * 4
|
||
|
if nearby > timestep:
|
||
|
index -= 1
|
||
|
four_steps[1] = steps[index]
|
||
|
four_steps[2] = steps[index + 1]
|
||
|
if index == 0:
|
||
|
four_steps[0] = 2 * four_steps[1] - four_steps[2]
|
||
|
else:
|
||
|
four_steps[0] = steps[index - 1]
|
||
|
if index + 2 == len(steps):
|
||
|
four_steps[3] = 2 * four_steps[2] - four_steps[1]
|
||
|
else:
|
||
|
four_steps[3] = steps[index + 2]
|
||
|
# find interpolation coefficients
|
||
|
coefficients = self._cubic_interpolation(timestep, *four_steps)
|
||
|
formula = [[steps[index], coefficients[1]],
|
||
|
[steps[index + 1], coefficients[2]]]
|
||
|
if index == 0:
|
||
|
formula[0][1] += 2 * coefficients[0]
|
||
|
formula[1][1] -= coefficients[0]
|
||
|
else:
|
||
|
formula.append([steps[index - 1], coefficients[0]])
|
||
|
if index + 2 == len(steps):
|
||
|
formula[0][1] -= coefficients[3]
|
||
|
formula[1][1] += 2 * coefficients[3]
|
||
|
else:
|
||
|
formula.append([steps[index + 2], coefficients[3]])
|
||
|
else:
|
||
|
self._error(
|
||
|
'Unknown interpolation technique',
|
||
|
'The specified interpolation technique "%s" is not '
|
||
|
'recognized.' % interpolation)
|
||
|
# create the new timestep
|
||
|
self.create_timestep(timestep)
|
||
|
# use the given formula to create the new step
|
||
|
# formula = list of (timestep_index, contribution)
|
||
|
formula = [(self._get_internal_timestep_index(x[0]), x[1])
|
||
|
for x in formula]
|
||
|
this_index = self._get_internal_timestep_index(timestep)
|
||
|
# element fields, side set fields, node set fields
|
||
|
for thing in itertools.chain(list(self.element_blocks.values()),
|
||
|
list(self.node_sets.values()),
|
||
|
list(self.side_sets.values())):
|
||
|
fields = thing[-1]
|
||
|
for values in list(fields.values()):
|
||
|
new_values = [0.0] * len(values[0])
|
||
|
for index, phi in formula:
|
||
|
for i in range(len(values[0])):
|
||
|
new_values[i] += values[index][i] * phi
|
||
|
values[this_index] = new_values
|
||
|
# node fields
|
||
|
for values in list(self.node_fields.values()):
|
||
|
new_values = [0.0] * len(values[0])
|
||
|
for index, phi in formula:
|
||
|
for i in range(len(values[0])):
|
||
|
new_values[i] += values[index][i] * phi
|
||
|
values[this_index] = new_values
|
||
|
# global variables
|
||
|
for values in list(self.global_variables.values()):
|
||
|
new_value = sum([values[x[0]] * x[1] for x in formula])
|
||
|
values[this_index] = new_value
|
||
|
|
||
|
def _find_new_timestep_index(self, new_timestep):
|
||
|
"""
|
||
|
Return the index at which to create the new timestep.
|
||
|
|
||
|
This returns the index to keep the list in monotonically increasing
|
||
|
order.
|
||
|
|
||
|
"""
|
||
|
# find the index at which to to insert this
|
||
|
# if the new step is above all the others, put it at the end
|
||
|
higher_times = [x for x in self.timesteps if x > new_timestep]
|
||
|
# if the new time is higher than all existing times, then add it to the
|
||
|
# end
|
||
|
if not higher_times:
|
||
|
return len(self.timesteps)
|
||
|
return self.timesteps.index(min(higher_times))
|
||
|
|
||
|
def copy_timestep(self, timestep, new_timestep):
|
||
|
"""
|
||
|
Create a copy of an existing timestep.
|
||
|
|
||
|
This copies all information from the old timestep including values
|
||
|
of global variables, node fields, element fields, node set fields and
|
||
|
side set fields.
|
||
|
|
||
|
Example:
|
||
|
object.copy_timestep(0.0, 1.0)
|
||
|
|
||
|
"""
|
||
|
[timestep] = self._format_id_list([timestep],
|
||
|
self.get_timesteps(),
|
||
|
'timestep',
|
||
|
single=True)
|
||
|
if self.timestep_exists(new_timestep):
|
||
|
self._exists_warning(new_timestep, 'timestep')
|
||
|
return
|
||
|
# find the index at which to insert the new timestep
|
||
|
new_index = self._find_new_timestep_index(new_timestep)
|
||
|
# get the index of the existing timestep prior to adding the new
|
||
|
# timestep
|
||
|
old_index = self._get_internal_timestep_index(timestep)
|
||
|
# insert the new timestep
|
||
|
self.timesteps.insert(new_index, new_timestep)
|
||
|
# adjust node_fields
|
||
|
for field in list(self.node_fields.values()):
|
||
|
field.insert(new_index, list(field[old_index]))
|
||
|
# adjust self.global_variables
|
||
|
for values in list(self.global_variables.values()):
|
||
|
values.insert(new_index, values[old_index])
|
||
|
# adjust self.element_blocks, self.node_sets, self.side_sets
|
||
|
for thing in itertools.chain(list(self.element_blocks.values()),
|
||
|
list(self.node_sets.values()),
|
||
|
list(self.side_sets.values())):
|
||
|
fields = thing[-1]
|
||
|
for field in list(fields.values()):
|
||
|
field.insert(new_index, list(field[old_index]))
|
||
|
|
||
|
def create_timestep(self, timestep):
|
||
|
"""
|
||
|
Create a new timestep.
|
||
|
|
||
|
Field information at the created timestep is set to the default value.
|
||
|
|
||
|
Example:
|
||
|
>>> model.create_timestep(0)
|
||
|
|
||
|
"""
|
||
|
timestep = float(timestep)
|
||
|
if self.timestep_exists(timestep):
|
||
|
self._exists_warning(timestep, 'timestep')
|
||
|
return
|
||
|
# find the index at which to to insert this
|
||
|
timestep_index = self._find_new_timestep_index(timestep)
|
||
|
# add the given timestep
|
||
|
self.timesteps.insert(timestep_index, timestep)
|
||
|
# adjust self.node_fields
|
||
|
for name, field in list(self.node_fields.items()):
|
||
|
value = self._get_default_field_value(name)
|
||
|
field.insert(timestep_index, [value] * len(self.nodes))
|
||
|
# adjust element blocks fields
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
element_count = self.get_element_count(id_)
|
||
|
fields = self._get_element_block_fields(id_)
|
||
|
for name, field_values in list(fields.items()):
|
||
|
value = self._get_default_field_value(name)
|
||
|
field_values.insert(timestep_index, [value] * element_count)
|
||
|
# adjust side set fields
|
||
|
for id_ in self.get_side_set_ids():
|
||
|
members = self.get_side_set_members(id_)
|
||
|
fields = self._get_side_set_fields(id_)
|
||
|
member_count = len(members)
|
||
|
for name, values in list(fields.items()):
|
||
|
value = self._get_default_field_value(name)
|
||
|
values.insert(timestep_index, [value] * member_count)
|
||
|
# adjust node set fields
|
||
|
for id_ in self.get_node_set_ids():
|
||
|
members = self.get_node_set_members(id_)
|
||
|
fields = self._get_node_set_fields(id_)
|
||
|
member_count = len(members)
|
||
|
for name, values in list(fields.items()):
|
||
|
value = self._get_default_field_value(name)
|
||
|
values.insert(timestep_index, [value] * member_count)
|
||
|
# adjust self.global_variables
|
||
|
for name, values in list(self.global_variables.items()):
|
||
|
values.insert(timestep_index, self._get_default_field_value(name))
|
||
|
|
||
|
def _replace_name_case(self, new_list, original_list):
|
||
|
"""
|
||
|
Return the lowercase version of all strings in the given list.
|
||
|
|
||
|
Example:
|
||
|
>>> model._replace_name_case(['x', 'z', 'fred'], ['X', 'Fred', 'Z'])
|
||
|
['X', 'Z', 'Fred']
|
||
|
|
||
|
"""
|
||
|
original_case = dict((x.lower(), x) for x in original_list)
|
||
|
if len(original_case) != len(original_list):
|
||
|
self._warning(
|
||
|
'Ambiguous string case.',
|
||
|
'There are multiple strings in the list which have '
|
||
|
'identical lowercase representations. One will be '
|
||
|
'chosen at random.')
|
||
|
for item in new_list:
|
||
|
if not item.lower() in original_case:
|
||
|
self._bug(
|
||
|
'Unrecognized string.',
|
||
|
'The string "%s" appears in the new list but '
|
||
|
'not in the original list.' % item)
|
||
|
return [original_case[x.lower()] for x in new_list]
|
||
|
|
||
|
def _sort_field_names(self, original_field_names):
|
||
|
"""
|
||
|
Return field names sorted in a SIERRA-friendly manner.
|
||
|
|
||
|
In order for SIERRA to recognize vectors, tensors, and element fields
|
||
|
with multiple integration points, fields must be sorted in a specific
|
||
|
order. This function provides that sort order.
|
||
|
|
||
|
As fields within exomerge are stored in a set, exomerge has no internal
|
||
|
or natural field order. This routine is only necessary for writing to
|
||
|
ExodusII files.
|
||
|
|
||
|
"""
|
||
|
field_names = [x.lower() for x in original_field_names]
|
||
|
# Look through all fields to find multi-component fields and store
|
||
|
# these as tuples of the following form:
|
||
|
# ('base_name', 'component', integration_points)
|
||
|
multicomponent_fields = set()
|
||
|
for name in field_names:
|
||
|
# see if it has an integration point
|
||
|
if re.match('.*_[0-9]+$', name):
|
||
|
(name, integration_point) = name.rsplit('_', 1)
|
||
|
integration_point = int(integration_point)
|
||
|
else:
|
||
|
integration_point = None
|
||
|
# see if it possibly has a component
|
||
|
if re.match('.*_.+$', name):
|
||
|
component = name.rsplit('_', 1)[1]
|
||
|
if component in self.ALL_MULTI_COMPONENT_FIELD_SUBSCRIPTS:
|
||
|
name = name.rsplit('_', 1)[0]
|
||
|
multicomponent_fields.add(
|
||
|
(name, component, integration_point))
|
||
|
# now sort multi-component fields
|
||
|
base_names = set(x for x, _, _ in multicomponent_fields)
|
||
|
sorted_field_names = dict()
|
||
|
field_names = set(field_names)
|
||
|
for base_name in base_names:
|
||
|
# find all components of this form
|
||
|
components = set(x for name, x, _ in multicomponent_fields
|
||
|
if name == base_name)
|
||
|
# find max integration point value
|
||
|
integration_points = set(x for name, _, x in multicomponent_fields
|
||
|
if name == base_name and x is not None)
|
||
|
if integration_points:
|
||
|
integration_point_count = max(
|
||
|
x for name, _, x in multicomponent_fields
|
||
|
if name == base_name and x is not None)
|
||
|
else:
|
||
|
integration_point_count = None
|
||
|
|
||
|
# see if the components match the form of something
|
||
|
matching_form = None
|
||
|
for form, included_components in (list(
|
||
|
self.MULTI_COMPONENT_FIELD_SUBSCRIPTS.items())):
|
||
|
if set(included_components) == components:
|
||
|
matching_form = form
|
||
|
if not matching_form:
|
||
|
continue
|
||
|
# see if all components and integration points are present
|
||
|
mid = [
|
||
|
'_' + x
|
||
|
for x in self.MULTI_COMPONENT_FIELD_SUBSCRIPTS[matching_form]
|
||
|
]
|
||
|
if integration_point_count is None:
|
||
|
last = ['']
|
||
|
else:
|
||
|
last = [
|
||
|
'_' + str(x + 1) for x in range(integration_point_count)
|
||
|
]
|
||
|
all_names = [base_name + m + s for s in last for m in mid]
|
||
|
if set(all_names).issubset(field_names):
|
||
|
sorted_field_names[all_names[0]] = all_names
|
||
|
field_names = field_names - set(all_names)
|
||
|
# sort field names which are not part of multicomponent fields
|
||
|
field_names = sorted(field_names)
|
||
|
# for each list of field names, find place to splice into list
|
||
|
place_to_insert = dict()
|
||
|
for name in list(sorted_field_names.keys()):
|
||
|
place = bisect.bisect_left(field_names, name)
|
||
|
if place not in place_to_insert:
|
||
|
place_to_insert[place] = [name]
|
||
|
else:
|
||
|
place_to_insert[place].append(name)
|
||
|
# splice them in
|
||
|
for place in sorted(list(place_to_insert.keys()), reverse=True):
|
||
|
for name in place_to_insert[place]:
|
||
|
field_names[place:place] = sorted_field_names[name]
|
||
|
return self._replace_name_case(field_names, original_field_names)
|
||
|
|
||
|
def _reorder_list(self, the_list, new_index):
|
||
|
"""
|
||
|
Reorder a list by permuting items.
|
||
|
|
||
|
The item at index 0 in the original list is moved to index
|
||
|
'new_index[0]', and so on.
|
||
|
|
||
|
"""
|
||
|
# ensure arguments are valid
|
||
|
self._input_check(new_index, [list, len(the_list), int])
|
||
|
# create new list
|
||
|
the_list[:] = [the_list[i] for i in new_index]
|
||
|
|
||
|
def _sort_timesteps(self):
|
||
|
"""Sort timesteps so that they are in ascending order."""
|
||
|
index_map = [self.timesteps.index(x) for x in sorted(self.timesteps)]
|
||
|
self._reorder_list(self.timesteps, index_map)
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
for values in list(fields.values()):
|
||
|
self._reorder_list(values, index_map)
|
||
|
for values in list(self.node_fields.values()):
|
||
|
self._reorder_list(values, index_map)
|
||
|
for values in list(self.global_variables.values()):
|
||
|
self._reorder_list(values, index_map)
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
for values in list(fields.values()):
|
||
|
self._reorder_list(values, index_map)
|
||
|
for side_set_id in self.get_side_set_ids():
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
for values in list(fields.values()):
|
||
|
self._reorder_list(values, index_map)
|
||
|
|
||
|
def _apply_node_map(self, node_map):
|
||
|
"""
|
||
|
Apply the given node map to reorder all nodes.
|
||
|
|
||
|
Node i becomes node node_map[i].
|
||
|
|
||
|
"""
|
||
|
assert sorted(node_map) == list(range(len(self.nodes)))
|
||
|
# find reverse_node_map
|
||
|
reverse_node_map = self._get_reverse_index_map(node_map)
|
||
|
# reorder nodes
|
||
|
self.nodes = [self.nodes[x] for x in reverse_node_map]
|
||
|
# reorder element connectivity
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
connectivity[:] = [node_map[x] for x in connectivity]
|
||
|
# reorder node fields
|
||
|
for field in list(self.node_fields.values()):
|
||
|
for timestep_index in range(len(self.timesteps)):
|
||
|
values = field[timestep_index]
|
||
|
field[timestep_index] = [values[x] for x in reverse_node_map]
|
||
|
# reorder nodes in node sets
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
members[:] = [node_map[x] for x in members]
|
||
|
|
||
|
def _order_nodes(self):
|
||
|
"""
|
||
|
Redefine node ordering in a consistent manner.
|
||
|
|
||
|
This was introduced because running exodiff on two different
|
||
|
models with the same information but differing node numbering will
|
||
|
fail. By running this routine before export_model, the numbering
|
||
|
will be consistent and will avoid this problem.
|
||
|
|
||
|
"""
|
||
|
# Define new node numbering by the order in which nodes are encountered
|
||
|
# in element blocks by ascending element block id. For nodes not used
|
||
|
# in any element blocks, they are sorted by x coordinate, then y
|
||
|
# coordinate, then z coordinate.
|
||
|
# node i on old model becomes node_map[i]
|
||
|
# node i in new model was reverse_node_map[i]
|
||
|
next_index = 0
|
||
|
node_map = [None] * len(self.nodes)
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
for node_index in connectivity:
|
||
|
if node_map[node_index] is None:
|
||
|
node_map[node_index] = next_index
|
||
|
next_index += 1
|
||
|
still_to_sort = []
|
||
|
for index, map_ in enumerate(node_map):
|
||
|
if map_ is None:
|
||
|
still_to_sort.append((self.nodes[index], index))
|
||
|
still_to_sort.sort()
|
||
|
for _, node_index in still_to_sort:
|
||
|
node_map[node_index] = next_index
|
||
|
next_index += 1
|
||
|
self._apply_node_map(node_map)
|
||
|
|
||
|
def get_input_deck(self):
|
||
|
"""
|
||
|
Return each line of the input deck stored in the file.
|
||
|
|
||
|
Many SIERRA applications, when running a problem, will store the input
|
||
|
deck within the results file. This function retrieves that
|
||
|
information, if it exists. Note that due to format restriction, the
|
||
|
retrieved input deck may not exactly match the original file.
|
||
|
|
||
|
Example:
|
||
|
>>> object = exomerge.import_model('results.e')
|
||
|
>>> model.get_input_deck()
|
||
|
|
||
|
"""
|
||
|
continuation = False
|
||
|
input_deck = []
|
||
|
begin_depth = 0
|
||
|
first_word = None
|
||
|
for line in self.info_records:
|
||
|
if not continuation:
|
||
|
first_word = line.strip().split(' ', 1)[0].lower()
|
||
|
if not continuation and first_word == 'begin':
|
||
|
begin_depth += 1
|
||
|
if begin_depth > 0:
|
||
|
if continuation:
|
||
|
input_deck[-1] = input_deck[-1][:-1] + line
|
||
|
else:
|
||
|
input_deck.append(line)
|
||
|
continuation = (begin_depth > 0 and len(line) == 80
|
||
|
and line[-1] == '\\')
|
||
|
if not continuation and first_word == 'end':
|
||
|
begin_depth -= 1
|
||
|
return '\n'.join(input_deck)
|
||
|
|
||
|
def get_length_scale(self):
|
||
|
"""
|
||
|
Return the length scale of the model.
|
||
|
|
||
|
The length scale is defined as the largest of the following:
|
||
|
* absolute nodal coordinate component
|
||
|
* total range in nodal coordinate component
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_length_scale()
|
||
|
|
||
|
"""
|
||
|
if not self.nodes:
|
||
|
return 0.0
|
||
|
bounds = [[self.nodes[0][d], self.nodes[0][d]] for d in range(3)]
|
||
|
for node in self.nodes:
|
||
|
for d in range(3):
|
||
|
if node[d] < bounds[d][0]:
|
||
|
bounds[d][0] = node[d]
|
||
|
if node[d] > bounds[d][1]:
|
||
|
bounds[d][1] = node[d]
|
||
|
relative = max([bound[1] - bound[0] for bound in bounds])
|
||
|
abs0 = max([abs(bound[0]) for bound in bounds])
|
||
|
abs1 = max([abs(bound[1]) for bound in bounds])
|
||
|
absolute = max([abs0, abs1])
|
||
|
return max(relative, absolute)
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_reverse_index_map(index_map):
|
||
|
"""Return the reverse map for the given index mapping scheme."""
|
||
|
reverse_index_map = [None] * len(index_map)
|
||
|
for index, new_index in enumerate(index_map):
|
||
|
reverse_index_map[new_index] = index
|
||
|
return reverse_index_map
|
||
|
|
||
|
@staticmethod
|
||
|
def _dot(vector_one, vector_two):
|
||
|
"""Return the dot product of two things."""
|
||
|
return sum(a * b for a, b in zip(vector_one, vector_two))
|
||
|
|
||
|
@staticmethod
|
||
|
def _distance_squared_between(point_one, point_two):
|
||
|
"""Return the distance squared between two three-dimensional points."""
|
||
|
return ((point_two[0] - point_one[0])**2 +
|
||
|
(point_two[1] - point_one[1])**2 +
|
||
|
(point_two[2] - point_one[2])**2)
|
||
|
|
||
|
@staticmethod
|
||
|
def _distance_between(point_one, point_two):
|
||
|
"""Return the distance between two three-dimensional points."""
|
||
|
return math.sqrt((point_two[0] - point_one[0])**2 +
|
||
|
(point_two[1] - point_one[1])**2 +
|
||
|
(point_two[2] - point_one[2])**2)
|
||
|
|
||
|
@staticmethod
|
||
|
def _values_match(value, list_of_values):
|
||
|
"""
|
||
|
Return True if the list contains no items apart from the given value.
|
||
|
|
||
|
Example:
|
||
|
>>> _values_match(0, [0, 0, 0])
|
||
|
True
|
||
|
>>> _values_match(1, [0, 1, 0])
|
||
|
False
|
||
|
|
||
|
"""
|
||
|
if not isinstance(value, float) or not math.isnan(value):
|
||
|
return not any(x != value for x in list_of_values)
|
||
|
return not any(not math.isnan(x) for x in list_of_values)
|
||
|
|
||
|
def _merge_node_groups(self, node_groups, suppress_warnings=False):
|
||
|
"""
|
||
|
Merge nodes in the given node groups.
|
||
|
|
||
|
This updates node field and node set field values with the average of
|
||
|
the values at the merged nodes. If they differ, a warning is output in
|
||
|
addition.
|
||
|
|
||
|
Node sets and node set fields are updated accordingly.
|
||
|
|
||
|
Example:
|
||
|
>>> model._merge_node_groups({1: [2, 3], 5: [7, 8]})
|
||
|
|
||
|
"""
|
||
|
# ensure slave nodes are not duplicated
|
||
|
slave_nodes_set = set()
|
||
|
duplicated_slave_nodes = set()
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
for slave in slaves:
|
||
|
if slave in slave_nodes_set:
|
||
|
duplicated_slave_nodes.add(slave)
|
||
|
else:
|
||
|
slave_nodes_set.add(slave)
|
||
|
if duplicated_slave_nodes:
|
||
|
problem_groups = dict()
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
for index in slaves:
|
||
|
if index in duplicated_slave_nodes:
|
||
|
problem_groups[master] = slaves
|
||
|
groups = '\n '.join(
|
||
|
str(x) + ': ' + str(y)
|
||
|
for x, y in list(problem_groups.items()))
|
||
|
self._bug(
|
||
|
'Invalid merged node groups.',
|
||
|
'Slaves nodes were found in multiple merged groups. '
|
||
|
'Conflicting merged node groups:\n %s' % (groups))
|
||
|
# ensure validity of input
|
||
|
# slave nodes are not repeated
|
||
|
# master nodes are never slave nodes
|
||
|
master_nodes = sorted(node_groups.keys())
|
||
|
slave_nodes = sorted(list(
|
||
|
itertools.chain(*list(node_groups.values()))))
|
||
|
slave_nodes_set = set(slave_nodes)
|
||
|
# ensure master nodes are not slave nodes
|
||
|
for master in master_nodes:
|
||
|
if master in slave_nodes_set:
|
||
|
self._bug(
|
||
|
'Invalid merged node groups.',
|
||
|
'The master node %d is also found in a slave node '
|
||
|
'group.' % (master))
|
||
|
# ensure slave nodes are not in multiple groups
|
||
|
if not sorted(slave_nodes_set) == slave_nodes:
|
||
|
self._bug('Invalid merged node groups.',
|
||
|
'Slave nodes are duplicated in multiple groups.')
|
||
|
# First, remap all nodes such that slave nodes appear at the very
|
||
|
# end.
|
||
|
next_master_index = 0
|
||
|
first_slave_index = len(self.nodes) - len(slave_nodes)
|
||
|
next_slave_index = first_slave_index
|
||
|
node_map = []
|
||
|
index = 0
|
||
|
for slave_node in slave_nodes:
|
||
|
if slave_node != index:
|
||
|
count = slave_node - index
|
||
|
assert count > 0
|
||
|
node_map.extend(
|
||
|
range(next_master_index, next_master_index + count))
|
||
|
index += count
|
||
|
next_master_index += count
|
||
|
node_map.append(next_slave_index)
|
||
|
next_slave_index += 1
|
||
|
index += 1
|
||
|
count = first_slave_index - next_master_index
|
||
|
node_map.extend(range(next_master_index, next_master_index + count))
|
||
|
next_master_index += count
|
||
|
assert next_master_index == first_slave_index
|
||
|
assert next_slave_index == len(self.nodes)
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
assert node_map[master] < first_slave_index
|
||
|
assert min([node_map[x] for x in slaves]) >= first_slave_index
|
||
|
# apply this node map
|
||
|
self._apply_node_map(node_map)
|
||
|
# apply the mapping to node_groups
|
||
|
node_groups = dict((node_map[key], [node_map[x] for x in values])
|
||
|
for key, values in list(node_groups.items()))
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
assert master < first_slave_index
|
||
|
assert min(slaves) >= first_slave_index
|
||
|
# get connectivity mapping
|
||
|
connectivity_map = list(range(len(self.nodes)))
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
for slave in slaves:
|
||
|
connectivity_map[slave] = master
|
||
|
# change connectivity in element_blocks
|
||
|
for element_block_id in self.get_element_block_ids():
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
connectivity[:] = [connectivity_map[x] for x in connectivity]
|
||
|
# change self.node_fields
|
||
|
node_field_value_warnings = 0
|
||
|
for name, all_values in list(self.node_fields.items()):
|
||
|
for values in all_values:
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
master_value = values[master]
|
||
|
slave_values = [values[x] for x in slaves]
|
||
|
if not self._values_match(master_value, slave_values):
|
||
|
if (not node_field_value_warnings
|
||
|
and not suppress_warnings):
|
||
|
self._warning(
|
||
|
'Node field values do not match.',
|
||
|
'Nodes are being merged but values at these '
|
||
|
'nodes for node field "%s" do not match. An '
|
||
|
'averaged value will be used.\n'
|
||
|
'\n'
|
||
|
'Future warnings of this type will be '
|
||
|
'suppressed.' % (name))
|
||
|
node_field_value_warnings += 1
|
||
|
values[master] = (values[master] + sum(slave_values)
|
||
|
) / float(1 + len(slaves))
|
||
|
del values[first_slave_index:]
|
||
|
# change self.node_sets
|
||
|
node_set_member_warnings = 0
|
||
|
node_set_value_warnings = 0
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
members_set = set(members)
|
||
|
member_indices_to_delete = []
|
||
|
for master, slaves in list(node_groups.items()):
|
||
|
master_included = master in members_set
|
||
|
slaves_included = []
|
||
|
slaves_not_included = []
|
||
|
for slave in slaves:
|
||
|
if slave in members_set:
|
||
|
slaves_included.append(slave)
|
||
|
else:
|
||
|
slaves_not_included.append(slave)
|
||
|
if not master_included and not slaves_included:
|
||
|
continue
|
||
|
# warn if not all nodes are in the set
|
||
|
if not master_included or slaves_not_included:
|
||
|
if not node_set_member_warnings and not suppress_warnings:
|
||
|
self._warning(
|
||
|
'Ambiguous merge of nodes.',
|
||
|
'Node are being merged, but only some of these '
|
||
|
'nodes belong to a given node set. The operation '
|
||
|
'is therefore ambiguous on whether or not to '
|
||
|
'include the merged node in the set. The merged '
|
||
|
'node will be included in the set.\n\nFuture '
|
||
|
'warnings of this type will be suppressed.')
|
||
|
node_set_member_warnings += 1
|
||
|
# if master node is not included, steal the position of a
|
||
|
# slave node
|
||
|
if not master_included:
|
||
|
members[members.index(slaves_included[0])] = master
|
||
|
del slaves_included[0]
|
||
|
master_included = True
|
||
|
if not slaves_included:
|
||
|
continue
|
||
|
master_index = members.index(master)
|
||
|
slave_indices = [members.index(x) for x in slaves_included]
|
||
|
# mark slaves to delete
|
||
|
member_indices_to_delete.extend(slave_indices)
|
||
|
# average values, warn if they are not the same
|
||
|
for name, all_values in list(fields.items()):
|
||
|
for values in all_values:
|
||
|
slave_values = [values[x] for x in slave_indices]
|
||
|
if not self._values_match(values[master_index],
|
||
|
slave_values):
|
||
|
if (not node_set_value_warnings
|
||
|
and not suppress_warnings):
|
||
|
self._warning(
|
||
|
'Node set field values do not match.',
|
||
|
'Nodes are being merged but values at '
|
||
|
'these nodes for node set field %s do '
|
||
|
'not match. An averaged values will '
|
||
|
'be used.\n\nFuture warnings of this '
|
||
|
'type will be suppressed.' % (name))
|
||
|
node_set_value_warnings += 1
|
||
|
new_value = (values[master_index] + sum(slave_values)
|
||
|
) / float(1 + len(slave_indices))
|
||
|
values[master_index] = new_value
|
||
|
# delete slave members
|
||
|
for index in sorted(member_indices_to_delete, reverse=True):
|
||
|
del members[index]
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
del values[index]
|
||
|
# delete those nodes
|
||
|
del self.nodes[first_slave_index:]
|
||
|
|
||
|
def _merge_node_pairs(self, node_pairs):
|
||
|
"""
|
||
|
Merge the given node pairs.
|
||
|
|
||
|
Example:
|
||
|
>>> model._merge_node_pairs([(1, 3), (2, 5), (1, 5)])
|
||
|
|
||
|
"""
|
||
|
# create groups of nodes to merge
|
||
|
node_group = [None] * len(self.nodes)
|
||
|
merge_group = []
|
||
|
for pair in node_pairs:
|
||
|
master, slave = sorted(pair)
|
||
|
master_group = node_group[master]
|
||
|
slave_group = node_group[slave]
|
||
|
if master_group is None and slave_group is None:
|
||
|
node_group[master] = len(merge_group)
|
||
|
node_group[slave] = len(merge_group)
|
||
|
merge_group.append([master, slave])
|
||
|
elif master_group is None:
|
||
|
node_group[master] = slave_group
|
||
|
merge_group[slave_group].append(master)
|
||
|
elif slave_group is None:
|
||
|
node_group[slave] = master_group
|
||
|
merge_group[master_group].append(slave)
|
||
|
elif master_group != slave_group:
|
||
|
for index in merge_group[slave_group]:
|
||
|
node_group[index] = master_group
|
||
|
merge_group[master_group].extend(merge_group[slave_group])
|
||
|
merge_group[slave_group] = []
|
||
|
group_to_merge = dict()
|
||
|
for group in merge_group:
|
||
|
if not group:
|
||
|
continue
|
||
|
master = min(group)
|
||
|
assert master not in group_to_merge
|
||
|
group_to_merge[master] = sorted(set(group) - set([master]))
|
||
|
self._merge_node_groups(group_to_merge)
|
||
|
|
||
|
def delete_duplicate_elements(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Delete duplicate elements.
|
||
|
|
||
|
For this calculation, a duplicate element is an element which shares
|
||
|
all of its nodes with another element.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
# go through each block and delete elements
|
||
|
for id_ in element_block_ids:
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
element_count = self.get_element_count(id_)
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
assert len(connectivity) == nodes_per_element * element_count
|
||
|
# sort elements into sets which contain
|
||
|
elements = set()
|
||
|
duplicates = []
|
||
|
for x in range(0, element_count):
|
||
|
start = x * nodes_per_element
|
||
|
element = set(connectivity[start:start + nodes_per_element])
|
||
|
element = tuple(sorted(element))
|
||
|
if element in elements:
|
||
|
duplicates.append(x)
|
||
|
else:
|
||
|
elements.add(element)
|
||
|
self._delete_elements(id_, duplicates)
|
||
|
|
||
|
def _find_close_nodes(self, tolerance):
|
||
|
"""
|
||
|
Return groups of nodes that are close to one another.
|
||
|
|
||
|
This will search for all nodes which are separated by at most
|
||
|
'tolerance' and return a list of node groups.
|
||
|
|
||
|
If node A is close to node B, and node B is close to node C, then each
|
||
|
A, B, and C will belong to the same node group, regardless of whether
|
||
|
or not node A is close to node C.
|
||
|
|
||
|
Results are returned as a dict of node groups with the lowest index
|
||
|
node being the key and the value as the list of slave nodes. For
|
||
|
example, if nodes 0 and 1 are close, and 2 and 3 are close, it would
|
||
|
return:
|
||
|
>>> model._find_close_nodes()
|
||
|
{0: [1], 2: [3]}
|
||
|
|
||
|
"""
|
||
|
# create a sorting vector
|
||
|
sorting_vector = [math.pi, 2.0, 0.5 * math.sqrt(2)]
|
||
|
scale = math.sqrt(sum([x * x for x in sorting_vector]))
|
||
|
sorting_vector = [x / scale for x in sorting_vector]
|
||
|
# create a list of node indices sorted by distance along that vector
|
||
|
sorted_nodal_distances = sorted([(self._dot(x, sorting_vector), i)
|
||
|
for i, x in enumerate(self.nodes)])
|
||
|
# go through the list to find node groups
|
||
|
node_count = len(self.nodes)
|
||
|
lower_index = 0
|
||
|
upper_index = 1
|
||
|
master_nodes = list(range(len(self.nodes)))
|
||
|
while lower_index < node_count:
|
||
|
if (upper_index >= node_count
|
||
|
or sorted_nodal_distances[upper_index][0] -
|
||
|
sorted_nodal_distances[lower_index][0] > tolerance):
|
||
|
lower_index += 1
|
||
|
upper_index = lower_index + 1
|
||
|
continue
|
||
|
lower_element = sorted_nodal_distances[lower_index]
|
||
|
upper_element = sorted_nodal_distances[upper_index]
|
||
|
if self._distance_between(
|
||
|
self.nodes[lower_element[1]],
|
||
|
self.nodes[upper_element[1]]) <= tolerance:
|
||
|
one, two = sorted([lower_element[1], upper_element[1]])
|
||
|
master_nodes[two] = one
|
||
|
upper_index += 1
|
||
|
# create a list of close node groups
|
||
|
close_node_groups = dict()
|
||
|
for index, master_node in enumerate(master_nodes):
|
||
|
if index != master_node:
|
||
|
while master_nodes[master_node] != master_node:
|
||
|
assert master_nodes[master_node] < master_node
|
||
|
master_node = master_nodes[master_node]
|
||
|
if master_node not in close_node_groups:
|
||
|
close_node_groups[master_node] = []
|
||
|
close_node_groups[master_node].append(index)
|
||
|
# return the result
|
||
|
return close_node_groups
|
||
|
|
||
|
def merge_nodes(self,
|
||
|
tolerance=1e-6,
|
||
|
relative_tolerance=True,
|
||
|
suppress_warnings=False):
|
||
|
"""
|
||
|
Merge nodes that are closer than the given tolerance.
|
||
|
|
||
|
Node fields, node sets and node set fields are updated accordingly.
|
||
|
Using a tolerance of 0 will merge nodes at the exact same location.
|
||
|
|
||
|
If 'relative_tolerace=True', this will multiply the tolerance by the
|
||
|
length scale of the model as obtained from 'get_length_scale()'.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.merge_nodes(0)
|
||
|
>>> model.merge_nodes()
|
||
|
|
||
|
"""
|
||
|
# find the absolute tolerance
|
||
|
if relative_tolerance:
|
||
|
tolerance *= self.get_length_scale()
|
||
|
# find the node groups
|
||
|
close_node_groups = self._find_close_nodes(tolerance)
|
||
|
# merge these node groups
|
||
|
self._merge_node_groups(close_node_groups,
|
||
|
suppress_warnings=suppress_warnings)
|
||
|
|
||
|
def _duplicate_nodes(self, node_indices, new_node_indices):
|
||
|
"""
|
||
|
Create duplicates of the given nodes.
|
||
|
|
||
|
This updates node fields, node sets, and node set fields consistently.
|
||
|
If a node is part of a node set, its duplicated node will be added to
|
||
|
the node set.
|
||
|
|
||
|
"""
|
||
|
new_node_indices[:] = range(len(self.nodes),
|
||
|
len(self.nodes) + len(node_indices))
|
||
|
# update self.nodes
|
||
|
new_nodes = [list(self.nodes[x]) for x in node_indices]
|
||
|
self.create_nodes(new_nodes)
|
||
|
# update self.node_fields
|
||
|
for all_values in list(self.node_fields.values()):
|
||
|
for values in all_values:
|
||
|
for master, slave in zip(node_indices, new_node_indices):
|
||
|
values[slave] = values[master]
|
||
|
# update self.node_sets
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
for master, slave in zip(node_indices, new_node_indices):
|
||
|
if master in members:
|
||
|
members.append(slave)
|
||
|
master_index = members.index(master)
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
values.append(values[master_index])
|
||
|
|
||
|
def _create_averaged_nodes(self, node_indices, new_node_indices):
|
||
|
"""
|
||
|
Create duplicates nodes which are averaged from the given nodes.
|
||
|
|
||
|
If all nodes which are being averaged are in a given node set, the
|
||
|
newly created node is also added the node set.
|
||
|
|
||
|
For example, 'node_indices = [(0, 1, 2)]' will create a new node which
|
||
|
is the average of nodes 1, 2, and 3. Passing
|
||
|
'node_indices = [((0, 0.25), (1, 0.75))]' will create a node which is
|
||
|
a weighted combination of nodes 0 and 1.
|
||
|
|
||
|
"""
|
||
|
first_node_index = len(self.nodes)
|
||
|
new_node_indices[:] = list(
|
||
|
range(len(self.nodes),
|
||
|
len(self.nodes) + len(node_indices)))
|
||
|
# format node_indices such that each value is of the given form:
|
||
|
# ((index, weight), (index2, weight2), ...)
|
||
|
new_node_indices = []
|
||
|
for index_list in node_indices:
|
||
|
new_index_list = []
|
||
|
if isinstance(index_list, int):
|
||
|
new_index_list = [[index_list, 1.0]]
|
||
|
else:
|
||
|
length = float(len(index_list))
|
||
|
for specifier in index_list:
|
||
|
if isinstance(specifier, int):
|
||
|
new_index_list.append([specifier, 1.0 / length])
|
||
|
else:
|
||
|
new_index_list.append(specifier)
|
||
|
new_node_indices.append(new_index_list)
|
||
|
node_indices = new_node_indices
|
||
|
# ensure weights add up to 1
|
||
|
for index_list in node_indices:
|
||
|
total = sum(x[1] for x in index_list)
|
||
|
if abs(total - 1.0) > 1e-14:
|
||
|
self._bug(
|
||
|
'Incorrect weights.',
|
||
|
'The given node averaging weights do not add up '
|
||
|
'to 1.')
|
||
|
# create the new nodes
|
||
|
first_new_node_index = len(self.nodes)
|
||
|
new_nodes = []
|
||
|
for index_list in node_indices:
|
||
|
this_new_node = [
|
||
|
sum(weight * self.nodes[index][d]
|
||
|
for index, weight in index_list) for d in range(3)
|
||
|
]
|
||
|
new_nodes.append(this_new_node)
|
||
|
self.create_nodes(new_nodes)
|
||
|
# update self.node_fields
|
||
|
for all_values in list(self.node_fields.values()):
|
||
|
for values in all_values:
|
||
|
new_values = [
|
||
|
sum(weight * values[index] for index, weight in index_list)
|
||
|
for index_list in node_indices
|
||
|
]
|
||
|
values[first_node_index:] = new_values
|
||
|
# update self.node_sets
|
||
|
for node_set_id in self.get_node_set_ids():
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
members_set = set(members)
|
||
|
index_from_member_index = dict(
|
||
|
(node_index, index)
|
||
|
for index, node_index in enumerate(members))
|
||
|
for node_index, index_list in enumerate(node_indices):
|
||
|
check = set(x in members_set for x, _ in index_list)
|
||
|
if False in check:
|
||
|
continue
|
||
|
# okay, we need to add this node to the set
|
||
|
members.append(first_new_node_index + node_index)
|
||
|
# and calculate its value for all fields
|
||
|
for all_values in list(fields.values()):
|
||
|
for values in all_values:
|
||
|
new_value = sum(weight *
|
||
|
values[index_from_member_index[index]]
|
||
|
for index, weight in index_list)
|
||
|
values.append(new_value)
|
||
|
|
||
|
def unmerge_element_blocks(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Duplicate nodes to unmerge element blocks.
|
||
|
|
||
|
For elements blocks that share one or more nodes, duplicate these nodes
|
||
|
and unmerge the element blocks. For example, if element block A and B
|
||
|
share node 1, that node will be duplicated, block A will use the
|
||
|
original node and block B will use the duplicate.
|
||
|
|
||
|
Node fields, node sets, and node set fields are updated accordingly.
|
||
|
|
||
|
Example:
|
||
|
>>> model.unmerge_element_blocks()
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
if len(element_block_ids) <= 1:
|
||
|
return
|
||
|
# Separate each pair of blocks. Keep nodes on block id1 the same and
|
||
|
# create new nodes for block id2. Update node fields. Update node
|
||
|
# sets such that if a shared node is part of the set, the duplicated
|
||
|
# node is also part of the set.
|
||
|
for id1, id2 in itertools.combinations(element_block_ids, 2):
|
||
|
shared_nodes_set = set.intersection(
|
||
|
set(self.get_nodes_in_element_block(id1)),
|
||
|
set(self.get_nodes_in_element_block(id2)))
|
||
|
shared_nodes = sorted(shared_nodes_set)
|
||
|
if not shared_nodes:
|
||
|
continue
|
||
|
# on block id2, old node x becomes new node
|
||
|
# node_offset + shared_nodes.index(x)
|
||
|
# create the new nodes
|
||
|
new_shared_nodes = []
|
||
|
self._duplicate_nodes(shared_nodes, new_shared_nodes)
|
||
|
# update element block 2 connectivity
|
||
|
connectivity = self.get_connectivity(id2)
|
||
|
for index, node_index in enumerate(connectivity):
|
||
|
if node_index in shared_nodes_set:
|
||
|
connectivity[index] = new_shared_nodes[shared_nodes.index(
|
||
|
node_index)]
|
||
|
# blocks should no longer share nodes
|
||
|
assert not set.intersection(
|
||
|
set(self.get_nodes_in_element_block(id1)),
|
||
|
set(self.get_nodes_in_element_block(id2)))
|
||
|
|
||
|
def import_model(self,
|
||
|
filename,
|
||
|
element_block_ids='all',
|
||
|
timesteps='all',
|
||
|
node_field_names='all',
|
||
|
element_field_names='all',
|
||
|
side_set_ids='all',
|
||
|
node_set_ids='all',
|
||
|
global_variable_names='all',
|
||
|
node_set_field_names='all',
|
||
|
side_set_field_names='all'):
|
||
|
"""
|
||
|
Import information from an ExodusII file.
|
||
|
|
||
|
This will add to the current model in memory, so multiple calls will
|
||
|
act to merge files. As a shortcut, one may use the
|
||
|
'exomerge.import_model' to create a new model from a file.
|
||
|
>>> model = exomerge.import_model('output.e')
|
||
|
|
||
|
By default, this will import all information from the given ExodusII
|
||
|
results file. To import only part of a mesh file, or to load only a
|
||
|
particular timestep, one can use the following options.
|
||
|
>>> model.import_model('output.e', element_block_ids=[1, 2])
|
||
|
>>> model.import_model('output.e', timesteps='last')
|
||
|
>>> model.import_model('output.e', node_field_names='disp_*')
|
||
|
|
||
|
To import only the mesh without any field information, one can use the
|
||
|
following syntax.
|
||
|
>>> model.import_model('output.e', timesteps='none')
|
||
|
|
||
|
Example:
|
||
|
>>> model.import_model('mesh_file.g')
|
||
|
>>> model.import_model('results_file.e')
|
||
|
|
||
|
"""
|
||
|
# open the file for read
|
||
|
if not os.path.isfile(filename):
|
||
|
self._error('File not found',
|
||
|
'The specified file "%s" was not found.' % filename)
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
save_stdout = sys.stdout
|
||
|
sys.stdout = DummyFile()
|
||
|
exodus_file = exodus.exodus(filename, mode='r')
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
sys.stdout = save_stdout
|
||
|
# format timesteps to retrieve
|
||
|
file_timesteps = list(exodus_file.get_times())
|
||
|
if timesteps == 'last_if_any':
|
||
|
if file_timesteps:
|
||
|
timesteps = 'last'
|
||
|
else:
|
||
|
timesteps = 'none'
|
||
|
timesteps = self._format_id_list(timesteps, file_timesteps, 'timestep')
|
||
|
# format block id list
|
||
|
file_element_block_ids = list(exodus_file.get_elem_blk_ids())
|
||
|
file_element_block_names = []
|
||
|
if file_element_block_ids:
|
||
|
file_element_block_names = list(exodus_file.get_elem_blk_names())
|
||
|
element_block_ids = self._format_id_list(
|
||
|
element_block_ids, sorted(file_element_block_ids), 'element block')
|
||
|
# format other id lists
|
||
|
node_field_names = self._format_id_list(
|
||
|
node_field_names,
|
||
|
sorted(list(exodus_file.get_node_variable_names())), 'node field')
|
||
|
file_element_field_names = list(
|
||
|
exodus_file.get_element_variable_names())
|
||
|
element_field_names = self._format_id_list(
|
||
|
element_field_names, sorted(file_element_field_names),
|
||
|
'element field')
|
||
|
file_side_set_ids = list(exodus_file.get_side_set_ids())
|
||
|
file_side_set_names = []
|
||
|
if file_side_set_ids:
|
||
|
file_side_set_names = list(exodus_file.get_side_set_names())
|
||
|
side_set_ids = self._format_id_list(side_set_ids,
|
||
|
sorted(file_side_set_ids),
|
||
|
'side set')
|
||
|
file_node_set_ids = list(exodus_file.get_node_set_ids())
|
||
|
file_node_set_names = []
|
||
|
if file_node_set_ids:
|
||
|
file_node_set_names = list(exodus_file.get_node_set_names())
|
||
|
node_set_ids = self._format_id_list(node_set_ids,
|
||
|
sorted(file_node_set_ids),
|
||
|
'node set')
|
||
|
global_variable_names = self._format_id_list(
|
||
|
global_variable_names,
|
||
|
sorted(list(exodus_file.get_global_variable_names())),
|
||
|
'global variable')
|
||
|
file_node_set_field_names = list(
|
||
|
exodus_file.get_node_set_variable_names())
|
||
|
node_set_field_names = self._format_id_list(
|
||
|
node_set_field_names, sorted(file_node_set_field_names),
|
||
|
'node set field')
|
||
|
file_side_set_field_names = list(
|
||
|
exodus_file.get_side_set_variable_names())
|
||
|
side_set_field_names = self._format_id_list(
|
||
|
side_set_field_names, sorted(file_side_set_field_names),
|
||
|
'node set field')
|
||
|
# ensure element blocks in this file do not already exist
|
||
|
for element_block_id in element_block_ids:
|
||
|
if self.element_block_exists(element_block_id):
|
||
|
self._error(
|
||
|
'Element block already exists.',
|
||
|
'Cannot import element block \"%d\" since it already '
|
||
|
'exists in the model.' % element_block_id)
|
||
|
# create new nodes used in this file
|
||
|
node_offset = len(self.nodes)
|
||
|
# store nodes we are going to import
|
||
|
# if we're importing all element blocks, then import all nodes
|
||
|
# else only import nodes in the element blocks we're choosing
|
||
|
# note that if there are no element blocks, this means we import all
|
||
|
# nodes
|
||
|
# note: new_used_nodes is 1-based
|
||
|
if element_block_ids == sorted(file_element_block_ids):
|
||
|
new_used_nodes = list(range(1, exodus_file.num_nodes() + 1))
|
||
|
else:
|
||
|
new_used_nodes = [False] * exodus_file.num_nodes()
|
||
|
# add nodes in blocks we are importing
|
||
|
for element_block_id in element_block_ids:
|
||
|
connectivity = exodus_file.get_elem_connectivity(
|
||
|
element_block_id)[0]
|
||
|
for i in connectivity:
|
||
|
new_used_nodes[i - 1] = True
|
||
|
# save indices for nodes we want to import
|
||
|
new_used_nodes = [i + 1 for i, x in enumerate(new_used_nodes) if x]
|
||
|
# get new node coordinates
|
||
|
new_nodes = []
|
||
|
file_coords = [list(x) for x in exodus_file.get_coords()]
|
||
|
for i in new_used_nodes:
|
||
|
new_nodes.append([file_coords[x][i - 1] for x in range(3)])
|
||
|
# create node mapping
|
||
|
# node i in the file refers to node node_map[i] in the model
|
||
|
node_map = [None] * (exodus_file.num_nodes() + 1)
|
||
|
for i, node in enumerate(new_used_nodes):
|
||
|
node_map[node] = i + node_offset
|
||
|
# create new nodes
|
||
|
self.create_nodes(new_nodes)
|
||
|
# find element mapping
|
||
|
# element i in the file refers to
|
||
|
# - element block id file_block_from_element[i]
|
||
|
# - local element index file_index_from_element[i]
|
||
|
# in the model
|
||
|
elements_in_block = []
|
||
|
for i in file_element_block_ids:
|
||
|
elements_in_block.append(exodus_file.elem_blk_info(i)[1])
|
||
|
file_block_from_element = [None]
|
||
|
file_index_from_element = [None]
|
||
|
for i, ebid in enumerate(file_element_block_ids):
|
||
|
if ebid in element_block_ids:
|
||
|
file_block_from_element.extend([ebid] * elements_in_block[i])
|
||
|
file_index_from_element.extend(range(elements_in_block[i]))
|
||
|
else:
|
||
|
file_block_from_element.extend([None] * elements_in_block[i])
|
||
|
file_index_from_element.extend([None] * elements_in_block[i])
|
||
|
# create new element blocks
|
||
|
for element_block_id in element_block_ids:
|
||
|
new_connectivity = list(
|
||
|
exodus_file.get_elem_connectivity(element_block_id))[0]
|
||
|
# apply node map
|
||
|
new_connectivity = [node_map[x] for x in new_connectivity]
|
||
|
# get the element block name
|
||
|
element_block_name = file_element_block_names[
|
||
|
file_element_block_ids.index(element_block_id)]
|
||
|
# create the element block
|
||
|
self.create_element_block(
|
||
|
element_block_id,
|
||
|
list(exodus_file.elem_blk_info(element_block_id)),
|
||
|
connectivity=new_connectivity)
|
||
|
if element_block_name:
|
||
|
self.rename_element_block(element_block_id, element_block_name)
|
||
|
# get indices of each timestep and create new timesteps
|
||
|
# list of (timestep_index_in_file, timestep_index_in_model)
|
||
|
timestep_indices = []
|
||
|
duplicate_timesteps = False
|
||
|
for timestep in timesteps:
|
||
|
if not self.timestep_exists(timestep):
|
||
|
self.create_timestep(timestep)
|
||
|
else:
|
||
|
duplicate_timesteps = True
|
||
|
timestep_indices.append(
|
||
|
(file_timesteps.index(timestep) + 1,
|
||
|
self._get_internal_timestep_index(timestep)))
|
||
|
# get list of timestep indices present in the model but not included
|
||
|
# in the import
|
||
|
included_timestep_indices = [x[1] for x in timestep_indices]
|
||
|
excluded_timestep_indices = []
|
||
|
for i in range(len(self.timesteps)):
|
||
|
if i not in included_timestep_indices:
|
||
|
excluded_timestep_indices.append(i)
|
||
|
# create new node fields
|
||
|
for node_field_name in node_field_names:
|
||
|
if not self.node_field_exists(node_field_name):
|
||
|
self.create_node_field(node_field_name)
|
||
|
# populate node field info
|
||
|
for node_field_name in node_field_names:
|
||
|
for timestep_index in timestep_indices:
|
||
|
file_node_field_values = list(
|
||
|
exodus_file.get_node_variable_values(
|
||
|
node_field_name, timestep_index[0]))
|
||
|
model_values = self.node_fields[node_field_name][
|
||
|
timestep_index[1]]
|
||
|
for i in new_used_nodes:
|
||
|
model_values[node_map[i]] = file_node_field_values[i - 1]
|
||
|
# populate node sets
|
||
|
for node_set_id in node_set_ids:
|
||
|
file_node_set_members = exodus_file.get_node_set_nodes(node_set_id)
|
||
|
model_node_set_members = []
|
||
|
# add nodes which were included
|
||
|
for i in file_node_set_members:
|
||
|
if node_map[i] is not None:
|
||
|
model_node_set_members.append(node_map[i])
|
||
|
# create the node set or add to an existing set
|
||
|
if not self.node_set_exists(node_set_id):
|
||
|
self.create_node_set(node_set_id, model_node_set_members)
|
||
|
else:
|
||
|
self.add_nodes_to_node_set(node_set_id, model_node_set_members)
|
||
|
node_set_name = file_node_set_names[file_node_set_ids.index(
|
||
|
node_set_id)]
|
||
|
if node_set_name:
|
||
|
self.rename_node_set(node_set_id, node_set_name)
|
||
|
# populate side sets
|
||
|
for side_set_id in side_set_ids:
|
||
|
file_side_set_members = exodus_file.get_side_set(side_set_id)
|
||
|
model_side_set_members = []
|
||
|
# add nodes which were included
|
||
|
for element_index, side_number in zip(*file_side_set_members):
|
||
|
if file_block_from_element[element_index] is not None:
|
||
|
model_side_set_members.append(
|
||
|
(file_block_from_element[element_index],
|
||
|
file_index_from_element[element_index],
|
||
|
side_number - 1))
|
||
|
# create the side set or add to an existing set
|
||
|
if not self.side_set_exists(side_set_id):
|
||
|
self.create_side_set(side_set_id, model_side_set_members)
|
||
|
else:
|
||
|
self.add_faces_to_side_set(side_set_id, model_side_set_members)
|
||
|
side_set_name = file_side_set_names[file_side_set_ids.index(
|
||
|
side_set_id)]
|
||
|
if side_set_name:
|
||
|
self.rename_side_set(side_set_id, side_set_name)
|
||
|
# store truth table for node set field info
|
||
|
if node_set_field_names:
|
||
|
file_node_set_truth_table = []
|
||
|
for file_node_set_id in file_node_set_ids:
|
||
|
file_node_set_truth_table.append(
|
||
|
exodus_file.get_node_set_variable_truth_table(
|
||
|
file_node_set_id))
|
||
|
# populate node set fields
|
||
|
for node_set_field_name in node_set_field_names:
|
||
|
for node_set_id in node_set_ids:
|
||
|
# don't process if field does not exist in file
|
||
|
field_exists = file_node_set_truth_table[
|
||
|
file_node_set_ids.index(node_set_id)][
|
||
|
file_node_set_field_names.index(node_set_field_name)]
|
||
|
if not field_exists:
|
||
|
continue
|
||
|
# process each included timestep
|
||
|
model_values = [None] * len(self.timesteps)
|
||
|
for timestep_index in timestep_indices:
|
||
|
file_values = list(
|
||
|
exodus_file.get_node_set_variable_values(
|
||
|
node_set_id, node_set_field_name,
|
||
|
timestep_index[0]))
|
||
|
model_values[timestep_index[1]] = file_values
|
||
|
# add default field value to excluded timestep
|
||
|
for timestep_index in excluded_timestep_indices:
|
||
|
default_value = self._get_default_field_value(
|
||
|
node_set_field_name)
|
||
|
node_count = len(self.get_node_set_members(node_set_id))
|
||
|
field = [default_value] * node_count
|
||
|
model_values[timestep_index] = field
|
||
|
# assign all values
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
fields[node_set_field_name] = model_values
|
||
|
# store truth table for side set field info
|
||
|
if side_set_field_names:
|
||
|
file_side_set_truth_table = []
|
||
|
for file_side_set_id in file_side_set_ids:
|
||
|
file_side_set_truth_table.append(
|
||
|
exodus_file.get_side_set_variable_truth_table(
|
||
|
file_side_set_id))
|
||
|
# populate side set fields
|
||
|
for side_set_field_name in side_set_field_names:
|
||
|
for side_set_id in side_set_ids:
|
||
|
# don't process if field does not exist in file
|
||
|
field_exists = file_side_set_truth_table[
|
||
|
file_side_set_ids.index(side_set_id)][
|
||
|
file_side_set_field_names.index(side_set_field_name)]
|
||
|
if not field_exists:
|
||
|
continue
|
||
|
# process each included timestep
|
||
|
model_values = [None] * len(self.timesteps)
|
||
|
for timestep_index in timestep_indices:
|
||
|
file_values = list(
|
||
|
exodus_file.get_side_set_variable_values(
|
||
|
side_set_id, side_set_field_name,
|
||
|
timestep_index[0]))
|
||
|
model_values[timestep_index[1]] = file_values
|
||
|
# add default field value to excluded timestep
|
||
|
for timestep_index in excluded_timestep_indices:
|
||
|
default_value = self._get_default_field_value(
|
||
|
side_set_field_name)
|
||
|
side_count = len(self.get_side_set_members(side_set_id))
|
||
|
field = [default_value] * side_count
|
||
|
model_values[timestep_index] = field
|
||
|
# assign all values
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
fields[side_set_field_name] = model_values
|
||
|
# store truth table for element field info
|
||
|
if element_field_names:
|
||
|
file_truth_table = []
|
||
|
for file_element_block_id in file_element_block_ids:
|
||
|
file_truth_table.append(
|
||
|
exodus_file.get_element_variable_truth_table(
|
||
|
file_element_block_id))
|
||
|
# populate element fields
|
||
|
for element_field_name in element_field_names:
|
||
|
for element_block_id in element_block_ids:
|
||
|
# don't process if field does not exist in file
|
||
|
field_exists = file_truth_table[file_element_block_ids.index(
|
||
|
element_block_id)][file_element_field_names.index(
|
||
|
element_field_name)]
|
||
|
if not field_exists:
|
||
|
continue
|
||
|
# process each included timestep
|
||
|
model_values = [None] * len(self.timesteps)
|
||
|
for timestep_index in timestep_indices:
|
||
|
file_values = list(
|
||
|
exodus_file.get_element_variable_values(
|
||
|
element_block_id, element_field_name,
|
||
|
timestep_index[0]))
|
||
|
model_values[timestep_index[1]] = file_values
|
||
|
# add default field value to excluded timestep
|
||
|
for timestep_index in excluded_timestep_indices:
|
||
|
default_value = self._get_default_field_value(
|
||
|
element_field_name)
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
field = [default_value] * element_count
|
||
|
model_values[timestep_index] = field
|
||
|
# assign all values
|
||
|
fields = self._get_element_block_fields(element_block_id)
|
||
|
fields[element_field_name] = model_values
|
||
|
# get global variables
|
||
|
for global_variable_name in global_variable_names:
|
||
|
# create the variable
|
||
|
if not self.global_variable_exists(global_variable_name):
|
||
|
self.create_global_variable(global_variable_name)
|
||
|
else:
|
||
|
if duplicate_timesteps:
|
||
|
self._exists_warning(global_variable_name,
|
||
|
'global variable')
|
||
|
# get values
|
||
|
model_values = self.global_variables[global_variable_name]
|
||
|
for timestep_index in timestep_indices:
|
||
|
file_value = exodus_file.get_global_variable_value(
|
||
|
global_variable_name, timestep_index[0])
|
||
|
model_values[timestep_index[1]] = file_value
|
||
|
# add info records
|
||
|
if (exodus_file.num_info_records() > 0):
|
||
|
self.info_records += exodus_file.get_info_records()
|
||
|
# add qa records
|
||
|
if (exodus_file.num_qa_records() > 0):
|
||
|
self.qa_records += exodus_file.get_qa_records()
|
||
|
# add title if one does not already exist
|
||
|
# else add it to an info record
|
||
|
if not self.title:
|
||
|
self.title = exodus_file.title()
|
||
|
else:
|
||
|
self.info_records.append('Discarded title from the '
|
||
|
'following file:')
|
||
|
# split filename string if filename is larger than 79 characters
|
||
|
filename_wrap_list = textwrap.fill(filename, width=79).split('\n')
|
||
|
# append interpreter continuation char "\\" to end of continuation line while splitting
|
||
|
for i in range(len(filename_wrap_list) - 1):
|
||
|
filename_wrap_list[i] += "\\"
|
||
|
# append multiple split list to records
|
||
|
self.info_records.extend(filename_wrap_list)
|
||
|
self.info_records.append(exodus_file.title())
|
||
|
# run a check on the model to ensure arrays are correct sizes
|
||
|
self._verify()
|
||
|
# close the file
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
save_stdout = sys.stdout
|
||
|
sys.stdout = DummyFile()
|
||
|
exodus_file.close()
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
sys.stdout = save_stdout
|
||
|
|
||
|
def export_model(self,
|
||
|
filename='output_exomerge.e',
|
||
|
element_block_ids='all',
|
||
|
timesteps='all',
|
||
|
side_set_ids='all',
|
||
|
node_set_ids='all',
|
||
|
global_variable_names='auto',
|
||
|
node_field_names='auto',
|
||
|
element_field_names='auto',
|
||
|
side_set_field_names='auto',
|
||
|
node_set_field_names='auto'):
|
||
|
"""
|
||
|
Export the current model to an ExodusII file.
|
||
|
|
||
|
Examples:
|
||
|
>>> model.export_model('output.g')
|
||
|
|
||
|
"""
|
||
|
# verify information is valid
|
||
|
self._verify()
|
||
|
# format subset of data to export
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids)
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
node_set_ids = self._format_node_set_id_list(node_set_ids)
|
||
|
timesteps = self._format_id_list(timesteps, self.get_timesteps(),
|
||
|
'timestep')
|
||
|
# If no timesteps are exported, or if no nodes are defined, then no
|
||
|
# fields can be exported.
|
||
|
if element_field_names == 'auto':
|
||
|
if timesteps:
|
||
|
element_field_names = self.get_element_field_names(
|
||
|
element_block_ids)
|
||
|
else:
|
||
|
element_field_names = 'none'
|
||
|
if global_variable_names == 'auto':
|
||
|
if timesteps:
|
||
|
global_variable_names = 'all'
|
||
|
else:
|
||
|
global_variable_names = 'none'
|
||
|
if node_field_names == 'auto':
|
||
|
if timesteps and self.nodes:
|
||
|
node_field_names = 'all'
|
||
|
else:
|
||
|
node_field_names = 'none'
|
||
|
if node_set_field_names == 'auto':
|
||
|
if timesteps and self.nodes:
|
||
|
node_set_field_names = self.get_node_set_field_names(
|
||
|
node_set_ids)
|
||
|
else:
|
||
|
node_set_field_names = 'none'
|
||
|
if side_set_field_names == 'auto':
|
||
|
if timesteps:
|
||
|
side_set_field_names = self.get_side_set_field_names(
|
||
|
side_set_ids)
|
||
|
else:
|
||
|
side_set_field_names = 'none'
|
||
|
# format each list
|
||
|
global_variable_names = self._format_id_list(
|
||
|
global_variable_names, self.get_global_variable_names(),
|
||
|
'global variable')
|
||
|
element_field_names = self._format_id_list(
|
||
|
element_field_names, self.get_element_field_names(),
|
||
|
'element field')
|
||
|
node_field_names = self._format_id_list(node_field_names,
|
||
|
self.get_node_field_names(),
|
||
|
'node field')
|
||
|
node_set_field_names = self._format_id_list(
|
||
|
node_set_field_names, self.get_node_set_field_names(),
|
||
|
'node set field')
|
||
|
side_set_field_names = self._format_id_list(
|
||
|
side_set_field_names, self.get_side_set_field_names(),
|
||
|
'side set field')
|
||
|
# delete the file if it exists
|
||
|
if os.path.isfile(filename):
|
||
|
os.remove(filename)
|
||
|
# create file
|
||
|
if self.title == '':
|
||
|
self.title = '<no title supplied>'
|
||
|
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
save_stdout = sys.stdout
|
||
|
sys.stdout = DummyFile()
|
||
|
new_file = exodus.exodus(filename, 'w', 'ctype', self.title, 3,
|
||
|
len(self.nodes),
|
||
|
self.get_element_count(element_block_ids),
|
||
|
len(element_block_ids), len(node_set_ids),
|
||
|
len(side_set_ids))
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
sys.stdout = save_stdout
|
||
|
# put timesteps into chronological order
|
||
|
timestep_indices = [(self.timesteps.index(x), index + 1)
|
||
|
for index, x in enumerate(timesteps)]
|
||
|
# write times
|
||
|
for index, timestep in enumerate(timesteps):
|
||
|
new_file.put_time(index + 1, timestep)
|
||
|
# write global variables
|
||
|
new_file.set_global_variable_number(len(global_variable_names))
|
||
|
for index, name in enumerate(global_variable_names):
|
||
|
new_file.put_global_variable_name(name, index + 1)
|
||
|
values = self.global_variables[name]
|
||
|
for timestep_index in timestep_indices:
|
||
|
new_file.put_global_variable_value(name, timestep_index[1],
|
||
|
values[timestep_index[0]])
|
||
|
# write nodes
|
||
|
new_file.put_coords([x[0] for x in self.nodes],
|
||
|
[x[1] for x in self.nodes],
|
||
|
[x[2] for x in self.nodes])
|
||
|
# write node fields
|
||
|
new_file.set_node_variable_number(len(node_field_names))
|
||
|
for index, name in enumerate(node_field_names):
|
||
|
new_file.put_node_variable_name(name, index + 1)
|
||
|
values = self.node_fields[name]
|
||
|
for timestep_index in timestep_indices:
|
||
|
new_file.put_node_variable_values(name, timestep_index[1],
|
||
|
values[timestep_index[0]])
|
||
|
# write element blocks
|
||
|
for id_ in element_block_ids:
|
||
|
name, info, connectivity, fields = self.element_blocks[id_]
|
||
|
new_file.put_elem_blk_info(id_, *info)
|
||
|
# connectivity in file must be 1-based
|
||
|
temp_connectivity = [x + 1 for x in connectivity]
|
||
|
new_file.put_elem_connectivity(id_, temp_connectivity)
|
||
|
if name:
|
||
|
new_file.put_elem_blk_name(id_, name)
|
||
|
# write element fields
|
||
|
new_file.set_element_variable_number(len(element_field_names))
|
||
|
for index, name in enumerate(element_field_names):
|
||
|
new_file.put_element_variable_name(name, index + 1)
|
||
|
if element_field_names:
|
||
|
truth_table = self._create_element_field_truth_table(
|
||
|
element_block_ids, element_field_names)
|
||
|
new_file.set_element_variable_truth_table(truth_table)
|
||
|
for block_id in element_block_ids:
|
||
|
fields = self._get_element_block_fields(block_id)
|
||
|
for name in element_field_names:
|
||
|
if name not in fields:
|
||
|
continue
|
||
|
field = fields[name]
|
||
|
for timestep_index in timestep_indices:
|
||
|
new_file.put_element_variable_values(
|
||
|
block_id, name, timestep_index[1],
|
||
|
field[timestep_index[0]])
|
||
|
# get first element in each block
|
||
|
element_count = [
|
||
|
self.get_element_count(id_) for id_ in element_block_ids
|
||
|
]
|
||
|
first_element = [
|
||
|
sum(element_count[:i]) + 1 for i in range(len(element_count))
|
||
|
]
|
||
|
first_element = dict((element_block_ids[i], first_element[i])
|
||
|
for i in range(len(element_block_ids)))
|
||
|
# write side sets
|
||
|
for id_ in side_set_ids:
|
||
|
name = self.get_side_set_name(id_)
|
||
|
members = self.get_side_set_members(id_)
|
||
|
new_file.put_side_set_params(id_, len(members), 0)
|
||
|
if members:
|
||
|
elements = [
|
||
|
first_element[block_id] + index
|
||
|
for block_id, index, _ in members
|
||
|
]
|
||
|
sides = [x[2] + 1 for x in members]
|
||
|
new_file.put_side_set(id_, elements, sides)
|
||
|
if name:
|
||
|
new_file.put_side_set_name(id_, name)
|
||
|
# write side set fields
|
||
|
new_file.set_side_set_variable_number(len(side_set_field_names))
|
||
|
for index, name in enumerate(side_set_field_names):
|
||
|
new_file.put_side_set_variable_name(name, index + 1)
|
||
|
if side_set_field_names:
|
||
|
truth_table = self._create_side_set_field_truth_table(
|
||
|
side_set_ids, side_set_field_names)
|
||
|
new_file.set_side_set_variable_truth_table(truth_table)
|
||
|
for side_set_id in side_set_ids:
|
||
|
members = self.get_side_set_members(side_set_id)
|
||
|
fields = self._get_side_set_fields(side_set_id)
|
||
|
if not members:
|
||
|
continue
|
||
|
for name in side_set_field_names:
|
||
|
if name not in fields:
|
||
|
continue
|
||
|
field = fields[name]
|
||
|
for timestep_index in timestep_indices:
|
||
|
new_file.put_side_set_variable_values(
|
||
|
side_set_id, name, timestep_index[1],
|
||
|
field[timestep_index[0]])
|
||
|
# write node sets
|
||
|
for id_ in node_set_ids:
|
||
|
name = self.get_node_set_name(id_)
|
||
|
members = self.get_node_set_members(id_)
|
||
|
new_file.put_node_set_params(id_, len(members))
|
||
|
if members:
|
||
|
# convert to 1-based indexing
|
||
|
temp_members = [x + 1 for x in members]
|
||
|
new_file.put_node_set(id_, temp_members)
|
||
|
if name:
|
||
|
new_file.put_node_set_name(id_, name)
|
||
|
# write node set fields
|
||
|
new_file.set_node_set_variable_number(len(node_set_field_names))
|
||
|
for index, name in enumerate(node_set_field_names):
|
||
|
new_file.put_node_set_variable_name(name, index + 1)
|
||
|
if node_set_field_names:
|
||
|
truth_table = self._create_node_set_field_truth_table(
|
||
|
node_set_ids, node_set_field_names)
|
||
|
new_file.set_node_set_variable_truth_table(truth_table)
|
||
|
for node_set_id in node_set_ids:
|
||
|
members = self.get_node_set_members(node_set_id)
|
||
|
fields = self._get_node_set_fields(node_set_id)
|
||
|
if not members:
|
||
|
continue
|
||
|
for name in node_set_field_names:
|
||
|
if name not in fields:
|
||
|
continue
|
||
|
field = fields[name]
|
||
|
for timestep_index in timestep_indices:
|
||
|
new_file.put_node_set_variable_values(
|
||
|
node_set_id, name, timestep_index[1],
|
||
|
field[timestep_index[0]])
|
||
|
# write info records
|
||
|
new_file.put_info_records(self.info_records)
|
||
|
# write qa records (and append one for this program)
|
||
|
self.qa_records.append(self._get_qa_record())
|
||
|
new_file.put_qa_records(self.qa_records)
|
||
|
del self.qa_records[-1]
|
||
|
# close file
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
save_stdout = sys.stdout
|
||
|
sys.stdout = DummyFile()
|
||
|
new_file.close()
|
||
|
if SUPPRESS_EXODUS_OUTPUT:
|
||
|
sys.stdout = save_stdout
|
||
|
|
||
|
def get_side_set_area(self, side_set_ids):
|
||
|
"""
|
||
|
Return the total area of the given side sets.
|
||
|
|
||
|
Example:
|
||
|
>>> model.get_side_set_area('all')
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
# we do this by first creating one or more new element block out of
|
||
|
# the faces within the side sets and then calculating their "volume."
|
||
|
new_blocks = self._create_element_blocks_from_side_sets(side_set_ids)
|
||
|
total_area = 0.0
|
||
|
for id_ in new_blocks:
|
||
|
total_area += self.get_element_block_volume(id_)
|
||
|
self.delete_element_block(id_, delete_orphaned_nodes=False)
|
||
|
return total_area
|
||
|
|
||
|
def _get_thickness_from_volume_and_area(self, volume, area):
|
||
|
"""Return the thickness of a disk given the volume and area."""
|
||
|
if volume == 0.0:
|
||
|
return 0
|
||
|
|
||
|
# max phi (for a sphere) is 6^(-1/3) * pi^(-1/6)
|
||
|
phi = math.pow(volume, 1 / 3.0) / math.pow(area, 1 / 2.0)
|
||
|
# find a solution, if possible
|
||
|
max_alpha = math.pi**(-1.0 / 6) * 6.0**(-1.0 / 3)
|
||
|
low = 0.0
|
||
|
high = max_alpha
|
||
|
a = 1.0
|
||
|
b = 1.5 - 1 / (16 * math.pi * phi**6)
|
||
|
c = 0.75
|
||
|
d = 0.125
|
||
|
high_value = a * high**3 + b * high**2 + c * high + d
|
||
|
for _ in range(53):
|
||
|
mid = (low + high) / 2
|
||
|
mid_value = a * high**3 + b * high**2 + c * high + d
|
||
|
if (high_value > 0) == (mid_value > 0):
|
||
|
high_value = mid_value
|
||
|
high = mid
|
||
|
else:
|
||
|
low = mid
|
||
|
# in the case this fails, return NaN
|
||
|
if low == 0 or high == max_alpha:
|
||
|
return float('nan')
|
||
|
alpha = (low + high) / 2.0
|
||
|
height = (volume * 4 * alpha**2 / math.pi)**(1.0 / 3)
|
||
|
return height
|
||
|
|
||
|
def _calculate_element_block_thickness(self, element_block_ids):
|
||
|
"""
|
||
|
Return the approximate thickness of the given element blocks.
|
||
|
|
||
|
This approximates the thickness by calculating the volume and exposed
|
||
|
surface area of the element blocks, then returning the thickness of a
|
||
|
short disk with the same characteristics.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
# delete any element blocks which are not of dimension 3
|
||
|
new_ids = []
|
||
|
for id_ in element_block_ids:
|
||
|
if self.get_element_block_dimension(id_) != 3:
|
||
|
self._warning(
|
||
|
'Unexpected element block dimension',
|
||
|
'Element blocks used to determine element edge '
|
||
|
'length are expected to be of dimension 3.')
|
||
|
else:
|
||
|
new_ids.append(id_)
|
||
|
# get the volume
|
||
|
volume = self.get_element_block_volume(element_block_ids)
|
||
|
# find the external faces and put them into a side set
|
||
|
external_faces = self._get_external_element_faces(element_block_ids)
|
||
|
# find all face types
|
||
|
side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(side_set_id, external_faces)
|
||
|
area = self.get_side_set_area(side_set_id)
|
||
|
self.delete_side_set(side_set_id)
|
||
|
return self._get_thickness_from_volume_and_area(volume, area)
|
||
|
|
||
|
def get_element_block_extents(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return the extents of the element blocks as a list.
|
||
|
|
||
|
The results are returned in the following format:
|
||
|
[[min_x, max_x], [min_y, max_y], [min_z, max_z]]
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
# get a set of all nodes within the given element blocks
|
||
|
all_nodes = set()
|
||
|
for id_ in element_block_ids:
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
all_nodes.update(set(connectivity))
|
||
|
# find the extents of that set
|
||
|
extents = [[
|
||
|
min(self.nodes[x][d] for x in all_nodes),
|
||
|
max(self.nodes[x][d] for x in all_nodes)
|
||
|
] for d in range(3)]
|
||
|
return extents
|
||
|
|
||
|
def _create_element_blocks_from_side_sets(self, side_set_ids):
|
||
|
"""
|
||
|
Create new element block from the members of the given side sets.
|
||
|
|
||
|
A list of new element block ids is returned. Typically, only one
|
||
|
ID is in the list, but since multiple face types can be present within
|
||
|
the sideset, and element blocks can only contain a single element type,
|
||
|
it is sometimes necessary to make more than one element block.
|
||
|
|
||
|
"""
|
||
|
side_set_ids = self._format_side_set_id_list(side_set_ids)
|
||
|
# create a dict for new face elements to create
|
||
|
# e.g. new_elements['quad4'] = [[1, 2, 3, 4], [5, 6, 7, 8], ...]
|
||
|
new_elements = {}
|
||
|
for id_ in side_set_ids:
|
||
|
members = self.get_side_set_members(id_)
|
||
|
these_members = self._order_element_faces_by_block(members)
|
||
|
for element_block_id, members in list(these_members.items()):
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(
|
||
|
element_block_id)
|
||
|
face_mapping = self._get_face_mapping_from_id(element_block_id)
|
||
|
for element_index, face_index in members:
|
||
|
face_type = face_mapping[face_index][0]
|
||
|
if face_type not in new_elements:
|
||
|
new_elements[face_type] = []
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:
|
||
|
(element_index + 1) *
|
||
|
nodes_per_element]
|
||
|
new_elements[face_type].extend(
|
||
|
[local_node[x] for x in face_mapping[face_index][1]])
|
||
|
# now that we have the face elements in a local format, we create
|
||
|
# the elements
|
||
|
new_block_ids = []
|
||
|
for element_type, connectivity in list(new_elements.items()):
|
||
|
new_id = self._new_element_block_id()
|
||
|
self._assert(element_type in self.NODES_PER_ELEMENT)
|
||
|
nodes_per_element = self.NODES_PER_ELEMENT[element_type]
|
||
|
info = [
|
||
|
element_type,
|
||
|
len(connectivity) // nodes_per_element, nodes_per_element, 0
|
||
|
]
|
||
|
self.create_element_block(new_id, info, connectivity)
|
||
|
new_block_ids.append(new_id)
|
||
|
return new_block_ids
|
||
|
|
||
|
def _get_element_edge_indices(self, element_type):
|
||
|
"""
|
||
|
Return a list of element edges for the given element type.
|
||
|
|
||
|
The returned list is a list of 2-tuples of local element node indices.
|
||
|
|
||
|
"""
|
||
|
element_type = self._get_standard_element_type(element_type)
|
||
|
# if not a standard type, then we don't know the edge indices
|
||
|
if not self._is_standard_element_type(element_type):
|
||
|
return []
|
||
|
# if dimension is zero, it doesn't have edges
|
||
|
if self._get_dimension(element_type) == 0:
|
||
|
return []
|
||
|
# create a mock element
|
||
|
elements = dict()
|
||
|
elements[element_type] = [
|
||
|
list(range(self.NODES_PER_ELEMENT[element_type]))
|
||
|
]
|
||
|
iterations = self._get_dimension(element_type) - 1
|
||
|
# iterate until dimensions are zero
|
||
|
for _ in range(iterations):
|
||
|
new_elements = dict()
|
||
|
for element_type, connectivities in list(elements.items()):
|
||
|
face_mapping = self._get_face_mapping(element_type)
|
||
|
for face_type, indices in face_mapping:
|
||
|
if face_type not in new_elements:
|
||
|
new_elements[face_type] = []
|
||
|
for local_nodes in connectivities:
|
||
|
new_elements[face_type].append(
|
||
|
[local_nodes[x] for x in indices])
|
||
|
elements = new_elements
|
||
|
# now find the endpoints using the volume formula
|
||
|
edges = []
|
||
|
for element_type, values in list(elements.items()):
|
||
|
assert element_type in self.VOLUME_FORMULA
|
||
|
assert self.DIMENSION[element_type] == 1
|
||
|
formula = self.VOLUME_FORMULA[element_type][-1]
|
||
|
for local_nodes in values:
|
||
|
edges.append([local_nodes[x] for x in formula])
|
||
|
# now get the unique edges
|
||
|
unique_edges = set(tuple(sorted(x)) for x in edges)
|
||
|
return unique_edges
|
||
|
|
||
|
def get_element_edge_length_info(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return the minimum and average element edge lengths.
|
||
|
|
||
|
Only edges within element in the specified element blocks are counted.
|
||
|
All element blocks specified must be 3-dimensional.
|
||
|
|
||
|
The result is returned as [minimum, average].
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
minimum = sys.float_info.max
|
||
|
total = 0.0
|
||
|
edge_count = 0
|
||
|
for element_block_id in element_block_ids:
|
||
|
# get the edge endpoint info
|
||
|
endpoints = self._get_element_edge_indices(
|
||
|
self._get_element_type(element_block_id))
|
||
|
# form all element edges
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
edge_count += element_count * len(endpoints)
|
||
|
for element_index in range(element_count):
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
for edge in endpoints:
|
||
|
this_distance = self._distance_between(
|
||
|
self.nodes[local_node[edge[0]]],
|
||
|
self.nodes[local_node[edge[1]]])
|
||
|
total += this_distance
|
||
|
if this_distance < minimum:
|
||
|
minimum = this_distance
|
||
|
if edge_count == 0:
|
||
|
return [float('nan')] * 2
|
||
|
return [minimum, total / edge_count]
|
||
|
|
||
|
def _get_closest_point_distance_brute(self, points):
|
||
|
"""Return the distance between the two closest points."""
|
||
|
point_count = len(points)
|
||
|
dist = sys.float_info.max
|
||
|
for i in range(1, point_count):
|
||
|
for j in range(i):
|
||
|
dist = min(dist, self._distance_between(points[i], points[j]))
|
||
|
return dist
|
||
|
|
||
|
def _get_closest_point_distance(self, points, sorting_coord=0, trials=0):
|
||
|
"""Return the distance between the two closest points."""
|
||
|
# handle cases of 3 or less points by brute force
|
||
|
point_count = len(points)
|
||
|
if point_count <= 3:
|
||
|
return self._get_closest_point_distance_brute(points)
|
||
|
# find min and max values for the sorting coord
|
||
|
low = min(x[sorting_coord] for x in points)
|
||
|
high = max(x[sorting_coord] for x in points)
|
||
|
# if min and max values are the same, change the coord and try again
|
||
|
mid = (low + high) / 2.0
|
||
|
if low == mid or mid == high:
|
||
|
if trials >= 2:
|
||
|
return self._get_closest_point_distance_brute(points)
|
||
|
return self._get_closest_point_distance(points,
|
||
|
(sorting_coord + 1) % 3,
|
||
|
trials + 1)
|
||
|
# sort into two lists
|
||
|
low_points = [x for x in points if x[sorting_coord] <= mid]
|
||
|
high_points = [x for x in points if x[sorting_coord] > mid]
|
||
|
assert len(low_points) < point_count
|
||
|
assert len(high_points) < point_count
|
||
|
# find closest pair within each set
|
||
|
dist = min(self._get_closest_point_distance(low_points),
|
||
|
self._get_closest_point_distance(high_points))
|
||
|
del low_points
|
||
|
del high_points
|
||
|
# find points sufficiently close to centerline
|
||
|
mid_low = mid - dist
|
||
|
mid_high = mid - dist
|
||
|
mid_points = [
|
||
|
x for x in points
|
||
|
if x[sorting_coord] >= mid_low and x[sorting_coord] <= mid_high
|
||
|
]
|
||
|
if len(mid_points) == point_count:
|
||
|
return self._get_closest_point_distance_brute(points)
|
||
|
dist = min(
|
||
|
dist,
|
||
|
self._get_closest_point_distance(mid_points,
|
||
|
(sorting_coord + 1) % 3))
|
||
|
return dist
|
||
|
|
||
|
def get_closest_node_distance(self):
|
||
|
"""Return the distance between the two closest nodes."""
|
||
|
# create list of all nodes
|
||
|
point_list = list(tuple(x) for x in self.nodes)
|
||
|
return self._get_closest_point_distance(point_list)
|
||
|
|
||
|
def _input_check_error(self, argument, arg_format):
|
||
|
"""
|
||
|
Report an error that the argument is not of the expected format.
|
||
|
|
||
|
This is a helper function which gets called by _input_check in several
|
||
|
places.
|
||
|
|
||
|
"""
|
||
|
argument_string = '%s' % argument
|
||
|
if len(argument_string) > 50:
|
||
|
argument_string = argument_string[:47] + '...'
|
||
|
format_string = '%s' % arg_format
|
||
|
if len(format_string) > 50:
|
||
|
format_string = format_string[:47] + '...'
|
||
|
self._error(
|
||
|
'Unexpected argument type',
|
||
|
'The argument to a function failed an input check. This '
|
||
|
'is most likely due to passing an invalid argument to '
|
||
|
'the function.\n\n'
|
||
|
'Argument: %s\n'
|
||
|
'Expected type: %s' % (argument_string, format_string))
|
||
|
|
||
|
def _input_check(self, argument, arg_format):
|
||
|
"""
|
||
|
Verify an argument is of the expected format.
|
||
|
|
||
|
For example, to check if an argument is a list of 3 integers, set
|
||
|
arg_format=[list, 3, int]
|
||
|
|
||
|
For example, to check if an argument is a list of at least 3 integers,
|
||
|
set arg_format=[list, -3, int]
|
||
|
|
||
|
To check if an argument is a list of 3 lists, each with 2 integers, set
|
||
|
arg_format=[list, 3, list, 2, int].
|
||
|
|
||
|
Example:
|
||
|
>>> model._argument_check([1, 2, 3], [list, 3, int])
|
||
|
|
||
|
"""
|
||
|
self._assert(isinstance(arg_format, list))
|
||
|
if arg_format[0] is list:
|
||
|
self._assert(isinstance(arg_format[1], int))
|
||
|
self._assert(len(arg_format) > 2)
|
||
|
if not isinstance(argument, list):
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
if arg_format[1] > 0 and len(argument) != arg_format[1]:
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
if arg_format[1] <= 0 and len(argument) < -arg_format[1]:
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
for arg in argument:
|
||
|
self._input_check(arg, arg_format[2:])
|
||
|
else:
|
||
|
self._assert(len(arg_format) == 1)
|
||
|
if arg_format[0] is int:
|
||
|
if isinstance(argument, float):
|
||
|
if not argument.is_integer():
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
elif not isinstance(argument, int):
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
elif arg_format[0] is float:
|
||
|
if not isinstance(argument, float) and not isinstance(argument, int):
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
else:
|
||
|
if not isinstance(argument, arg_format[0]):
|
||
|
self._input_check_error(argument, arg_format)
|
||
|
|
||
|
def build_hex8_cube(self,
|
||
|
element_block_id='auto',
|
||
|
extents=1.0,
|
||
|
divisions=3):
|
||
|
"""
|
||
|
Create an element block in the shape of a cuboid.
|
||
|
|
||
|
Extent defines the extent of the block in the form
|
||
|
'[[minx, maxx], [miny, maxy], [minz, maxz]]'. If only one value per
|
||
|
dimension is given, the minimum is assumed to be zero. If a single
|
||
|
value is given, it is assumed a cube with that edge length, with min=0
|
||
|
for all dimensions.
|
||
|
|
||
|
Divisions is the number of divisions per dimension. This can be
|
||
|
defined as a single number for all dimensions or as a list of
|
||
|
3 numbers.
|
||
|
|
||
|
"""
|
||
|
# process shortcuts on arguments
|
||
|
if element_block_id == 'auto':
|
||
|
element_block_id = self._new_element_block_id()
|
||
|
if not isinstance(extents, list):
|
||
|
self._input_check(extents, [float])
|
||
|
extents = [[0.0, float(extents)]] * 3
|
||
|
else:
|
||
|
for i, x in enumerate(extents):
|
||
|
if not isinstance(x, list):
|
||
|
self._input_check(x, [float])
|
||
|
extents[i] = [0.0, float(x)]
|
||
|
if isinstance(divisions, int):
|
||
|
divisions = [divisions] * 3
|
||
|
# verify the input
|
||
|
self._input_check(extents, [list, 3, list, 2, float])
|
||
|
self._input_check(divisions, [list, 3, int])
|
||
|
dimx, dimy, dimz = divisions
|
||
|
[[minx, maxx], [miny, maxy], [minz, maxz]] = extents
|
||
|
# create the nodes
|
||
|
new_nodes = []
|
||
|
node_offset = len(self.nodes)
|
||
|
for k in range(dimz + 1):
|
||
|
z = minz + (maxz - minz) * float(k) / dimz
|
||
|
for j in range(dimy + 1):
|
||
|
y = miny + (maxy - miny) * float(j) / dimy
|
||
|
for i in range(dimx + 1):
|
||
|
x = minx + (maxx - minx) * float(i) / dimx
|
||
|
new_nodes.append([x, y, z])
|
||
|
del i, j, k
|
||
|
del x, y, z
|
||
|
# create the connectivity matrix
|
||
|
connectivity = []
|
||
|
formula = [0, 1, 1 + (dimx + 1), (dimx + 1)]
|
||
|
formula.extend([x + (dimx + 1) * (dimy + 1) for x in formula])
|
||
|
formula = [x + node_offset for x in formula]
|
||
|
for i in range(dimx):
|
||
|
for j in range(dimy):
|
||
|
for k in range(dimz):
|
||
|
first = (k * (dimy + 1) + j) * (dimx + 1) + i
|
||
|
connectivity.extend(first + x for x in formula)
|
||
|
del i, j, k
|
||
|
# now create the actual nodes
|
||
|
self.create_nodes(new_nodes)
|
||
|
# now create the actual block
|
||
|
self.create_element_block(element_block_id,
|
||
|
['hex8', dimx * dimy * dimz, 8, 0],
|
||
|
connectivity=connectivity)
|
||
|
|
||
|
def count_degenerate_elements(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return the number of degenerate elements in the given element blocks.
|
||
|
|
||
|
A degenerate element is an element which contains one or more nodes
|
||
|
which are a duplicate of another node within the same element.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
degenerate_element_count = 0
|
||
|
for element_block_id in element_block_ids:
|
||
|
_, _, connectivity, _ = self.element_blocks[element_block_id]
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
element_count = len(connectivity) // nodes_per_element
|
||
|
for element_index in range(element_count):
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
if len(set(local_node)) != nodes_per_element:
|
||
|
degenerate_element_count += 1
|
||
|
return degenerate_element_count
|
||
|
|
||
|
def count_disconnected_blocks(self, element_block_ids='all'):
|
||
|
"""
|
||
|
Return the number of disconnected blocks.
|
||
|
|
||
|
A disconnected block is a group of elements which are connected to
|
||
|
each other through one or more nodes.
|
||
|
|
||
|
"""
|
||
|
element_block_ids = self._format_element_block_id_list(
|
||
|
element_block_ids, empty_list_okay=False)
|
||
|
nodes = self.get_nodes_in_element_block(element_block_ids)
|
||
|
# for each node, find the lowest index node that it's connected to
|
||
|
master = list(range(len(self.nodes)))
|
||
|
for element_block_id in element_block_ids:
|
||
|
connectivity = self.get_connectivity(element_block_id)
|
||
|
nodes_per_element = self.get_nodes_per_element(element_block_id)
|
||
|
element_count = self.get_element_count(element_block_id)
|
||
|
for i in range(element_count):
|
||
|
local_node = connectivity[i * nodes_per_element:(i + 1) *
|
||
|
nodes_per_element]
|
||
|
# find lowest index master out of these
|
||
|
low = min(local_node)
|
||
|
for x in local_node:
|
||
|
this_low = x
|
||
|
while this_low != master[this_low]:
|
||
|
this_low = master[this_low]
|
||
|
low = min(low, this_low)
|
||
|
# now set the current master to the lowest index found
|
||
|
for x in local_node:
|
||
|
this_low = x
|
||
|
while this_low != master[this_low]:
|
||
|
this_low = master[this_low]
|
||
|
master[this_low] = low
|
||
|
master[this_low] = low
|
||
|
# now make sure master node list is one-deep
|
||
|
for i in nodes:
|
||
|
master[i] = master[master[i]]
|
||
|
# double check that master node list is one-deep
|
||
|
for i in nodes:
|
||
|
assert master[i] == master[master[i]]
|
||
|
# count the number of master nodes
|
||
|
block_count = sum(1 for x in nodes if master[x] == x)
|
||
|
return block_count
|
||
|
|
||
|
def _get_mating_faces(self, side_set_members_one, side_set_members_two):
|
||
|
"""
|
||
|
Return the mating faces between the side sets.
|
||
|
|
||
|
This will only return the faces within 'side_set_members_one'.
|
||
|
|
||
|
"""
|
||
|
# order by element block
|
||
|
members_one_by_block = self._order_element_faces_by_block(
|
||
|
side_set_members_one)
|
||
|
members_two_by_block = self._order_element_faces_by_block(
|
||
|
side_set_members_two)
|
||
|
# find the nodes within set two and create a set of them
|
||
|
faces_to_match = set()
|
||
|
for id_, members in list(members_two_by_block.items()):
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
face_mapping = self._get_face_mapping_from_id(id_)
|
||
|
for element_index, face_index in members:
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
face_nodes = [
|
||
|
local_node[x] for x in face_mapping[face_index][1]
|
||
|
]
|
||
|
faces_to_match.add(tuple(sorted(face_nodes)))
|
||
|
# now look through the original list for duplicates
|
||
|
mating_faces = []
|
||
|
for id_, members in list(members_one_by_block.items()):
|
||
|
nodes_per_element = self.get_nodes_per_element(id_)
|
||
|
connectivity = self.get_connectivity(id_)
|
||
|
face_mapping = self._get_face_mapping_from_id(id_)
|
||
|
for element_index, face_index in members:
|
||
|
local_node = connectivity[element_index *
|
||
|
nodes_per_element:(element_index +
|
||
|
1) *
|
||
|
nodes_per_element]
|
||
|
face_nodes = [
|
||
|
local_node[x] for x in face_mapping[face_index][1]
|
||
|
]
|
||
|
if tuple(sorted(face_nodes)) in faces_to_match:
|
||
|
mating_faces.append((id_, element_index, face_index))
|
||
|
return mating_faces
|
||
|
# now create a sorted list with (nodes, member)
|
||
|
# now find duplicate entries
|
||
|
|
||
|
def _detailed_summary(self):
|
||
|
"""Print a detailed summary of the model."""
|
||
|
# if no timesteps are defined, create one so we can store field values
|
||
|
# it will be deleted at the nd
|
||
|
timestep_created = False
|
||
|
if not self.timesteps:
|
||
|
timestep_created = True
|
||
|
self.create_timestep(0.0)
|
||
|
# print out general info
|
||
|
print('\nMODEL SUMMARY\n')
|
||
|
ids = self.get_element_block_ids()
|
||
|
element_block_count = len(ids)
|
||
|
element_count = self.get_element_count(ids)
|
||
|
print(('- Model contains %d element blocks and %d elements' %
|
||
|
(element_block_count, element_count)))
|
||
|
# if any elements exist...
|
||
|
if element_count:
|
||
|
# calculate element volume field
|
||
|
element_volume_field_name = self._new_element_field_name(1)
|
||
|
self.calculate_element_volumes(element_volume_field_name)
|
||
|
# calculate element centroid field
|
||
|
element_centroid_field_names = self._new_element_field_name(3)
|
||
|
self.calculate_element_centroids(element_centroid_field_names)
|
||
|
# print out element block info
|
||
|
if element_count:
|
||
|
extents = self.get_element_block_extents(ids)
|
||
|
print('- Extents are:')
|
||
|
for d in range(3):
|
||
|
print((' - %s: %g to %g, range of %g' %
|
||
|
('XYZ' [d], extents[d][0], extents[d][1],
|
||
|
extents[d][1] - extents[d][0])))
|
||
|
# print center of mass
|
||
|
cg = self.get_element_block_centroid(ids,
|
||
|
element_volume_field_name,
|
||
|
element_centroid_field_names)
|
||
|
cg = ['%g' % x for x in cg]
|
||
|
print(('- Center of volume is at [%s]' % (', '.join(cg))))
|
||
|
# print total volume
|
||
|
volume = self.get_element_block_volume(ids,
|
||
|
element_volume_field_name)
|
||
|
print(('- Total volume is %g' % volume))
|
||
|
# print total surface area
|
||
|
side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(side_set_id,
|
||
|
self._get_external_element_faces(ids))
|
||
|
area = self.get_side_set_area(side_set_id)
|
||
|
self.delete_side_set(side_set_id)
|
||
|
print(('- Total surface area is %d' % area))
|
||
|
# print number of disconnected blocks
|
||
|
connected_blocks = self.count_disconnected_blocks('all')
|
||
|
print(('- Contains %d disconnected blocks' % (connected_blocks)))
|
||
|
# print element edge length stats
|
||
|
minimum, average = self.get_element_edge_length_info(ids)
|
||
|
print(('- Average element edge length is %g' % (average)))
|
||
|
print(('- Smallest element edge length is %g' % (minimum)))
|
||
|
if self.nodes:
|
||
|
node_distance = self.get_closest_node_distance()
|
||
|
print(('- The closest node pair is %g apart' % (node_distance)))
|
||
|
print()
|
||
|
print('ELEMENT BLOCK INFO')
|
||
|
# find external faces for each element block
|
||
|
if element_count:
|
||
|
external_faces = dict((id_, self._get_external_element_faces(id_))
|
||
|
for id_ in self.get_element_block_ids())
|
||
|
# print info on each element block
|
||
|
for id_ in self.get_element_block_ids():
|
||
|
print()
|
||
|
name = self.get_element_block_name(id_)
|
||
|
print(('Element block ID %d%s:' % (id_, (' "%s"' %
|
||
|
(name)) if name else '')))
|
||
|
dim = self.get_element_block_dimension(id_)
|
||
|
element_count = self.get_element_count(id_)
|
||
|
element_type = self._get_element_type(id_)
|
||
|
dim_name = ' %d-dimensional' % dim if dim != -1 else ''
|
||
|
print(('- Contains %d "%s"%s elements' %
|
||
|
(element_count, element_type, dim_name)))
|
||
|
# if no elements, skip detailed info on this block
|
||
|
if not element_count:
|
||
|
continue
|
||
|
extents = self.get_element_block_extents(id_)
|
||
|
print('- Extents are:')
|
||
|
for d in range(3):
|
||
|
print((' - %s: %g to %g, range of %g' %
|
||
|
('XYZ' [d], extents[d][0], extents[d][1],
|
||
|
extents[d][1] - extents[d][0])))
|
||
|
# print center of mass
|
||
|
cg = self.get_element_block_centroid(id_,
|
||
|
element_volume_field_name,
|
||
|
element_centroid_field_names)
|
||
|
cg = ['%g' % x for x in cg]
|
||
|
print(('- Center of volume is at [%s]' % (', '.join(cg))))
|
||
|
# print total volume
|
||
|
volume = self.get_element_block_volume(id_,
|
||
|
element_volume_field_name)
|
||
|
print(('- Total volume is %g' % (volume)))
|
||
|
# print total surface area
|
||
|
side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(side_set_id, external_faces[id_])
|
||
|
area = self.get_side_set_area(side_set_id)
|
||
|
self.delete_side_set(side_set_id)
|
||
|
print(('- Total surface area is %g' % (area)))
|
||
|
# print number of disconnected blocks
|
||
|
connected_blocks = self.count_disconnected_blocks(id_)
|
||
|
print(('- Contains %d disconnected blocks' % (connected_blocks)))
|
||
|
# print element edge length stats
|
||
|
if dim == 3:
|
||
|
minimum, average = self.get_element_edge_length_info(id_)
|
||
|
print(('- Average element edge length is %g' % (average)))
|
||
|
print(('- Smallest element edge length is %g' % (minimum)))
|
||
|
# print thickness
|
||
|
thickness = self._get_thickness_from_volume_and_area(
|
||
|
volume, area)
|
||
|
print(('- Approximate thickness is %g' % (thickness)))
|
||
|
# print surface connectivity to other blocks
|
||
|
# find faces connected to other blocks
|
||
|
remaining_faces = set(external_faces[id_])
|
||
|
header_output = False
|
||
|
for other_id in self.get_element_block_ids():
|
||
|
if other_id == id_:
|
||
|
continue
|
||
|
mating_faces = self._get_mating_faces(external_faces[id_],
|
||
|
external_faces[other_id])
|
||
|
if mating_faces:
|
||
|
remaining_faces -= set(mating_faces)
|
||
|
side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(side_set_id, list(mating_faces))
|
||
|
area = self.get_side_set_area(side_set_id)
|
||
|
self.delete_side_set(side_set_id)
|
||
|
if not header_output:
|
||
|
print('- Connected to the following element blocks:')
|
||
|
header_output = True
|
||
|
print(
|
||
|
(' - To element block %d though %d faces '
|
||
|
'(area of %g)' % (other_id, len(mating_faces), area)))
|
||
|
if header_output and remaining_faces:
|
||
|
remaining_faces = list(remaining_faces)
|
||
|
side_set_id = self._new_side_set_id()
|
||
|
self.create_side_set(side_set_id, remaining_faces)
|
||
|
area = self.get_side_set_area(side_set_id)
|
||
|
self.delete_side_set(side_set_id)
|
||
|
print((' - To the outside through %d faces '
|
||
|
'(area of %g)' % (len(remaining_faces), area)))
|
||
|
if not header_output:
|
||
|
print('- Not connected to any element blocks')
|
||
|
# print node set info
|
||
|
print('\nNODE SET INFO\n')
|
||
|
ids = self.get_node_set_ids()
|
||
|
print(('There are %d node sets defined.' % (len(ids))))
|
||
|
for id_ in ids:
|
||
|
print()
|
||
|
name = self.get_node_set_name(id_)
|
||
|
print(('Node set ID %d%s:' % (id_,
|
||
|
(' "%s"' % (name)) if name else '')))
|
||
|
print(('- Contains %d members' %
|
||
|
(len(self.get_node_set_members(id_)))))
|
||
|
field_names = self.get_node_set_field_names(id_)
|
||
|
if field_names:
|
||
|
print(('- Has %d fields defined:' % (len(field_names))))
|
||
|
for name in field_names:
|
||
|
print((' - "%s"' % (name)))
|
||
|
# print node set info
|
||
|
print('\nSIDE SET INFO\n')
|
||
|
ids = self.get_side_set_ids()
|
||
|
print(('There are %d side sets defined.' % (len(ids))))
|
||
|
for id_ in ids:
|
||
|
print()
|
||
|
name = self.get_side_set_name(id_)
|
||
|
print(('Side set ID %d%s:' % (id_,
|
||
|
(' "%s"' % (name)) if name else '')))
|
||
|
members = self.get_side_set_members(id_)
|
||
|
member_count = len(members)
|
||
|
print(('- Contains %d members' % (member_count)))
|
||
|
parent_blocks = sorted(set(x[0] for x in members))
|
||
|
parent_string = ', '.join('%s' % (x) for x in parent_blocks)
|
||
|
print(('- Parent element block IDs: %s' % (parent_string)))
|
||
|
face_types = []
|
||
|
members_by_block = self._order_element_faces_by_block(members)
|
||
|
for block_id, these_members in list(members_by_block.items()):
|
||
|
element_type = self._get_element_type(block_id)
|
||
|
if not self._is_standard_element_type(element_type):
|
||
|
face_types.append('unknown')
|
||
|
continue
|
||
|
face_mapping = self._get_face_mapping_from_id(block_id)
|
||
|
face_indices = set(x[1] for x in these_members)
|
||
|
face_types.extend(face_mapping[x][0] for x in face_indices)
|
||
|
face_types = sorted(set(face_types))
|
||
|
print(('- Face types: %s' % (', '.join(face_types))))
|
||
|
area = self.get_side_set_area(id_)
|
||
|
print(('- Total area of %g' % (area)))
|
||
|
field_names = self.get_side_set_field_names(id_)
|
||
|
if field_names:
|
||
|
print(('- Has %d fields defined:' % (len(field_names))))
|
||
|
for name in field_names:
|
||
|
print((' - "%s"' % (name)))
|
||
|
# delete temporary timestep if created
|
||
|
if timestep_created:
|
||
|
self.delete_timestep(0.0)
|