Source code for plugins.ccxinp

#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""Interface with Abaqus/Calculix FE input files (.inp).

Attributes
----------
skip_unknown_eltype: bool
    If True, element blocks with an unrecognized element type are skipped.
    If False (default), an exception will be raised.

"""
import re
import numpy as np
from pyformex import Path
from pyformex import arraytools as at

_re_eltypeB = re.compile(r"^(?P<type>B)(?P<ndim>[23])(?P<degree>\d)?(?P<mod>(OS)?H*)$")
_re_eltype = re.compile(r"^(?P<type>[A-Z]+)(?P<ndim>[23]D)?(?P<nplex>\d+)?(?P<mod>[HIMRSW]*)$")

#
# List of known Abaqus/Calculix element type
#
#

abq_elems = [
    'B21', 'B21H', 'B22', 'B22H', 'B23', 'B23H',
    'B31', 'B31H', 'B32', 'B32H', 'B33', 'B33H',
    'C3D4', 'C3D4H',
    'C3D6', 'C3D6H',
    'C3D8', 'C3D8I', 'C3D8H', 'C3D8R', 'C3D8RH', 'C3D10',
    'C3D10H', 'C3D10M', 'C3D10MH',
    'C3D15', 'C3D15H',
    'C3D20', 'C3D20H', 'C3D20R', 'C3D20RH',
    'CAX6', 'CAX8', 'CAX8R',
    'CONN2D2', 'CONN3D2',
    'CPE3', 'CPE3H',
    'CPE4', 'CPE4H', 'CPE4I', 'CPE4IH', 'CPE4R', 'CPE4RH',
    'CPE6', 'CPE6H', 'CPE6M', 'CPE6MH',
    'CPE8', 'CPE8H', 'CPE8R', 'CPE8RH',
    'CPEG3', 'CPEG3H',
    'CPEG4', 'CPEG4H', 'CPEG4I', 'CPEG4IH', 'CPEG4R', 'CPEG4RH',
    'CPEG6', 'CPEG6H', 'CPEG6M', 'CPEG6MH',
    'CPEG8', 'CPEG8H', 'CPEG8R', 'CPEG8RH',
    'CPS3',
    'CPS4', 'CPS4I', 'CPS4R',
    'CPS6', 'CPS6M',
    'CPS8', 'CPS8R', 'CPS8M',
    'DASHPOTA',
    'FRAME2D', 'FRAME3D',
    'M3D3',
    'M3D4', 'M3D4R',
    'M3D6', 'M3D8',
    'M3D8R',
    'M3D9', 'M3D9R',
    'MASS',
    'S3', 'S3R', 'S3RS',
    'S4', 'S4R', 'S4RS', 'S4RSW', 'S4R5',
    'S8R', 'S8R5',
    'S9R5',
    'SPRINGA',
    'STRI3', 'STRI65',
    'SC8R',
    'SFM3D3',
    'SFM3D4', 'SFM3D4R',
    'SFM3D6',
    'SFM3D8', 'SFM3D8R',
    'T2D2', 'T2D2H', 'T2D3', 'T2D3H',
    'T3D2', 'T3D2H', 'T3D3', 'T3D3H',
    'R2D2', 'RB2D2', 'RB3D2', 'R3D3', 'R3D4',
    'RAX2',
]

ccx_elems = [
    'B31', 'B32', 'B32R',
    'C3D6', 'C3D8', 'C3D8I', 'C3D10', 'C3D15', 'C3D20', 'C3D20R',
    'CAX6', 'CAX8', 'CAX8R',
    'CPE4', 'CPE8', 'CPE8R', 'CPS4', 'CPS8', 'CPS8R',
    'D',
    'DASHPOTA',
    'DCOUP3D',
    'F3D8', 'F3D8R',
    'GAPUNI',
    'M3D3', 'M3D4', 'M3D8',
    'MASS',
    'S4', 'S6', 'S8', 'S8R',
    'SPRING1', 'SPRING2', 'SPRINGA',
    'T2D2',
    'T3D2',
    'T3D3',
    'U1',
]

# Element types with a single fixed plexitude
# Fill these in for unknown eltypes to make them recognized
fixed_nplex = {
    'MASS': 1,
}

# Default pyFormex element types
# The base key is nplex. the secondary key is the ndim
pyf_eltypes = {
    1: 'point',
    2: 'line2',
    3: 'tri3',
    4: {2: 'quad4', 3: 'tet4'},
    6: {2: '', 3: 'wedge6'},
    8: {2: 'quad8', 3: 'hex8'},
    9: 'quad9',
    10: 'tet10',
    15: '',
    20: 'hex20',
}


[docs]def abq_eltype(eltype): """Analyze an Abaqus element type and return eltype characteristics. Returns a dictionary with: - type: the element base type - ndim: the dimensionality of the model space - nplex: the plexitude (number of nodes) - mod: a modifier string - pyf: the corresponding pyFormex element type (this can be a dict with nplex as key) Currently, all these fields are returned as strings. We should probably change ndim and nplex to an int. """ if eltype.startswith('B'): m = _re_eltypeB.fullmatch(eltype) else: m = _re_eltype.fullmatch(eltype) if m: d = m.groupdict() if d['type'] == 'B': degree = int(d['degree']) nplex = 3 if degree == 2 else 2 elif d['type'] == 'FRAME': nplex = 2 elif d['type'] in ['SPRINGA', 'DASHPOTA']: nplex = (1, 2) d['pyf'] = {1: 'point', 2: 'line2'} else: if d['nplex'] is not None: nplex = int(d['nplex']) else: nplex = fixed_nplex.get(d['type'], None) if nplex is None: return {} if d['type'] in ('B', 'T'): d['pyf'] = f'line{nplex}' d['nplex'] = nplex if 'ndim' not in d or d['ndim'] is None: if d['type'][:2] in ['CP', 'CA', 'S']: d['ndim'] = '2' if d['type'] in ['R'] and d['nplex']==4: d['ndim'] = '2' try: ndim = int(d['ndim'][0]) except Exception: ndim = 3 d['ndim'] = ndim if 'mod' not in d or d['mod'] is None: d['mod'] = '' d['avail'] = 'A' # Available in Abaqus if 'pyf' not in d: eltype = pyf_eltypes.get(d['nplex'], '') if isinstance(eltype, dict): eltype = eltype[d['ndim']] d['pyf'] = eltype else: d = {} return d
known_eltypes = { 1: {'point': ['SPRINGA', 'DASHPOTA', 'MASS']}, 2: {'line2': ['SPRINGA', 'DASHPOTA', 'CONN', 'FRAME', 'T', 'B', 'RB', 'RAX', ]}, 3: {'line3': ['B', ], 'tri3': ['M', 'CPS', 'CPE', 'CPEG', 'S', 'SFM', 'R', ]}, 4: {'quad4': ['M', 'CPS', 'CPE', 'CPEG', 'S', 'SFM', 'R', ], 'tet4': ['C', ]}, 6: {'': ['M', 'CPS', 'CPE', 'CPEG', 'SFM', ], 'wedge6': ['C', ]}, 8: {'quad8': ['M', 'CPS', 'CPE', 'CPEG', 'CAX', 'S', 'SFM', ], 'hex8': ['C', ]}, 9: {'quad9': ['M', 'S']}, 10: {'tet10': ['C', ]}, 15: {'': ['C', ]}, 20: {'hex20': ['C', ]}, } def print_catalog(short=False): for el in abq_elems: d = abq_eltype(el) if d: if short: print(f"Eltype {el} = {d['pyf']}") else: print(f"Eltype {el} = {d}") else: print(f"No match: {el}") # # TODO: S?R5 elements are scanned wrongly # class InpModel(): pass model = None system = None skip_unknown_eltype = False log = None part = None datadir = None
[docs]def startPart(name): """Start a new part.""" global part print(f"Start part {name}") model.parts.append({'name': name}) part = model.parts[-1]
[docs]def readCommand(line): """Read a command line, return the command and a dict with options""" if line[0] == '*': line = line[1:] s = line.split(',') s = [si.strip() for si in s] cmd = s[0] opts = {} for si in s[1:]: kv = si.split('=') k = kv[0] if len(kv) > 1: v = kv[1] else: v = True opts[k] = v return cmd, opts
def _do_HEADING(opts, data): """Read the nodal data""" model.heading = '\n'.join(data) def _do_PART(opts, data): """Set the part name""" print(opts) startPart(opts['NAME']) def _do_SYSTEM(opts, data): """Read the system data""" global system if len(data) == 0: system = None return s = data[0].split(',') A = [float(v) for v in s[:3]] try: B = [float(v) for v in s[3:]] except Exception: B, C = None, None if len(data) > 1: C = [float(v) for v in data[1].split('')] else: B[2] = 0. C = [-B[1], B[0], 0.] t = np.array(A) if B is None: r = None else: r = at.rotmat(np.array([A, B, C])) system = (t, r) def _do_NODE(opts, data): """Read the nodal data""" print(f"NODES: {len(data)}") nnodes = len(data) print(f"Read {nnodes} nodes") ndata = len(data[0].split()) if isinstance(datadir, Path): filename = datadir / f"{part['name']}-NODE.data" print(f"Write nodes to {filename}") with open(filename, 'w') as f: f.write(',\n'.join(data)) data = ','.join(data) x = np.fromstring(data, dtype=np.float32, count=ndata*nnodes, sep=',').reshape(-1, ndata) nodid = x[:, 0].astype(np.int32) coords = x[:, 1:] if system: t, r = system if r is not None: coords = np.dot(coords, r) coords += t if 'coords' in part: part['nodid'] = np.concatenate([part['nodid'], nodid]) part['coords'] = np.concatenate([part['coords'], coords], axis=0) else: part['nodid'] = nodid part['coords'] = coords print(f"#NODES in part: {len(part['coords'])}") def _do_ELEMENT(opts, data): """Read element data""" if 'elems' not in part: part['elems'] = [] if 'elid' not in part: part['elid'] = [] sim_type = opts['TYPE'] print(f"ELEMENT: {sim_type} {len(data)}") if sim_type.startswith('B3'): nextranodes = 1 else: nextranodes = 0 d = abq_eltype(sim_type) eltype = d.get('pyf', None) if not eltype: msg = f"Element type '{sim_type}' can not yet be imported" if skip_unknown_eltype: print(msg) log.write(msg) return else: raise ValueError(msg) if isinstance(eltype, dict): plex = np.asarray([d.count(',') for d in data]) plexes = np.unique(plex) if len(plexes) > 1: # We have to split the data print(data) print(plex) raise ValueError( 'Mixed plexitude SPRINGA/DASHPOTA elements are not yet implemented') else: nplex = plexes[0] split_data = {nplex: (eltype[nplex], data)} else: nplex = d['nplex'] split_data = {nplex: (eltype, data)} for nplex in split_data: eltype, data = split_data[nplex] nelems = len(data) print(f"Read {nelems} elements of type {sim_type} " f"-> {eltype}, plexitude {nplex}") ndata = 1 + nplex + nextranodes # elem number, nodes, extranodes ncomma = ndata-1 if nextranodes > 0: # add extranodes for elements missing them missing = ncomma - np.asarray([d.count(',') for d in data]) if (missing > 0).any(): data = [d if c <= 0 else d + ', -1'*c for c, d in zip(missing, data)] print("Added missing extra nodes") if isinstance(datadir, Path): elemfile = datadir / f"{part['name']}-ELEMENT.data" with open(elemfile, 'w') as f: f.write(',\n'.join(data)) e = np.fromstring(','.join(data), dtype=np.int32, count=ndata*nelems, sep=',').reshape(-1, ndata) elid = e[:, 0] elems = e[:, 1:] if nextranodes <= 0 else e[:, 1:-nextranodes] print(f"#ELEMS: {elems.shape} of type {eltype} " f"using nodes {elems.min()}..{elems.max()}") part['elems'].append((eltype, elems)) part['elid'].append(elid) print(f"Element blocks: {len(part['elems'])}") def endCommand(cmd, opts, data): global log func = f"_do_{cmd}" if func in globals(): globals()[func](opts, data) else: log.write(f"Don't know how to handle keyword '{cmd}'\n") # TODO: Add a abaqus/calculix flavour argument
[docs]def readInpFile(fn, tempdir=None): """Read an input file (.inp) Tries to read a file in Abaqus INP format and returns the recognized meshes. Parameters ---------- fn: :term:`path_like` The filename of the input path. tempdir: :term:`path_like`, optional The pathname to a directory where intermediary data can be stored. If not provided, no intermediary data are stored. Warning ------- Element blocks with an unrecognized element type will raise an exception, unless :attr:`~plugins.ccxinp.skip_unknown_eltype` is set to True. Returns ------- InpModel A data class with the following attributes: - `heading`: the heading read from the .inp file - `parts`: a list with parts. See Notes Notes ----- Each part is a dict and can contain the following keys: - `name`: string: the part name - `coords`: float (nnod,3) array: the nodal coordinates - `nodid`: int (nnod,) array: node numbers; default is np.arange(nnod) - `elems`: int (nelems,nplex) array: element connectivity - `elid`: int (nelems,) array: element numbers; default is np.arange(nelems) See Also -------- :func:`fileread.readINP`: read an INP file and return FEModel objects. """ global line, part, log, model, datadir fn = Path(fn) logname = Path('.') / fn.stem + '_ccxinp.log' model = InpModel() model.parts = [] startPart('DEFAULT') cmd, opts = '', None if tempdir is None: datadir = None else: datadir = Path(tempdir) datadir.mkdir(exist_ok=True) with open(logname, 'w') as log: with open(fn) as fil: data_cont = False data = [] for line in fil: if len(line) == 0: break if line.startswith('**'): # skip comments continue line = line.upper() if line.startswith('*'): if cmd: endCommand(cmd, opts, data) cmd = '' if line[1] != '*': data = [] cmd, opts = readCommand(line[1:]) log.write(f"Keyword {cmd}; Options {opts}\n") data_cont = False else: line = line.strip() if data_cont: data[-1] += line else: data.append(line) data_cont = line.endswith(',') print(f"Number of parts in model: {len(model.parts)}") return model
if __name__ in ('__draw__', '__script__'): def print_match(): for eltype in abq_elems: if eltype.startswith('B'): m = _re_eltypeB.fullmatch(eltype) else: m = _re_eltype.fullmatch(eltype) if m: print(f"{eltype}: {m.groupdict()}") else: print(f"{eltype} NOT RECOGNIZED") print_match() print_catalog() # End