# coding: utf-8
"""
This file defines:
- model, nid_offset, eid_offset = bdf_mirror(bdf_filename, plane='xz')
- model, nid_offset, eid_offset = write_bdf_symmetric(
bdf_filename, out_filename=None, encoding=None,
size=8, is_double=False,
enddata=None, close=True, plane='xz')
- model = make_symmetric_model(
bdf_filename, plane='xz', zero_tol=1e-12,
log=None, debug=True)
"""
from __future__ import (nested_scopes, generators, division, absolute_import,
print_function, unicode_literals)
from warnings import warn
import numpy as np
from pyNastran.bdf.cards.coordinate_systems import CORD1R, CORD1C, CORD1S, CORD2R, CORD2C, CORD2S
from pyNastran.bdf.cards.loads.static_loads import (
FORCE, FORCE1, FORCE2, MOMENT, MOMENT1, MOMENT2,
PLOAD, PLOAD2, PLOAD4)
from pyNastran.bdf.cards.thermal.loads import TEMP, QVOL, QBDY1, QBDY2, QBDY3, QHBDY
from pyNastran.bdf.cards.aero.aero import CAERO1, SPLINE1
from pyNastran.bdf.cards.bdf_sets import SET1 #, SET3
from pyNastran.bdf.bdf import BDF, read_bdf
[docs]def get_model(bdf_filename, log=None, debug=True):
"""helper method"""
if isinstance(bdf_filename, BDF):
model = bdf_filename
else:
# str, StringIO
model = read_bdf(bdf_filename, validate=True, xref=True,
punch=False, skip_cards=None,
read_cards=None,
encoding=None, log=log,
debug=debug, mode='msc')
return model
[docs]def bdf_mirror_plane(bdf_filename, plane, mirror_model=None,
log=None, debug=True, use_nid_offset=True):
"""mirrors a model about an arbitrary plane"""
model = get_model(bdf_filename, log=log, debug=debug)
if mirror_model is None:
mirror_model = BDF(debug=debug, log=log, mode='msc')
nid_offset, plane = _mirror_nodes_plane(model, mirror_model, plane,
use_nid_offset=use_nid_offset)
eid_offset = _mirror_elements(model, mirror_model, nid_offset)
#_mirror_loads(model, nid_offset, eid_offset)
return model, mirror_model, nid_offset, eid_offset
[docs]def bdf_mirror(bdf_filename, plane='xz', log=None, debug=True):
"""
Mirrors the model about the symmetry plane
Parameters
----------
bdf_filename : str / BDF()
str : the bdf filename
BDF : the BDF model object
plane : str; {'xy', 'yz', 'xz'}; default='xz'
the plane to mirror about
xz : +y/-y
yz : +x/-x
xy : +z/-z
Returns
-------
model : BDF()
BDF : the BDF model object
nid_offset : int
the offset node id
eid_offset : int
the offset element id
"""
model = get_model(bdf_filename, log=log, debug=debug)
mirror_model = model
nid_offset, plane = _mirror_nodes(model, plane=plane)
eid_offset = _mirror_elements(model, mirror_model, nid_offset)
_mirror_loads(model, nid_offset, eid_offset)
_mirror_aero(model, nid_offset, plane=plane)
return model, nid_offset, eid_offset
[docs]def write_bdf_symmetric(bdf_filename, out_filename=None, encoding=None,
size=8, is_double=False,
enddata=None, close=True, plane='xz', log=None):
"""
Mirrors the model about the symmetry plane
Parameters
----------
bdf_filename : str / BDF()
str : the bdf filename
BDF : the BDF model object
out_filename : varies; default=None
str - the name to call the output bdf
file - a file object
StringIO() - a StringIO object
None - pops a dialog
encoding : str; default=None -> system specified encoding
the unicode encoding
latin1, and utf8 are generally good options
size : int; {8, 16}
the field size
is_double : bool; default=False
False : small field
True : large field
enddata : bool; default=None
bool - enable/disable writing ENDDATA
None - depends on input BDF
close : bool; default=True
should the output file be closed
plane : str; {'xy', 'yz', 'xz'}; default='xz'
the plane to mirror about
xz : +y/-y
yz : +x/-x
xy : +z/-z
Returns
-------
model : BDF()
BDF : the BDF model object
nid_offset : int
the offset node id
eid_offset : int
the offset element id
Notes
-----
Updates the BDF object to be symmetric
- see bdf_mirror if you don't want to write the model
Doesn't equivalence nodes on the centerline.
Considers
- nodes : GRID
- elements, rigid_elements, mass_elements : see ``_mirror_elements``
- loads : see ``_mirror_loads``
- aero cards : see ``_mirror_aero``
"""
model, nid_offset, eid_offset = bdf_mirror(bdf_filename, plane=plane, log=None)
model.write_bdf(out_filename=out_filename, encoding=encoding,
size=size, is_double=is_double,
interspersed=False, enddata=enddata, close=close)
return model, nid_offset, eid_offset
[docs]def _mirror_nodes(model, plane='xz'):
"""
Mirrors the GRIDs
.. warning:: doesn't consider coordinate systems;
it could, but you'd need 20 new coordinate systems
.. warning:: doesn't mirror SPOINTs, EPOINTs
"""
nid_offset = 0
iy, plane = _plane_to_iy(plane)
if model.nodes:
nid_offset = max(model.node_ids)
for (nid, node) in sorted(model.nodes.items()):
xyz = node.get_position()
nid2 = nid + nid_offset
xyz2 = xyz.copy()
xyz2[iy] *= -1.
model.add_grid(nid2, xyz2, cp=0, cd=node.cd, ps=node.ps, seid=node.seid)
return nid_offset, plane
[docs]def _mirror_nodes_plane(model, mirror_model, plane, use_nid_offset=True):
"""
Mirrors the GRIDs about an arbitrary plane
Parameters
----------
model : BDF
???
mirror_model : BDF
???
plane : str
???
use_nid_offset : bool
???
Returns
-------
nid_offset : int
the node id offset
plane : str
the sorted plane; ZX -> xz
.. warning:: doesn't consider coordinate systems;
it could, but you'd need 20 new coordinate systems
.. warning:: doesn't mirror SPOINTs, EPOINTs
https://mathinsight.org/distance_point_plane
"""
nid_offset = 0
if model.nodes:
all_nodes, xyz_cid0 = model.get_xyz_in_coord_no_xref(
cid=0, fdtype='float64', sort_ids=True)
unused_cid = max(model.coords) + 1
origin = plane[0, :]
# just a triangle's normal vector
n1 = plane[0, :]
n2 = plane[1, :]
n3 = plane[2, :]
normal = np.cross(n2 - n1, n3 - n1)
normal /= np.linalg.norm(normal)
#cord2r = model.add_cord2r(cid, plane[0, :], plane[1, :], plane[2, :])
#del model.coords[cid]
#print(cord2r)
#origin = cord2r.origin
#normal = cord2r.beta()[2, :]
#print('normal =', normal)
vector = xyz_cid0 - origin
assert xyz_cid0.shape == vector.shape, 'xyz_cid0.shape=%s; vector.shape=%s' % (xyz_cid0.shape, vector.shape)
v_dot_n = vector * normal[np.newaxis, :]
assert v_dot_n.shape == vector.shape, 'v_dot_n.shape=%s; vector.shape=%s' % (v_dot_n.shape, vector.shape)
distance = np.linalg.norm(v_dot_n, axis=1)
assert v_dot_n.shape[0] == len(distance), 'v_dot_n.shape=%s; distance.shape=%s' % (v_dot_n.shape, distance.shape)
# we're some distance from the plane, but we don't know the
# direction, so we take the max distance from the plane and
# project it in the +normal direction and then check that
# distance in comparison to the known distance
#
max_distance = distance.max()
imax = np.where(distance == max_distance)[0][0]
distance0 = distance[imax]
xyz0 = xyz_cid0[imax, :] + distance0 * normal
v0_dot_n = xyz0 * normal
distance_plus = np.linalg.norm(v0_dot_n)
if distance_plus > 1.1*distance0:
xyz_cid0_2 = xyz_cid0 - 2 * distance[:, np.newaxis] * normal[np.newaxis, :]
else:
xyz_cid0_2 = xyz_cid0 + 2 * distance[:, np.newaxis] * normal[np.newaxis, :]
if use_nid_offset:
nid_offset = max(all_nodes)
for nid, xyz2 in zip(all_nodes, xyz_cid0_2):
node = model.nodes[nid]
nid2 = nid + nid_offset
mirror_model.add_grid(nid2, xyz2, cp=0, cd=node.cd, ps=node.ps, seid=node.seid)
return nid_offset, plane
[docs]def _plane_to_iy(plane):
"""gets the index fo the mirror plane"""
plane = plane.strip().lower()
plane_sorted = ''.join(sorted(set(plane)))
if plane_sorted == 'yz':
iy = 0
if plane_sorted == 'xz':
iy = 1
elif plane_sorted == 'xy':
iy = 2
else: # pragma: no cover
raise NotImplementedError("plane=%r and must be 'yz', 'xz', or 'xy'." % plane)
return iy, plane_sorted
[docs]def _mirror_elements(model, mirror_model, nid_offset, use_eid_offset=True):
"""
Mirrors the elements
elements:
0d : CELAS1, CELAS2, CELAS3, CELAS4, CDAMP1, CDAMP2, CDAMP3, CDAMP4, CDAMP5
CFAST, CBUSH, CBUSH1D
2d : CTRIA3, CQUAD4, CTRIA6, CQUAD8, CQUAD, CTRIAR, CQUADR
3d : ???
missing : CVISC, CTRIAX, CTRIAX6, CQUADX, CQUADX8, CCONEAX
rigid_elements:
loaded: RBE2, RBE3
missing: RBAR, RBAR1
mass_elements:
loaded: CONM2
missing CONM1, CMASS1, CMASS2, CMASS3, CMASS4
Notes
-----
Doesn't handle CBAR/CBEAM offsets
Doesn't handle CBEAM SPOINTs
"""
eid_max_elements = 0
eid_max_masses = 0
eid_max_rigid = 0
if use_eid_offset:
if model.elements:
eid_max_elements = max(model.elements.keys())
if model.masses:
eid_max_masses = max(model.masses.keys())
if model.rigid_elements:
eid_max_rigid = max(model.rigid_elements.keys())
eid_offset = max(eid_max_elements, eid_max_masses, eid_max_rigid)
if model.elements:
__mirror_elements(model, mirror_model, nid_offset, eid_offset)
if model.masses:
for eid, element in sorted(model.masses.items()):
eid_mirror = eid + eid_offset
#print(element.get_stats())
if element.type == 'CONM2':
old_nid = element.nid
new_nid = old_nid + nid_offset
mass = element.mass
cid = element.cid
X = element.X
I = element.I
mirror_model.add_conm2(eid_mirror, new_nid, mass, cid=cid, X=X, I=I, comment='')
else: # pragma: no cover
#print(element.get_stats())
mirror_model.log.warning('skipping mass element:\n%s' % str(element))
if model.rigid_elements:
__mirror_rigid_elements(model, mirror_model, nid_offset, eid_offset)
return eid_offset
[docs]def __mirror_elements(model, mirror_model,
nid_offset, eid_offset):
"""mirrors model.elements"""
shells = {'CTRIA3', 'CQUAD4', 'CTRIAR', 'CQUADR'}
shell_nones = {'CTRIA6', 'CQUAD8', 'CQUAD', }
rods = {'CROD', 'CONROD', 'CTUBE'}
spring_dampers = {
'CELAS1', 'CELAS2', 'CELAS3', 'CELAS4',
'CDAMP1', 'CDAMP2', 'CDAMP3', 'CDAMP4', 'CDAMP5',
}
def _set_nodes(element, nodes):
try:
element.nodes = nodes
except AttributeError:
print(element.get_stats())
print(nodes)
raise
for eid, element in sorted(model.elements.items()):
etype = element.type
if etype in ['CHBDYG', 'CHBDYE']:
continue
nodes = element.node_ids
#try:
#nodes = [node_id + nid_offset for node_id in nodes]
#except TypeError:
#msg = 'cannot mirror %r eid=%s because None exists in nodes=%s' % (
#element.type, eid, nodes)
#model.log.warning(msg)
#continue
eid_mirror = eid + eid_offset
fields = element.repr_fields()
fields[1] = eid_mirror
mirror_model.add_card(fields, etype)
element2 = mirror_model.elements[eid_mirror]
if etype in shells:
nodes = [node_id + nid_offset for node_id in nodes]
element2.nodes = nodes
element.flip_normal() # nodes = nodes[::-1]
elif etype in shell_nones:
nodes = [node_id + nid_offset if node_id is not None else None
for node_id in nodes]
element2.nodes = nodes
element.flip_normal() # nodes = nodes[::-1]
elif etype in rods:
nodes = [node_id + nid_offset for node_id in nodes]
element2.nodes = nodes
elif etype in ['CBAR', 'CBEAM']:
nodes = [node_id + nid_offset for node_id in nodes]
element2.nodes = nodes
g0 = element2.g0 + nid_offset if element2.g0 is not None else None
element2.g0 = g0
elif etype == 'CGAP':
#nodes = [node_id + nid_offset if node_id is not None else None
#for node_id in nodes]
#_set_nodes(element2, nodes)
ga = element2.ga + nid_offset if element2.ga is not None else None
gb = element2.gb + nid_offset if element2.gb is not None else None
g0 = element2.g0 + nid_offset if element2.g0 is not None else None
element2.ga = ga
element2.gb = gb
element2.g0 = g0
elif etype == 'CBUSH1D':
ga = element2.ga + nid_offset if element2.ga is not None else None
gb = element2.gb + nid_offset if element2.gb is not None else None
element2.ga = ga
element2.gb = gb
elif etype == 'CFAST':
ga = element2.ga + nid_offset if element2.ga is not None else None
gb = element2.gb + nid_offset if element2.gb is not None else None
gs = element2.gs + nid_offset if element2.gs is not None else None
element2.ga = ga
element2.gb = gb
element2.gs = gs
elif etype == 'CCONEAX':
pass
elif etype in [spring_dampers]:
nodes = [node_id + nid_offset if node_id is not None else None for node_id in nodes]
_set_nodes(element2, nodes)
#print(nodes)
#element2.nodes = nodes
elif etype == 'GENEL':
element2.ul = element2.ul + nid_offset
element2.ud = element2.ud + nid_offset
else:
try:
element2.nodes = nodes
except AttributeError: # pragma: no cover
print(element.get_stats())
raise
return
[docs]def __mirror_rigid_elements(model, mirror_model,
nid_offset, eid_offset):
"""mirrors model.rigid_elements"""
for eid, rigid_element in sorted(model.rigid_elements.items()):
eid_mirror = eid + eid_offset
if rigid_element.type == 'RBE2':
Gmi_node_ids = rigid_element.Gmi_node_ids
Gn = rigid_element.Gn()
Gijs = None
ref_grid_id = None
elif rigid_element.type == 'RBE3':
Gmi_node_ids = rigid_element.Gmi_node_ids
Gijs = rigid_element.Gijs
ref_grid_id = rigid_element.ref_grid_id
Gn = None
else:
model.log.warning('_write_elements_symmetric: %s not implemented' % rigid_element.type)
continue
#raise NotImplementedError(msg)
#if rigid_element.type in ['RBE2', 'RBE3']:
Gmi_node_ids_mirror = [node_id + nid_offset for node_id in Gmi_node_ids]
if Gn:
Gn_mirror = Gn + nid_offset
if Gijs:
Gijs_mirror = [[node_id + nid_offset for node_id in nodes] for nodes in Gijs]
if ref_grid_id:
ref_grid_id_mirror = ref_grid_id + nid_offset
if rigid_element.type == 'RBE2':
rigid_element2 = mirror_model.add_rbe2(eid_mirror, Gn_mirror, rigid_element.cm,
Gmi_node_ids_mirror)
elif rigid_element.type == 'RBE3':
rigid_element2 = mirror_model.add_rbe3(
eid_mirror, ref_grid_id_mirror, rigid_element.refc, rigid_element.weights,
rigid_element.comps, Gijs_mirror
)
else: # pragma: no cover
mirror_model.log.warning('skipping:\n%s' % str(rigid_element))
[docs]def _mirror_loads(model, nid_offset=0, eid_offset=0):
"""
Mirrors the loads. A mirrored force acts in the same direction.
Considers:
- PLOAD4
- no coordinate systems (assumes cid=0)
- FORCE, FORCE1, FORCE2, MOMENT, MOMENT1, MOMENT2
- PLOAD, PLOAD2
- TEMP, QVOL, QHBDY, QBDY1, QBDY2, QBDY3
"""
for unused_load_id, loads in model.loads.items():
for load in loads:
loads_new = []
load_type = load.type
if load_type == 'PLOAD4':
g1 = None
g34 = None
if load.g1 is not None:
g1 = load.g1 + nid_offset
if load.g34 is not None:
g34 = load.g34 + nid_offset
eids = [eid + eid_offset for eid in load.eids]
load = PLOAD4(
load.sid, eids, load.pressures, g1, g34,
cid=load.cid, nvector=load.nvector,
surf_or_line=load.surf_or_line,
line_load_dir=load.line_load_dir, comment='')
loads_new.append(load)
elif load_type == 'FORCE':
load = FORCE(load.sid, load.node + nid_offset, load.mag, load.xyz,
cid=load.cid, comment='')
loads_new.append(load)
elif load_type == 'FORCE1':
load = FORCE1(load.sid, load.node + nid_offset, load.mag,
load.g1 + nid_offset, load.g2 + nid_offset, comment='')
loads_new.append(load)
elif load_type == 'FORCE2':
load = FORCE2(load.sid, load.node + nid_offset, load.mag,
load.g1 + nid_offset, load.g2 + nid_offset,
load.g3 + nid_offset, load.g4 + nid_offset, comment='')
loads_new.append(load)
elif load_type == 'MOMENT':
load = MOMENT(load.sid, load.node + nid_offset, load.mag, load.xyz,
cid=load.cid, comment='')
loads_new.append(load)
elif load_type == 'MOMENT1':
load = MOMENT1(load.sid, load.node + nid_offset, load.mag,
load.g1 + nid_offset, load.g2 + nid_offset, comment='')
loads_new.append(load)
elif load_type == 'MOMENT2':
load = MOMENT2(load.sid, load.node + nid_offset, load.mag,
load.g1 + nid_offset, load.g2 + nid_offset,
load.g3 + nid_offset, load.g4 + nid_offset, comment='')
loads_new.append(load)
elif load_type == 'PLOAD':
nodes = [nid + nid_offset for nid in load.nodes]
load = PLOAD(load.sid, load.pressure, nodes, comment='')
loads_new.append(load)
elif load_type == 'PLOAD2':
eids = [eid + eid_offset for eid in load.eids]
load = PLOAD2(load.sid, load.pressure, eids, comment='')
loads_new.append(load)
elif load_type == 'QVOL':
elements = [eid + eid_offset for eid in load.elements]
load = QVOL(load.sid, load.qvol, nid_offset + load.control_point, elements)
loads_new.append(load)
elif load_type == 'QHBDY':
grids = [nid + nid_offset for nid in load.grids]
load = QHBDY(load.sid, load.flag, load.q0, grids, af=load.af)
loads_new.append(load)
elif load_type == 'QBDY1':
eids = [eid + eid_offset for eid in load.eids]
load = QBDY1(load.sid, load.qflux, eids)
loads_new.append(load)
elif load_type == 'QBDY2':
load = QBDY2(load.sid, load.eid + eid_offset, load.qfluxs, comment='')
loads_new.append(load)
elif load_type == 'QBDY3':
eids = [eid + eid_offset for eid in load.eids]
load = QBDY3(load.sid, load.q0, load.cntrlnd + nid_offset, eids)
loads_new.append(load)
elif load_type == 'TEMP':
temperatures = {}
for nid, temp in load.temperatures.items():
temperatures[nid + nid_offset] = temp
load = TEMP(load.sid, temperatures)
loads_new.append(load)
elif load_type == 'GRAV':
pass
else: # pragma: no cover
model.log.warning('skipping:\n%s' % load.rstrip())
if loads_new:
loads += loads_new
[docs]def _mirror_aero(model, nid_offset, plane):
"""
Mirrors the aero cards
Considers:
- AEROS
- doesn't consider sideslip
- CAERO1
- doesn't consider sideslip
- considers Cp
- considers lchord/lspan/nchord/nspan
- SPLINE1
- handle boxes
- SET1
- handle nodes
Doesnt consider:
- AELIST
- AESURF
- AERO
- AEFORCE
- AEPRES
- CAERO2/3/4/5
- PAERO1/2/3/4/5
- AESURFS
"""
is_aero = False
aero_cids_set = set([])
if model.aeros is not None:
is_aero = True
aeros = model.aeros
# The ACSID must be a rectangular coordinate system.
# Flow is in the positive x-direction (T1).
if aeros.acsid > 0:
model.log.error('Sideslip coordinate system (ACSID) mirroring is not supported')
# REFB should be full span, even on half-span models
# REFS should be half area on half-span models
aeros.sref *= 2.
if plane == 'xz':
aeros.sym_xz = 0
elif plane == 'yz':
aeros.sym_yz = 0
else:
model.log.error('not mirroring plane %r; only xz, yz' % plane)
caero_id_offset = 0
if len(model.caeros):
is_aero = True
caero_id_max = max(model.caero_ids)
caero_id_offset = np.max(model.caeros[caero_id_max].box_ids.flat)
caeros = []
for unused_caero_id, caero in model.caeros.items():
if caero.type == 'CAERO1':
# the AEFACTs are assumed to be the same on the left and right side
# I think the spanwise direction will be fine
lchord = caero.lchord
nchord = caero.nchord
lspan = caero.lspan
nspan = caero.nspan
p1, p4 = caero.get_leading_edge_points()
p1 = p1.copy()
p4 = p4.copy()
x12 = caero.x12
x43 = caero.x43
if plane == 'xz': # flip the y
p1[1] *= -1.
p4[1] *= -1.
elif plane == 'xy':
p1[2] *= -1.
p4[2] *= -1.
else: # pragma: no cover
raise NotImplementedError('plane=%r not supported in CAERO1' % plane)
eid2 = caero.eid + caero_id_offset
caero_new = CAERO1(eid2, caero.pid, caero.igroup,
p1, x12, p4, x43,
cp=0,
nspan=nspan, lspan=lspan,
nchord=nchord, lchord=lchord,
comment='')
# we flip the normal so if we ever use W2GJ it's going to be consistent
caero_new.flip_normal()
caeros.append(caero_new)
else: # pragma: no cover
model.log.error('skipping (only supports CAERO1):\n%s' % caero.rstrip())
for caero in caeros:
model._add_caero_object(caero)
nsplines = len(model.splines)
sets_max = max(model.sets) if len(model.sets) else 0
if caero_id_offset == 0 and nsplines:
model.log.error("cant mirror splines because CAEROs don't exist...")
elif nsplines and sets_max == 0:
model.log.error("cant mirror splines because SET1/3 don't exist...")
elif nsplines:
is_aero = True
splines = []
spline_sets_to_duplicate = []
spline_max = max(model.splines)
for unused_spline_id, spline in model.splines.items():
if spline.type == 'SPLINE1':
eid = spline.eid + spline_max
caero = spline.caero + caero_id_offset
method = spline.method
usage = spline.usage
box1 = spline.box1 + caero_id_offset
box2 = spline.box2 + caero_id_offset
setg = spline.setg + sets_max
dz = spline.dz
melements = spline.melements
nelements = spline.nelements
spline_new = SPLINE1(eid, caero, box1, box2, setg, dz=dz,
method=method, usage=usage,
nelements=nelements, melements=melements, comment='')
splines.append(spline_new)
spline_sets_to_duplicate.append(spline.setg)
else: # pragma: no cover
model.log.error('skipping (only support SPLINE1):\n%s' % spline.rstrip())
#print("spline_sets_to_duplicate =", spline_sets_to_duplicate)
msg = ', which is required to mirror:\n%s' % spline.rstrip()
sets_to_add = []
for set_id in spline_sets_to_duplicate:
set_card = model.Set(set_id, msg=msg)
if set_card.type == 'SET1':
sid = set_card.sid + sets_max
ids = [nid + nid_offset for nid in set_card.ids]
is_skin = set_card.is_skin
set_card = SET1(sid, ids, is_skin=is_skin, comment='')
sets_to_add.append(set_card)
else: # pragma: no cover
model.log.error('skipping (only support SET1):\n%s' % set_card.rstrip())
for spline in splines:
model._add_spline_object(spline)
for set_card in sets_to_add:
model._add_set_object(set_card)
aelist_id_offset = 0
cid_offset = max(model.coords) if len(model.coords) > 1 else 0
if is_aero:
_asymmetrically_mirror_aero_coords(model, aero_cids_set, cid_offset, plane)
model.pop_parse_errors()
[docs]def _asymmetrically_mirror_aero_coords(model,
aero_cids_set,
cid_offset, plane='xz'):
"""we'll leave i the same, flip j, and invert k"""
# doesn't handle CORD1x
coord_map = {
'CORD1R': CORD1R,
'CORD1C': CORD1C,
'CORD1S': CORD1S,
'CORD2R': CORD2R,
'CORD2C': CORD2C,
'CORD2S': CORD2S,
}
aero_cids = list(aero_cids_set)
aero_cids.sort()
for cid in aero_cids:
coord = model.Coord(cid)
if cid == 0:
continue
cid_new = cid + cid_offset
if coord.type in {'CORD2R', 'CORD2C', 'CORD2S'}:
i, j, unused_k = coord.beta().copy()
origin = coord.origin.copy()
if plane == 'xz':
origin[1] *= -1
j[1] *= -1
elif plane == 'xy':
origin[2] *= -1
j[2] *= -1
else:
model.log.warning('skipping coord_id=%s' % coord.cid)
return
#k = np.cross(i, j)
coord_obj = coord_map[coord.type] # CORD2R/C/S
coord_new = coord_obj.add_ijk(cid_new, origin=origin, i=i, j=j, k=None,
rid=0, comment='')
else:
model.log.warning('skipping coord_id=%s' % coord.cid)
continue
model.coords[cid_new] = coord_new
return
[docs]def make_symmetric_model(bdf_filename, plane='xz', zero_tol=1e-12, log=None, debug=True):
"""
Makes a symmetric model from a full model
Parameters
----------
bdf_filename : str / BDF()
str : the bdf filename
BDF : the BDF model object
plane : str; {'xy', 'yz', 'xz'}; default='xz'
the plane to mirror about
xz : +y/-y
yz : +x/-x
xy : +z/-z
zaero_tol : float; default=1e-12
the symmetry plane tolerance
Returns
-------
model : BDF()
BDF : the BDF model object
## TODO: doesn't handle elements straddling the centerline
"""
model = get_model(bdf_filename, log=log, debug=debug)
iy, plane = _plane_to_iy(plane)
nids_to_remove = []
eids_to_remove = []
caero_ids_to_remove = []
zero = -zero_tol
for eid, elem in model.elements.items():
xyz = elem.Centroid()
if xyz[iy] < zero:
eids_to_remove.append(eid)
for nid, node in model.nodes.items():
xyz = node.get_position()
if xyz[iy] < zero:
nids_to_remove.append(nid)
for nid in nids_to_remove:
del model.nodes[nid]
for eid in eids_to_remove:
del model.elements[eid]
for caero_id, caero in model.caeros.items():
if caero.type == 'CAERO1':
p1, p2, p3, p4 = caero.get_points()
#print(caero)
if p1[iy] <= zero and p4[iy] <= zero:
#print('p1=%s p4=%s' % (p1, p4))
caero_ids_to_remove.append(caero_id)
elif p1[iy] < zero:
p1[iy] = 0.
caero.set_points([p1, p2, p3, p4])
elif p4[iy] < zero:
p4[iy] = 0.
caero.set_points([p1, p2, p3, p4])
elif caero.type == 'CAERO2':
# TODO: a CAERO2 can't be half symmetric...can it?
# TODO: it can be skewed though...
p1, p2 = caero.get_points()
if p1[iy] <= zero and p2[iy] <= zero:
#print('p1=%s p4=%s' % (p1, p4))
caero_ids_to_remove.append(caero_id)
else: # pragma: no cover
raise NotImplementedError(caero)
for caero_id in caero_ids_to_remove:
del model.caeros[caero_id]
#print('nids_to_remove =', nids_to_remove)
for unused_spline_id, spline in model.splines.items():
caero = spline.caero
#setg = spline.setg
#print('caero = ', caero)
nids = spline.setg_ref.ids # list
#spline.uncross_reference()
#i = 0
nids = list(set(nids) - set(nids_to_remove))
nids.sort()
spline.setg_ref.ids_ref = None
spline.setg_ref.ids = nids
plane_to_labels_keep_map = {
'yz' : ['URDD4', 'URDD2', 'URDD3', 'SIDES', 'YAW'], # yz
'xz' : ['URDD1', 'URDD5', 'URDD3', 'PITCH', 'ANGLEA'], # xz plane
'xy' : ['URDD1', 'URDD2', 'URDD6', 'ROLL'], # xy plane
}
all_labels = {
'URDD4', 'URDD2', 'URDD3', 'SIDES', 'YAW',
'URDD1', 'URDD5', 'URDD3', 'PITCH', 'ANGLEA',
'URDD1', 'URDD2', 'URDD6', 'ROLL',
}
labels_to_keep = plane_to_labels_keep_map[plane]
labels_to_remove = [label for label in all_labels if label not in labels_to_keep]
#print('labels_to_remove =', labels_to_remove)
for aestat_id in list(model.aestats.keys()):
aestat = model.aestats[aestat_id]
if aestat.label in labels_to_remove:
del model.aestats[aestat_id]
for unused_trim_id, trim in model.trims.items():
labels = trim.labels
ilabels_to_remove = [labels.index(label) for label in labels_to_remove
if label in labels]
#print("ilabels_to_remove =", ilabels_to_remove)
trim.uxz = [trim.uxs[ilabel] for ilabel in ilabels_to_remove]
trim.labels = [trim.labels[ilabel] for ilabel in ilabels_to_remove]
return model