#
##
## SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.5 (Thu Feb 8 19:11:13 CET 2024)
## 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/.
##
"""Hesperia
"""
_level = 'advanced'
_topics = ['geometry', 'FEA', 'domes', 'surface']
_techniques = ['menu', 'dialog', 'persistence', 'color']
import sys
import time
from pyformex import simple, utils
from pyformex.connectivity import Connectivity
from pyformex.fe.properties import *
from pyformex.fe.fe_abq import *
from pyformex.gui import menu
def howto():
pf.showInfo("""..
How to use the Hesperia application?
------------------------------------
#. If you want to save your work, start by opening a new project (File menu).
#. Create the geometry: it will be put in a Formex named 'F'.
#. Add (or read) properties to be used for the snow loading: enter a property number, then select the corresponding facets. Save the properties if you want to use them again later.
#. Create a Finite Element model : either shell or frame.
#. Perform the calculation:
For a shell model:
5a. Create an Abaqus input file
5b. Send the job to the cluster (job menu)
5c. Get the results back when finished (job menu)
For a frame model:
5a. Directly calculate using the CALPY module
#. Postprocess:
For an Abaqus job: use the postproc menu
For a Calpy job: use the hesperia menu
""")
def createGeometry():
global F
# Construct a triangle of an icosahedron oriented with a vertex in
# the y-direction, and divide its edges in n parts
n = 3
# Add a few extra rows to close the gap after projection
nplus = n+3
clear()
smoothwire()
view('front')
# Start with an equilateral triangle in the x-y-plane
A = simple.triangle()
A.setProp(1)
draw(A)
# Modular size
a, b, c = A.sizes()
print("Cell width: %s; height: %s" % (a, b))
# Create a mirrored triangle
B = A.reflect(1)
B.setProp(2)
draw(B)
# Replicate nplus times in 2 directions to create triangular pattern
F = A.replic2(1, nplus, a, -b, 0, 1, bias=-a/2, taper=1)
G = B.replic2(1, nplus-1, a, -b, 0, 1, bias=-a/2, taper=1)
clear()
F += G
draw(F)
# Get the top vertex and make it the origin
P = F[0, -1]
draw(Formex([P]), bbox='last')
F = F.translate(-P)
draw(F)
# Now rotate around the x axis over an angle so that the projection on the
# x-y plane is an isosceles triangle with top angle = 360/5 = 72 degrees.
# The base angles thus are (180-72)/2 = 54 degrees.
# Ratio of the height of the isosceles triangle over the icosaeder edge length.
c = 0.5*at.tand(54.)
angle = at.arccosd(at.tand(54.)/sqrt(3.))
print("Rotation Ratio: %s; Angle: %s degrees" % (c, angle))
F = F.rotate(angle, 0)
clear()
draw(F, colormap=['black', 'magenta', 'yellow', 'black'])
# Project it on the circumscribing sphere
# The sphere has radius ru
golden_ratio = 0.5 * (1. + sqrt(5.))
ru = 0.5 * a * sqrt(golden_ratio * sqrt(5.))
print("Radius of circumscribed sphere: %s" % ru)
ru *= n
C = [0., 0., -ru]
F = F.projectOnSphere(ru, center=C)
draw(F)
hx, hy, h = F.sizes()
print("Height of the dome: %s" % h)
# The base circle goes through bottom corner of n-th row,
# which will be the first point of the first triangle of the n-th row.
# Draw the point to check it.
i = (n-1)*n//2
P = F[i][0]
draw(Formex([P]), marksize=10, bbox='last')
# Get the radius of the base circle from the point's coordinates
x, y, z = P
rb = sqrt(x*x+y*y)
# Give the base points a z-coordinate 0
F = F.translate([0., 0., -z])
clear()
draw(F)
# Draw the base circle
H = simple.circle().scale(rb)
draw(H)
# Determine intersections with base plane
P = [0., 0., 0.]
N = [0., 0., 1.]
newprops = [5, 6, 6, None, 4, None, None]
F = F.cutWithPlane(P, N, side='+', newprops=newprops) # ,atol=0.0001)
#clear()
draw(F)
# Finally, create a rosette to make the circle complete
# and rotate 90 degrees to orient it like in the paper
clear()
F = F.rosette(5, 72.).rotate(90)
def cutOut(F, c, r):
"""Remove all elements of F contained in a sphere (c,r)"""
d = F.distanceFromPoint(c)
return F.select((d < r).any(axis=-1) == False)
# Cut out the door: remove all members having a point less than
# edge-length a away from the base point
p1 = [rb, 0., 0.]
F = cutOut(F, p1, 1.1*a*n/6) # a was a good size with n = 6
# Scale to the real geometry
scale = 7000. / F.sizes()[2]
print("Applying scale factor %s " % scale)
print(F.bbox())
F = F.scale(scale)
print(F.bbox())
clear()
draw(F, alpha=0.4)
export({'F': F})
[docs]def assignProperties():
"""Assign properties to the structure's facets"""
# make sure we have only one actor
clear()
FA = draw(F)
#drawNumbers(F)
p = 0
while True:
res = askItems([('Property', p)])
if not res:
break
p = res['Property']
sel = pick('element')
if 0 in sel:
if pf.debugon(pf.DEBUG.INFO):
print("PICKED NUMBERS:", sel)
F.prop[sel[0]] = p
undraw(FA)
FA = draw(F, bbox='last')
[docs]def exportProperties():
"""Save the current properties under a name"""
res = askItems([('Property Name', 'p')])
if res:
p = res['Property Name']
if not p.startswith('prop:'):
p = "prop:%s" % p
export({p: F.prop})
[docs]def selectProperties():
"""Select one of the saved properties"""
res = askItems([('Property Name', 'p')])
if res:
p = res['Property Name']
if p in pf.PF:
F.setProp(pf.PF[p])
[docs]def saveProperties(fn = None):
"""Save the current properties."""
if not fn:
fn = askFilename(dirname, filter="Property files (*.prop)")
if fn:
F.prop.tofile(fn, sep=',')
[docs]def readProperties(fn = None):
"""Read properties from file."""
if not fn:
fn = askFilename(filter="Property files (*.prop)")
if fn:
p = fromfile(fn, sep=',')
F.setProp(p)
clear()
draw(F)
[docs]def connections(elems):
"""Create lists of connections to lower entities.
Elems is an array giving the numbers of lower entities.
The result is a sequence of maxnr+1 lists, where maxnr is the
highest lower entity number. Each (possibly empty) list contains
the numbers of the rows of elems that contain (at least) one value
equal to the index of the list.
"""
return [(i, list(where(elems==i)[0])) for i in unique(elems.flat)]
#####################################################################
[docs]def createFrameModel():
"""Create the Finite Element Model.
It is supposed here that the Geometry has been created and is available
as a global variable F.
"""
wireframe()
lights(False)
# Turn the Formex structure into a TriSurface
# This guarantees that element i of the Formex is element i of the TriSurface
S = TriSurface(F)
nodes = S.coords
elems = S.elems # the triangles
# Create edges and faces from edges
print("The structure has %s nodes, %s edges and %s faces" % (S.ncoords(), S.nedges(), S.nfaces()))
# Remove the edges between to quad triangles
drawNumbers(S.coords)
quadtri = np.where(S.prop==6)[0]
nquadtri = quadtri.shape[0]
print("%s triangles are part of quadrilateral faces" % nquadtri)
faces = S.getElemEdges()[quadtri]
cnt, ind, xbin = at.histogram2(faces.reshape(-1), np.arange(faces.max()+1))
rem = np.where(cnt==2)[0]
print("Total edges %s" % len(S.edges))
print("Removing %s edges" % len(rem))
edges = S.edges[complement(rem, n=len(S.edges))]
print("Remaining edges %s" % len(edges))
# Create the steel structure
E = Formex(nodes[edges])
clear()
draw(E)
warning("Beware! This application is currently under revision.")
conn = connections(quadtri)
print(conn)
# Filter out the single connection edges
internal = [c[0] for c in conn if len(c[1]) > 1]
print("Internal edges in quadrilaterals: %s" % internal)
E = Formex(nodes[edges], 1)
E.prop[internal] = 6
wireframe()
clear()
draw(E)
# Remove internal edges
tubes = edges[E.prop != 6]
print("Number of tube elements after removing %s internals: %s" % (len(internal), tubes.shape[0]))
D = Formex(nodes[tubes], 1)
clear()
draw(D)
# Beam section and material properties
b = 60
h = 100
t = 4
b1 = b-2*t
h1 = h-2*t
A = b*h - b1*h1
print(b*h**3)
I1 = (b*h**3 - b1*h1**3) / 12
I2 = (h*b**3 - h1*b1**3) / 12
I12 = 0
J = 4 * A**2 / (2*(b+h)/t)
tube = {
'name': 'tube',
'cross_section': A,
'moment_inertia_11': I1,
'moment_inertia_22': I2,
'moment_inertia_12': I12,
'torsional_constant': J
}
steel = {
'name': 'steel',
'young_modulus': 206000,
'shear_modulus': 81500,
'density': 7.85e-9,
}
print(tube)
print(steel)
tubesection = ElemSection(section=tube, material=steel)
# Calculate the nodal loads
# Area of triangles
area, normals = S.areaNormals()
print("Area:\n%s" % area)
# compute bar lengths
bars = nodes[tubes]
barV = bars[:, 1, :] - bars[:, 0, :]
barL = sqrt((barV*barV).sum(axis=-1))
print("Member length:\n%s" % barL)
### DEFINE LOAD CASE (ask user) ###
res = askItems(
[_I('Steel', True),
_I('Glass', True),
_I('Snow', False),
_I('Solver', choices=['Calpy', 'Abaqus']),
])
if not res:
return
nlc = 0
for lc in ['Steel', 'Glass', 'Snow']:
if res[lc]:
nlc += 1
NODLoad = np.zeros((nlc, S.ncoords(), 3))
nlc = 0
if res['Steel']:
# the STEEL weight
lwgt = steel['density'] * tube['cross_section'] * 9810 # mm/s**2
print("Weight per length %s" % lwgt)
# assemble steel weight load
for e, L in zip(tubes, barL):
NODLoad[nlc, e] += [0., 0., - L * lwgt / 2]
nlc += 1
if res['Glass']:
# the GLASS weight
wgt = 450e-6 # N/mm**2
# assemble uniform glass load
for e, a in zip(S.elems, area):
NODLoad[nlc, e] += [0., 0., - a * wgt / 3]
nlc += 1
if res['Snow']:
# NON UNIFORM SNOW
fn = '../data/hesperia-nieve.prop'
snowp = fromfile(fn, sep=',')
snow_uniform = 320e-6 # N/mm**2
snow_non_uniform = {1: 333e-6, 2: 133e-6, 3: 133e-6, 4: 266e-6, 5: 266e-6, 6: 667e-6}
# assemble non-uniform snow load
for e, a, p in zip(S.elems, area, snowp):
NODLoad[nlc, e] += [0., 0., - a * snow_non_uniform[p] / 3]
nlc += 1
# For Abaqus: put the nodal loads in the properties database
print(NODLoad)
PDB = PropertyDB()
for lc in range(nlc):
for i, P in enumerate(NODLoad[lc]):
PDB.nodeProp(tag=lc, set=i, cload=[P[0], P[1], P[2], 0., 0., 0.])
# Get support nodes
botnodes = np.where(np.isclose(nodes[:, 2], 0.0))[0]
bot = nodes[botnodes]
print("There are %s support nodes." % bot.shape[0])
# Upper structure
nnodes = nodes.shape[0] # node number offset
ntubes = tubes.shape[0] # element number offset
PDB.elemProp(set=np.arange(ntubes), section=tubesection, eltype='FRAME3D')
# Create support systems (vertical beams)
bot2 = bot + [0., 0., -200.] # new nodes 200mm below bot
botnodes2 = np.arange(botnodes.shape[0]) + nnodes # node numbers
nodes = np.concatenate([nodes, bot2])
supports = np.column_stack([botnodes, botnodes2])
elems = np.concatenate([tubes, supports])
## !!!
## THIS SHOULD BE FIXED !!!
supportsection = ElemSection(material=steel, section={
'name': 'support',
'cross_section': A,
'moment_inertia_11': I1,
'moment_inertia_22': I2,
'moment_inertia_12': I12,
'torsional_constant': J
})
PDB.elemProp(set=np.arange(ntubes, elems.shape[0]), section=supportsection, eltype='FRAME3D')
# Finally, the botnodes2 get the support conditions
botnodes = botnodes2
## # Radial movement only
## np_fixed = NodeProperty(1,bound=[0,1,1,0,0,0],coords='cylindrical',coordset=[0,0,0,0,0,1])
## # No movement, since we left out the ring beam
## for i in botnodes:
## NodeProperty(i,bound=[1,1,1,0,0,0],coords='cylindrical',coordset=[0,0,0,0,0,1])
## np_central_loaded = NodeProperty(3, displacement=[[1,radial_displacement]],coords='cylindrical',coordset=[0,0,0,0,0,1])
## #np_transf = NodeProperty(0,coords='cylindrical',coordset=[0,0,0,0,0,1])
# Draw the supports
S = connect([Formex(bot), Formex(bot2)])
draw(S, color='black')
if res['Solver'] == 'Calpy':
fe_model = Dict(dict(solver='Calpy', nodes=nodes, elems=elems, prop=PDB, loads=NODLoad, botnodes=botnodes, nsteps=nlc))
else:
fe_model = Dict(dict(solver='Abaqus', nodes=nodes, elems=elems, prop=PDB, botnodes=botnodes, nsteps=nlc))
export({'fe_model': fe_model})
print("FE model created and exported as 'fe_model'")
#################### SHELL MODEL ########################################
[docs]def createShellModel():
"""Create the Finite Element Model.
It is supposed here that the Geometry has been created and is available
as a global variable F.
"""
# Turn the Formex structure into a TriSurface
# This guarantees that element i of the Formex is element i of the TriSurface
S = TriSurface(F)
print("The structure has %s nodes, %s edges and %s faces" % (S.ncoords(), S.nedges(), S.nfaces()))
clear()
draw(F)
# Shell section and material properties
# VALUES SHOULD BE SET CORRECTLY
glass_plate = {
'name': 'glass_plate',
'sectiontype': 'shell',
'thickness': 18,
'material': 'glass',
}
glass = {
'name': 'glass',
'young_modulus': 72000,
'shear_modulus': 26200,
'density': 2.5e-9, # T/mm**3
}
print(glass_plate)
print(glass)
glasssection = ElemSection(section=glass_plate, material=glass)
PDB = PropertyDB()
# All elements have same property:
PDB.elemProp(set=np.arange(S.nelems()), section=glasssection, eltype='STRI3')
# Calculate the nodal loads
# Area of triangles
area, normals = S.areaNormals()
print("Area:\n%s" % area)
### DEFINE LOAD CASE (ask user) ###
res = askItems([('Glass', True), ('Snow', False)])
if not res:
return
step = 0
if res['Glass']:
step += 1
NODLoad = np.zeros((S.ncoords(), 3))
# add the GLASS weight
wgt = 450e-6 # N/mm**2
# Or, calculate weight from density:
# wgt = glass_plate['thickness'] * glass['density'] * 9810
# assemble uniform glass load
for e, a in zip(S.elems, area):
NODLoad[e] += [0., 0., - a * wgt / 3]
# Put the nodal loads in the properties database
for i, P in enumerate(NODLoad):
PDB.nodeProp(tag=step, set=i, cload=[P[0], P[1], P[2], 0., 0., 0.])
if res['Snow']:
step += 1
NODLoad = np.zeros((S.ncoords(), 3))
# add NON UNIFORM SNOW
fn = '../data/hesperia-nieve.prop'
snowp = fromfile(fn, sep=',')
snow_uniform = 320e-6 # N/mm**2
snow_non_uniform = {1: 333e-6, 2: 133e-6, 3: 133e-6, 4: 266e-6, 5: 266e-6, 6: 667e-6}
# assemble non-uniform snow load
for e, a, p in zip(S.elems, area, snowp):
NODLoad[e] += [0., 0., - a * snow_non_uniform[p] / 3]
# Put the nodal loads in the properties database
for i, P in enumerate(NODLoad):
PDB.nodeProp(tag=step, set=[i], cload=[P[0], P[1], P[2], 0., 0., 0.])
# Get support nodes
botnodes = np.where(np.isclose(S.coords[:, 2], 0.0))[0]
bot = S.coords[botnodes].reshape((-1, 1, 3))
print("There are %s support nodes." % bot.shape[0])
botofs = bot + [0., 0., -0.2]
bbot2 = np.concatenate([bot, botofs], axis=1)
print(bbot2.shape)
Sup = Formex(bbot2)
draw(Sup)
## np_central_loaded = NodeProperty(3, displacement=[[1,radial_displacement]],coords='cylindrical',coordset=[0,0,0,0,0,1])
## #np_transf = NodeProperty(0,coords='cylindrical',coordset=[0,0,0,0,0,1])
## # Radial movement only
## np_fixed = NodeProperty(1,bound=[0,1,1,0,0,0],coords='cylindrical',coordset=[0,0,0,0,0,1])
# Since we left out the ring beam, we enforce no movement at the botnodes
bc = PDB.nodeProp(set=botnodes, bound=[1, 1, 1, 0, 0, 0], csys=CoordSystem('C', [0, 0, 0, 0, 0, 1]))
# And we record the name of the bottom nodes set
botnodeset = Nset(bc.nr)
fe_model = Dict(dict(model=FEModel(S), prop=PDB, botnodeset=botnodeset, nsteps=step))
export({'fe_model': fe_model})
smooth()
lights(False)
#####################################################################
#### Analyze the structure using Abaqus ####
#############################################################################
#### perform analysis with the calpy module ####
[docs]def runCalpyAnalysis():
"""Create data for Calpy analysis module and run Calpy on the data.
While we could write an analysis file in the Calpy format and then
run the Calpy program on it (like we did with Abaqus), we can (and do)
take another road here: Calpy has a Python/numpy interface, allowing
us to directly present the numerical data in arrays to the analysis
module.
It is supposed that the Finite Element model has been created and
exported under the name 'fe_model'.
"""
checkWorkdir()
############################
# Load the needed calpy modules
# You can prepend your own path here to override the installed calpy
# sys.path[0:0] = ['/home/bene/prj/calpy']
from pyformex.plugins import calpy_itf
calpy_itf.check()
import calpy
print(calpy)
calpy.options.optimize=True
from calpy import fe_util, beam3d
############################
try:
FE = named('fe_model')
## print FE.keys()
## nodes = FE.nodes
## elems = FE.elems
## prop = FE.prop
## nodloads = FE.loads
## botnodes = FE.botnodes
## nsteps = FE.nsteps
except Exception:
warning("I could not find the finite element model.\nMaybe you should try to create it first?")
return
# ask job name from user
res = askItems([('JobName', 'hesperia_frame'), ('Verbose Mode', False)])
if not res:
return
jobname = res['JobName']
if not jobname:
print("No Job Name: bailing out")
return
verbose = res['Verbose Mode']
nnod = FE.nodes.shape[0]
nel = FE.elems.shape[0]
print("Number of nodes: %s" % nnod)
print("Number of elements: %s" % nel)
# Create an extra node for beam orientations
#
# !!! This is ok for the support beams, but for the structural beams
# !!! this should be changed to the center of the sphere !!!
extra_node = np.array([[0.0, 0.0, 0.0]])
coords = np.concatenate([FE.nodes, extra_node])
nnod = coords.shape[0]
print("Adding a node for orientation: %s" % nnod)
# We extract the materials/sections from the property database
matprops = FE.prop.getProp(kind='e', attr=['section'])
# Beam Properties in Calpy consist of 7 values:
# E, G, rho, A, Izz, Iyy, J
# The beam y-axis lies in the plane of the 3 nodes i,j,k.
mats = np.array([[mat.young_modulus,
mat.shear_modulus,
mat.density,
mat.cross_section,
mat.moment_inertia_11,
mat.moment_inertia_22,
mat.moment_inertia_12,
] for mat in matprops])
if verbose:
print("Calpy.materials")
print(mats)
# Create element definitions:
# In calpy, each beam element is represented by 4 integer numbers:
# i j k matnr,
# where i,j are the node numbers,
# k is an extra node for specifying orientation of beam (around its axis),
# matnr refers to the material/section properties (i.e. the row nr in mats)
# Also notice that Calpy numbering starts at 1, not at 0 as customary
# in pyFormex; therefore we add 1 to elems.
# The third node for all beams is the last (extra) node, numbered nnod.
# We need to reshape tubeprops to allow concatenation
matnr = np.zeros(nel, dtype=int32)
for i, mat in enumerate(matprops): # proces in same order as above!
matnr[mat.set] = i+1
elements = np.concatenate([FE.elems + 1, # the normal node numbers
nnod * ones(shape=(nel, 1), dtype=int), # extra node
matnr.reshape((-1, 1))], # mat number
axis=1)
if verbose:
print("Calpy.elements")
print(elements)
# Boundary conditions
# While we could get the boundary conditions from the node properties
# database, we will formulate them directly from the numbers
# of the supported nodes (botnodes).
# Calpy (currently) only accepts boundary conditions in global
# (cartesian) coordinates. However, as we only use fully fixed
# (though hinged) support nodes, that presents no problem here.
# For each supported node, a list of 6 codes can (should)be given,
# corresponding to the six Degrees Of Freedom (DOFs): ux,uy,uz,rx,ry,rz.
# The code has value 1 if the DOF is fixed (=0.0) and 0 if it is free.
# The easiest way to set the correct boundary conditions array for Calpy
# is to put these codes in a text field and have them read with
# ReadBoundary.
s = ""
for n in FE.botnodes + 1: # again, the +1 is to comply with Calpy numbering!
s += " %d 1 1 1 1 1 1\n" % n # a fixed hinge
# Also clamp the fake extra node
s += " %d 1 1 1 1 1 1\n" % nnod
if verbose:
print("Specified boundary conditions")
print(s)
bcon = fe_util.ReadBoundary(nnod, 6, s)
fe_util.NumberEquations(bcon)
if verbose:
print("Calpy.DOF numbering")
print(bcon) # all DOFs are numbered from 1 to ndof
# The number of free DOFs remaining
ndof = bcon.max()
print("Number of DOF's: %s" % ndof)
# Create load vectors
# Calpy allows for multiple load cases in a single analysis.
# However, our script currently puts all loads together in a single
# load case. So the processing hereafter is rather simple, especially
# since Calpy provides a function to assemble a single concentrated
# load into the load vector. We initialize the load vector to zeros
# and then add all the concentrated loads from the properties database.
# A single concentrated load consists of 6 components, corresponding
# to the 6 DOFs of a node.
#
# AssembleVector takes 3 arguments: the global vector in which to
# assemble a nodal vector (length ndof), the nodal vector values
# (length 6), and a list of indices specifying the positions of the
# nodal DOFs in the global vector.
# Beware: The function does not change the global vector, but merely
# returns the value after assembling.
# Also notice that the indexing inside the bcon array uses numpy
# convention (starting at 0), thus no adding 1 is needed!
print("Assembling Concentrated Loads")
nlc = 1
loads = np.zeros((ndof, nlc), float)
for p in FE.prop.getProp('n', attr=['cload']):
cload = np.zeros(6)
for i, v in p.cload:
cload[i] += v
print(cload)
print(cload.shape)
loads[:, 0] = fe_util.AssembleVector(loads[:, 0], cload, bcon[p.set, :])
if verbose:
print("Calpy.Loads")
print(loads)
# Perform analysis
# OK, that is really everything there is to it. Now just run the
# analysis, and hope for the best ;)
# Enabling the Echo will print out the data.
# The result consists of nodal displacements and stress resultants.
print("Starting the Calpy analysis module --- this might take some time")
pf.app.processEvents()
#TODO: Use a Timer here
starttime = time.perf_counter()
displ, frc = beam3d.static(coords, bcon, mats, elements, loads, Echo=True)
print("Calpy analysis has finished --- Runtime was %s seconds." % (time.perf_counter()-starttime))
# Export the results, but throw way these for the extra (last) node
export({'calpy_results': (displ[:-1], frc)})
[docs]def postCalpy():
"""Show results from the Calpy analysis."""
from pyformex.plugins.postproc import niceNumber, frameScale
from pyformex.plugins.postproc_menu import showResults
try:
FE = named('fe_model')
displ, frc = named('calpy_results')
except Exception:
warning("I could not find the finite element model and/or the calpy results. Maybe you should try to first create them?")
raise
return
# The frc array returns element forces and has shape
# (nelems,nforcevalues,nloadcases)
# nforcevalues = 8 (Nx,Vy,Vz,Mx,My1,Mz1,My2,Mz2)
# Describe the nforcevalues element results in frc.
# For each result we give a short and a long description:
frc_contents = [('Nx', 'Normal force'),
('Vy', 'Shear force in local y-direction'),
('Vz', 'Shear force in local z-direction'),
('Mx', 'Torsional moment'),
('My', 'Bending moment around local y-axis'),
('Mz', 'Bending moment around local z-axis'),
('None', 'No results'),
]
# split in two lists
frc_keys = [c[0] for c in frc_contents]
frc_desc = [c[1] for c in frc_contents]
# Ask the user which results he wants
res = askItems([
_I('Type of result', choices=frc_desc),
_I('Load case', 0),
_I('Autocalculate deformation scale', True),
_I('Deformation scale', 100.),
_I('Show undeformed configuration', False),
_I('Animate results', False),
_I('Amplitude shape', 'linear', choices=['linear', 'sine']),
_I('Animation cycle', 'updown', choices=['up', 'updown', 'revert']),
_I('Number of cycles', 5),
_I('Number of frames', 10),
_I('Animation sleeptime', 0.1),
])
if res:
frcindex = frc_desc.index(res['Type of result'])
loadcase = res['Load case']
autoscale = res['Autocalculate deformation scale']
dscale = res['Deformation scale']
showref = res['Show undeformed configuration']
animate = res['Animate results']
shape = res['Amplitude shape']
cycle = res['Animation cycle']
count = res['Number of cycles']
nframes = res['Number of frames']
sleeptime = res['Animation sleeptime']
dis = displ[:, 0:3, loadcase]
if autoscale:
siz0 = Coords(FE.nodes).sizes()
siz1 = Coords(dis).sizes()
print(siz0)
print(siz1)
dscale = niceNumber(1./(siz1/siz0).max())
if animate:
dscale = dscale * frameScale(nframes, cycle=cycle, shape=shape)
# Get the scalar element result values from the frc array.
val = val1 = txt = None
if frcindex <= 5:
val = frc[:, frcindex, loadcase]
txt = frc_desc[frcindex]
if frcindex > 3:
# bending moment values at second node
val1 = frc[:, frcindex+2, loadcase]
showResults(FE.nodes, FE.elems, dis, txt, val, showref, dscale, count, sleeptime)
#############################################################################
######### Create a menu with interactive tasks #############
# TODO: This could/should use the PluginMenu facility
####################################################################
######### What to do when the script is executed ###################
def run():
# The sole intent of running this script is to create a top level
# menu 'Hesperia'. The typical action then might be 'show_menu()'.
# However, during development, you might want to change the menu's
# actions will pyFormex is running, so a 'reload' action seems
# more appropriate.
clear()
reload_menu()
if __name__ == '__draw__':
run()
# End