#
##
## SPDX-FileCopyrightText: © 2007-2022 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.1 (Sat May 21 14:49:50 CEST 2022)
## 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).
"""
import re
import numpy as np
from pyformex import Path
from pyformex import utils
_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>.*?)(?P<ndim>[23]D)?(?P<nplex>\d+)?(?P<mod>[HIMRSW]*)$")
#
# List of known Abaqus/Calculix element type
#
#
abq_elems = [
'SPRINGA', 'DASHPOTA',
'CONN3D2', 'CONN2D2',
'FRAME3D', 'FRAME2D',
'T2D2', 'T2D2H', 'T2D3', 'T2D3H',
'T3D2', 'T3D2H', 'T3D3', 'T3D3H',
'B21', 'B21H', 'B22', 'B22H', 'B23', 'B23H',
'B31', 'B31H', 'B32', 'B32H', 'B33', 'B33H',
'M3D3',
'M3D4', 'M3D4R',
'M3D6', 'M3D8',
'M3D8R',
'M3D9', 'M3D9R',
'CPS3',
'CPS4', 'CPS4I', 'CPS4R',
'CPS6', 'CPS6M',
'CPS8', 'CPS8R', 'CPS8M',
'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',
'CAX6', 'CAX8', 'CAX8R',
'S3', 'S3R', 'S3RS',
'S4', 'S4R', 'S4RS', 'S4RSW', 'S4R5',
'S8R', 'S8R5',
'S9R5',
'STRI3',
'STRI65',
'SC8R',
'SFM3D3',
'SFM3D4', 'SFM3D4R',
'SFM3D6',
'SFM3D8', 'SFM3D8R',
'C3D4', 'C3D4H',
'C3D6', 'C3D6H',
'C3D8', 'C3D8I', 'C3D8H', 'C3D8R', 'C3D8RH', 'C3D10',
'C3D10H', 'C3D10M', 'C3D10MH',
'C3D15', 'C3D15H',
'C3D20', 'C3D20H', 'C3D20R', 'C3D20RH',
'R2D2', 'RB2D2', 'RB3D2', 'RAX2', 'R3D3', 'R3D4',
]
# 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.match(eltype)
else:
m = _re_eltype.match(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:
nplex = int(d['nplex'])
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']},
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("No match: %s" % el)
#print_catalog()
#
# TODO: S... and RAX elements are still scanned wrongly
#
class InpModel(object):
pass
model = None
system = None
skip_unknown_eltype = True
log = None
part = None
datadir = None
[docs]def startPart(name):
"""Start a new part."""
global part
print("Start part %s" % 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 = rotmat(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("Read %s nodes" % nnodes)
ndata = len(data[0].split())
if isinstance(datadir, Path):
filename = datadir / '%s-NODE.data'%part['name']
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 = 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['pyf']
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 / '%s-ELEMENT.data'%part['name']
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 = '_do_%s' % cmd
if func in globals():
globals()[func](opts, data)
else:
#print("Data %s" % data)
log.write("Don't know how to handle keyword '%s'\n" % cmd)
[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.
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)
"""
global line, part, log, model, datadir
fn = Path(fn)
logname = Path('.') / fn.stem + '_ccxinp.log'
model = InpModel()
model.parts = []
startPart('DEFAULT')
cmd = ''
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("Keyword %s; Options %s\n" % (cmd, opts))
data_cont = False
else:
line = line.strip()
if data_cont:
data[-1] += line
else:
data.append(line)
data_cont = line.endswith(',')
print("Number of parts in model: %s" % len(model.parts))
return model
# End