ANCFThinPlateRevisionTest.py
You can view and download this file on Github: ANCFThinPlateRevisionTest.py
1#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2# This is an EXUDYN example
3#
4# Details: Tests of ObjectANCFThinPlate and exudyn.shells (#2857, #2858): the integration modes against
5# F L^3/(3 EI), the material curvature measure, large-rotation pure bending, the first
6# eigenfrequency, the mass, gravity, variable thickness (4 and 12 values), Kelvin-Voigt
7# damping (no damping of a rigid motion, the rule of the bending damping), the output
8# variables, ShellMesh with a surface map and a thickness function, and the visualization
9# settings of shells. Small meshes and linear or few-step static solutions; damping is checked
10# with the linearized system. Each test prints the measured and the reference value.
11# The result is the number of tests that passed.
12#
13# Author: Michael Pieber, Johannes Gerstmayr
14# Date: 2026-10-01, ported 2026-10-05
15#
16# Copyright:This file is part of Exudyn. Exudyn is free software. You can redistribute it and/or modify it under the terms of the Exudyn license. See 'LICENSE.txt' for more details.
17#
18#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
19
20import io
21import unittest
22import numpy as np
23import exudyn as exu
24from exudyn.utilities import (MarkerNodePosition, LoadForceVector, MarkerNodeCoordinate, NodePointGround,
25 CoordinateConstraint)
26from exudyn.shells import ShellMesh, SurfaceMap, AddNodeConstraints
27from exudyn.utilities import MarkerBodyMass, LoadMassProportional
28
29#material and strip geometry (Poisson ratio 0: plate strip = Euler-Bernoulli beam)
30youngsModulus = 2.1e11
31poissonRatio = 0.
32density = 7800.
33stripLength = 1.0
34stripWidth = 0.1
35stripThickness = 0.01
36bendingStiffness = youngsModulus * stripWidth * stripThickness**3 / 12. #EI of the strip
37plateBendingStiffness = youngsModulus * stripThickness**3 / 12. #D per unit width (nu = 0)
38tipForce = 1.
39gravity = 9.81
40numberOfPlateDOFsPerNode = 9 #position, slope x, slope y
41firstBeamEigenvalueFactor = 1.87510407 #lambda_1 of the clamped-free Euler-Bernoulli beam
42numberOfTipLoadSteps = 1
43numberOfBendingLoadSteps = 5
44
45
46def StaticSettings(numberOfLoadSteps=numberOfTipLoadSteps):
47 """Return simulation settings for the static tests (no solution file, sparse solver).
48 @param numberOfLoadSteps: number of static load steps
49 @return: exu.SimulationSettings"""
50 simulationSettings = exu.SimulationSettings()
51 simulationSettings.staticSolver.numberOfLoadSteps = numberOfLoadSteps
52 simulationSettings.staticSolver.newton.relativeTolerance = 1e-10
53 simulationSettings.staticSolver.newton.absoluteTolerance = 1e-3 #round-off floor of the residual (N)
54 simulationSettings.staticSolver.verboseMode = 0
55 simulationSettings.solution.file.write = False
56 simulationSettings.linearSolver.solverType = exu.LinearSolverType.EigenSparse
57 return simulationSettings
58
59
60def CreateStrip(numberOfElements=4, integrationMode=0, alongY=False, clamp=True, thicknessField=None,
61 massProportionalLoad=None, stiffnessProportionalDamping=0.):
62 """Create a plate strip (stripLength x stripWidth, one element across the width) with ShellMesh, optionally
63 clamped at the edge at the origin.
64 @param numberOfElements: number of elements along the strip
65 @param integrationMode: useReducedOrderIntegration of the elements
66 @param alongY: True: strip axis along global y, False: along global x
67 @param clamp: True: all 9 coordinates of the nodes at the origin edge are fixed
68 @param thicknessField: function f(x, y) of the thickness, evaluated at the nodes (4 values per element)
69 @param massProportionalLoad: a load per mass, e.g. gravity, on every element
70 @param stiffnessProportionalDamping: Kelvin-Voigt damping coefficient of ShellMesh
71 @return: (system container, mbs, dictionary of nodes, elements, edgeNodeNumbers and nodeId, list of tip node numbers)"""
72 systemContainer = exu.SystemContainer()
73 mbs = systemContainer.AddSystem()
74 (lengthX, lengthY, nx, ny) = ((stripWidth, stripLength, 1, numberOfElements) if alongY
75 else (stripLength, stripWidth, numberOfElements, 1))
76 plate = ShellMesh(vertices=[[0, 0, 0], [lengthX, 0, 0], [lengthX, lengthY, 0], [0, lengthY, 0]],
77 numberOfElementsX=nx, numberOfElementsY=ny, youngsModulus=youngsModulus,
78 poissonsRatio=poissonRatio, density=density, thickness=stripThickness,
79 stiffnessProportionalDamping=stiffnessProportionalDamping)
80 if thicknessField is not None:
81 plate.thicknessAtNodes = [thicknessField(ix * lengthX / nx, iy * lengthY / ny)
82 for iy in range(ny + 1) for ix in range(nx + 1)]
83 plate.CreateANCFThinPlateElements(mbs, useReducedOrderIntegration=integrationMode)
84 if massProportionalLoad is not None:
85 for elementNumber in plate.elementNumbers:
86 mbs.AddLoad(LoadMassProportional(markerNumber=mbs.AddMarker(MarkerBodyMass(bodyNumber=elementNumber)),
87 loadVector=massProportionalLoad))
88 built = {'nodes': plate.nodeNumbers, 'elements': plate.elementNumbers,
89 'edgeNodeNumbers': plate.boundaryNodeNumbers, 'nodeId': lambda iy, ix: iy * (nx + 1) + ix}
90 if alongY:
91 clampNodes, tipNodes = built['edgeNodeNumbers']['bottom'], built['edgeNodeNumbers']['top']
92 else:
93 clampNodes, tipNodes = built['edgeNodeNumbers']['left'], built['edgeNodeNumbers']['right']
94 if clamp:
95 for nodeNumber in clampNodes:
96 AddNodeConstraints(mbs, nodeNumber, dofs=list(range(numberOfPlateDOFsPerNode)))
97 return systemContainer, mbs, built, tipNodes
98
99
100def AddTipLoad(mbs, tipNodes, force=tipForce):
101 """Distribute a force in -z equally on the tip nodes.
102 @param mbs: main system
103 @param tipNodes: list of tip node numbers
104 @param force: total force (N)"""
105 for nodeNumber in tipNodes:
106 markerNumber = mbs.AddMarker(MarkerNodePosition(nodeNumber=nodeNumber))
107 mbs.AddLoad(LoadForceVector(markerNumber=markerNumber, loadVector=[0, 0, -force / len(tipNodes)]))
108
109
110def TipDeflection(mbs, tipNodes):
111 """Return the mean downward (-z) displacement of the tip nodes.
112 @param mbs: main system (solved)
113 @param tipNodes: list of tip node numbers
114 @return: deflection (m), positive downward"""
115 return -np.mean([mbs.GetNodeOutput(nodeNumber, exu.OutputVariableType.Displacement)[2] for nodeNumber in tipNodes])
116
117
118def RigidMotionVectors(mbs, rotationMatrix, angularVelocity):
119 """Coordinates of a rigid rotation of all plate nodes and the velocities of a rigid angular velocity in
120 this rotated configuration (positions and both slope vectors rotate with the body).
121 @param mbs: main system (assembled, NodePointSlope12 nodes only)
122 @param rotationMatrix: 3x3 rotation of the reference configuration
123 @param angularVelocity: angular velocity vector (rad/s)
124 @return: (ODE2 coordinates, ODE2 velocities) as numpy arrays"""
125 coordinates = np.zeros(mbs.systemData.ODE2Size())
126 velocities = np.zeros(mbs.systemData.ODE2Size())
127 for nodeNumber in range(mbs.systemData.NumberOfNodes()):
128 referenceCoordinates = np.array(mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates'))
129 firstIndex = mbs.GetNodeODE2Index(nodeNumber)
130 for vectorIndex in range(3):
131 referenceVector = referenceCoordinates[3 * vectorIndex:3 * vectorIndex + 3]
132 rotatedVector = rotationMatrix @ referenceVector
133 coordinates[firstIndex + 3 * vectorIndex:firstIndex + 3 * vectorIndex + 3] = rotatedVector - referenceVector
134 velocities[firstIndex + 3 * vectorIndex:firstIndex + 3 * vectorIndex + 3] = np.cross(angularVelocity, rotatedVector)
135 return coordinates, velocities
136
137
138def PrintComparison(label, measured, reference):
139 """Verbose test output: measured value, reference value and relative deviation.
140 @param label: description of the quantity
141 @param measured: measured value
142 @param reference: reference value (nonzero)"""
143 print(f'\n {label}: measured {measured:.9e}, reference {reference:.9e}, '
144 f'relative deviation {measured / reference - 1:+.3e}', end='')
145
146
147class TestANCFThinPlateElement(unittest.TestCase):
148 """Kinematics, integration, mass, gravity and thickness of ObjectANCFThinPlate."""
149
150 def testCantileverTipDeflectionAllIntegrationModes(self):
151 """Linear cantilever with tip load: cubic Hermite elements are exact, F L^3/(3 EI), for modes 0, 1, 2;
152 mode 2 is an alias of mode 1 (bitwise identical)."""
153 deflections = {}
154 for integrationMode in [0, 1, 2]:
155 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=2, integrationMode=integrationMode)
156 AddTipLoad(mbs, tipNodes)
157 mbs.Assemble()
158 mbs.SolveStatic(StaticSettings())
159 deflections[integrationMode] = TipDeflection(mbs, tipNodes)
160 reference = tipForce * stripLength**3 / (3 * bendingStiffness)
161 PrintComparison(f'tip deflection mode {integrationMode}', deflections[integrationMode], reference)
162 self.assertAlmostEqual(deflections[integrationMode] / reference, 1., delta=1e-6)
163 self.assertEqual(deflections[1], deflections[2])
164
165 @unittest.skipIf('[FAST]' in exu.config.Version(True),
166 'exudynCPPfast compiles the error checks away by design (#2911)')
167 def testInvalidIntegrationModeRaises(self):
168 """useReducedOrderIntegration = 3 must stop the solver with an error."""
169 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=2, integrationMode=3)
170 mbs.Assemble()
171 printToConsole = exu.config.printToConsole
172 exu.config.printToConsole = False #the expected error message would clutter the test output
173 try:
174 with self.assertRaises(ValueError):
175 mbs.SolveStatic(StaticSettings())
176 finally:
177 exu.config.printToConsole = printToConsole
178
179 def testMaterialCurvatureOfStretchedArc(self):
180 """Material curvature measure: a strip placed exactly on a circular arc of radius rho and stretched by
181 lambda has |kappa_xx| = lambda/rho (geometric measure: 1/rho) and Green-Lagrange strain (lambda^2-1)/2.
182 Pure kinematics, no solver."""
183 stretch, radius = 1.2, 1.0
184 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=4, clamp=False)
185 mbs.Assemble()
186 coordinates = np.zeros(mbs.systemData.ODE2Size())
187 for nodeNumber in range(mbs.systemData.NumberOfNodes()):
188 referenceCoordinates = np.array(mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates'))
189 arcAngle = stretch * referenceCoordinates[0] / radius
190 position = np.array([radius * np.sin(arcAngle), referenceCoordinates[1], radius * (1 - np.cos(arcAngle))])
191 slopeX = stretch * np.array([np.cos(arcAngle), 0, np.sin(arcAngle)])
192 firstIndex = mbs.GetNodeODE2Index(nodeNumber)
193 coordinates[firstIndex:firstIndex + 3] = position - referenceCoordinates[0:3]
194 coordinates[firstIndex + 3:firstIndex + 6] = slopeX - referenceCoordinates[3:6]
195 mbs.systemData.SetODE2Coordinates(coordinates.tolist(), configuration=exu.ConfigurationType.Current)
196 for elementNumber in built['elements']:
197 curvature = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.CurvatureLocal,
198 localPosition=[0, 0, 0], configuration=exu.ConfigurationType.Current)
199 strain = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.StrainLocal,
200 localPosition=[0, 0, 0], configuration=exu.ConfigurationType.Current)
201 PrintComparison(f'element {elementNumber} |kappa_xx|', abs(curvature[0]), stretch / radius)
202 self.assertAlmostEqual(abs(curvature[0]) / (stretch / radius), 1., delta=1e-2) #interpolation error 0.4 %
203 self.assertAlmostEqual(strain[0], 0.5 * (stretch**2 - 1), delta=1e-4)
204 self.assertAlmostEqual(curvature[1], 0., delta=1e-10)
205
206 def testPureBendingLargeRotation(self):
207 """Tip slope rotated by 1 rad: the strip forms a circular arc of radius L/theta; curvature theta/L,
208 membrane strain small compared with the surface bending strain."""
209 rotationAngle = 1.0
210 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=4)
211 groundNode = mbs.AddNode(NodePointGround())
212 targetSlope = [np.cos(rotationAngle), 0., np.sin(rotationAngle)]
213 for nodeNumber in tipNodes:
214 referenceCoordinates = mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates')
215 for component in range(3):
216 groundMarker = mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=groundNode, coordinate=0))
217 slopeMarker = mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=nodeNumber, coordinate=3 + component))
218 mbs.AddObject(CoordinateConstraint(markerNumbers=[groundMarker, slopeMarker],
219 offset=targetSlope[component] - referenceCoordinates[3 + component]))
220 mbs.Assemble()
221 mbs.SolveStatic(StaticSettings(numberOfBendingLoadSteps))
222 tipPosition = np.mean([mbs.GetNodeOutput(nodeNumber, exu.OutputVariableType.Position) for nodeNumber in tipNodes], axis=0)
223 exactTipPosition = stripLength / rotationAngle * np.array([np.sin(rotationAngle), 1 - np.cos(rotationAngle)])
224 arcError = np.linalg.norm(tipPosition[[0, 2]] - exactTipPosition)
225 print(f'\n tip position error {arcError:.2e} m', end='')
226 self.assertLess(arcError, 1e-5)
227 for elementNumber in built['elements']:
228 curvature = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.CurvatureLocal, localPosition=[0, 0, 0])
229 membraneStrain = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.StrainLocal, localPosition=[0, 0, 0])
230 surfaceStrain = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.StrainLocal, localPosition=[0, 0, 1])
231 PrintComparison(f'element {elementNumber} |kappa_xx|', abs(curvature[0]), rotationAngle / stripLength)
232 self.assertAlmostEqual(abs(curvature[0]) / (rotationAngle / stripLength), 1., delta=5e-3)
233 self.assertLess(abs(membraneStrain[0] / surfaceStrain[0]), 2e-3)
234
235 def testFirstEigenfrequencyCantilever(self):
236 """First bending eigenfrequency of the clamped strip vs. Euler-Bernoulli (mass and stiffness)."""
237 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=4, clamp=False)
238 mbs.Assemble()
239 constrainedCoordinates = []
240 for nodeNumber in built['edgeNodeNumbers']['left']:
241 firstIndex = mbs.GetNodeODE2Index(nodeNumber)
242 constrainedCoordinates += list(range(firstIndex, firstIndex + numberOfPlateDOFsPerNode))
243 frequencies = mbs.ComputeODE2Eigenvalues(constrainedCoordinates=constrainedCoordinates,
244 convert2Frequencies=True, useSparseSolver=False)[0]
245 reference = (firstBeamEigenvalueFactor**2 / (2 * np.pi)
246 * np.sqrt(bendingStiffness / (density * stripWidth * stripThickness * stripLength**4)))
247 PrintComparison('first eigenfrequency (Hz)', frequencies[0], reference)
248 self.assertAlmostEqual(frequencies[0] / reference, 1., delta=1e-4)
249
250 def testMassOfLinearlyTaperedPlate(self):
251 """Mass matrix with thickness h(x) = h0 (1 - x/(2L)): total mass rho b h0 L 3/4 (exact)."""
252 systemContainer, mbs, built, tipNodes = CreateStrip(
253 numberOfElements=4, clamp=False, thicknessField=lambda x, y: stripThickness * (1 - 0.5 * x / stripLength))
254 mbs.Assemble()
255 massMatrix = mbs.ComputeLinearizedSystem(StaticSettings())[0]
256 translationZ = np.zeros(mbs.systemData.ODE2Size())
257 for nodeNumber in range(mbs.systemData.NumberOfNodes()):
258 translationZ[mbs.GetNodeODE2Index(nodeNumber) + 2] = 1.
259 reference = density * stripWidth * stripThickness * stripLength * 0.75
260 PrintComparison('total mass (kg)', translationZ @ massMatrix @ translationZ, reference)
261 self.assertAlmostEqual(translationZ @ massMatrix @ translationZ / reference, 1., delta=1e-10)
262
263 def testGravityTipDeflection(self):
264 """Self-weight (LoadMassProportional on each element): tip deflection q L^4/(8 EI)."""
265 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=4, massProportionalLoad=[0, 0, -gravity])
266 mbs.Assemble()
267 mbs.SolveStatic(StaticSettings())
268 lineLoad = density * stripWidth * stripThickness * gravity
269 reference = lineLoad * stripLength**4 / (8 * bendingStiffness)
270 PrintComparison('tip deflection under self-weight', TipDeflection(mbs, tipNodes), reference)
271 self.assertAlmostEqual(TipDeflection(mbs, tipNodes) / reference, 1., delta=1e-4)
272
273 def testUniformVariableThicknessEqualsConstant(self):
274 """4 equal nodal thicknesses (point-wise stiffness D_base h) give the same result as constant thickness."""
275 deflections = []
276 for builderOptions in [{}, {'thicknessField': lambda x, y: stripThickness}]:
277 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=2, **builderOptions)
278 AddTipLoad(mbs, tipNodes)
279 mbs.Assemble()
280 mbs.SolveStatic(StaticSettings())
281 deflections.append(TipDeflection(mbs, tipNodes))
282 PrintComparison('tip deflection, 4 equal thickness values', deflections[1], deflections[0])
283 self.assertAlmostEqual(deflections[1] / deflections[0], 1., delta=1e-10)
284
285 def testLinearTaperTipDeflection(self):
286 """Thickness h(x) = h0 (1 - x/(2L)), bending stiffness point-wise with h^3: tip deflection
287 integral of F (L-x)^2 / (E b h(x)^3/12) dx."""
288 thicknessFunction = lambda x, y: stripThickness * (1 - 0.5 * x / stripLength)
289 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=4, thicknessField=thicknessFunction)
290 AddTipLoad(mbs, tipNodes)
291 mbs.Assemble()
292 mbs.SolveStatic(StaticSettings())
293 positions = np.linspace(0, stripLength, 20001)
294 integrand = tipForce * (stripLength - positions)**2 / (youngsModulus * stripWidth * thicknessFunction(positions, 0)**3 / 12)
295 reference = np.sum(0.5 * (integrand[1:] + integrand[:-1]) * np.diff(positions)) #trapezoidal rule
296 PrintComparison('tip deflection, linear taper', TipDeflection(mbs, tipNodes), reference)
297 self.assertAlmostEqual(TipDeflection(mbs, tipNodes) / reference, 1., delta=1e-3)
298
299 def testTwelveThicknessValuesEqualConstant(self):
300 """12 thickness values [h_i, h_s,i, h_t,i] with zero gradients give the deflection of the constant thickness."""
301 deflections = []
302 for twelveValues in [False, True]:
303 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=2)
304 if twelveValues:
305 for elementNumber in built['elements']:
306 mbs.SetObjectParameter(elementNumber, 'thickness', [stripThickness, 0., 0.] * 4)
307 AddTipLoad(mbs, tipNodes)
308 mbs.Assemble()
309 mbs.SolveStatic(StaticSettings())
310 deflections.append(TipDeflection(mbs, tipNodes))
311 PrintComparison('tip deflection, 12 thickness values', deflections[1], deflections[0])
312 self.assertAlmostEqual(deflections[1] / deflections[0], 1., delta=1e-12)
313
314class TestANCFThinPlateDamping(unittest.TestCase):
315 """Kelvin-Voigt damping, checked with the damping matrix of the linearized system."""
316
317 def DampingMatrix(self, membraneDamping, bendingDamping, coordinates=None):
318 """Damping matrix of a free 2-element strip.
319 @param membraneDamping: stiffnessProportionalDamping (s)
320 @param bendingDamping: bendingStiffnessProportionalDamping (s), negative: use membraneDamping
321 @param coordinates: optional ODE2 coordinates (initial and current configuration)
322 @return: (mbs, stiffness matrix, damping matrix)"""
323 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=2, clamp=False,
324 stiffnessProportionalDamping=membraneDamping)
325 for elementNumber in built['elements']:
326 mbs.SetObjectParameter(elementNumber, 'bendingStiffnessProportionalDamping', bendingDamping)
327 mbs.Assemble()
328 if coordinates is not None:
329 for configuration in [exu.ConfigurationType.Initial, exu.ConfigurationType.Current]:
330 mbs.systemData.SetODE2Coordinates(coordinates.tolist(), configuration=configuration)
331 massMatrix, stiffnessMatrix, dampingMatrix = mbs.ComputeLinearizedSystem(StaticSettings())
332 return mbs, stiffnessMatrix, dampingMatrix
333
334 def testNoDampingOfRigidRotation(self):
335 """In a finitely rotated configuration, a rigid angular velocity produces no damping force (strain
336 rates vanish), while a bending velocity does; also no elastic force change for rigid rotation."""
337 rotationAngle = 1.0
338 rotationMatrix = np.array([[np.cos(rotationAngle), 0, np.sin(rotationAngle)], [0, 1, 0],
339 [-np.sin(rotationAngle), 0, np.cos(rotationAngle)]])
340 mbs, stiffnessMatrix, dampingMatrix = self.DampingMatrix(1e-3, -1.)
341 coordinates, rigidVelocities = RigidMotionVectors(mbs, rotationMatrix, np.array([0.3, -0.5, 0.7]))
342 mbs, stiffnessMatrix, dampingMatrix = self.DampingMatrix(1e-3, -1., coordinates)
343 bendingVelocities = np.zeros(mbs.systemData.ODE2Size())
344 for nodeNumber in range(mbs.systemData.NumberOfNodes()):
345 referenceCoordinates = np.array(mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates'))
346 firstIndex = mbs.GetNodeODE2Index(nodeNumber)
347 bendingVelocities[firstIndex:firstIndex + 3] = rotationMatrix @ np.array([0, 0, referenceCoordinates[0]**2])
348 rigidForce = np.linalg.norm(dampingMatrix @ rigidVelocities)
349 bendingForce = np.linalg.norm(dampingMatrix @ bendingVelocities)
350 print(f'\n |D v_rigid| = {rigidForce:.3e}, |D v_bending| = {bendingForce:.3e}, '
351 f'|K v_rigid|/|K v_bending| = {np.linalg.norm(stiffnessMatrix @ rigidVelocities) / np.linalg.norm(stiffnessMatrix @ bendingVelocities):.3e}', end='')
352 self.assertGreater(bendingForce, 1.)
353 self.assertLess(rigidForce / bendingForce, 1e-9)
354 self.assertLess(np.linalg.norm(stiffnessMatrix @ rigidVelocities) / np.linalg.norm(stiffnessMatrix @ bendingVelocities), 1e-9)
355
356 def testBendingDampingParameterRule(self):
357 """Flat strip: out-of-plane velocity w = x^2 only strains in bending, in-plane velocity u = x only in the
358 membrane. beta_kappa >= 0 is used as is (0 switches bending damping off), negative uses beta_epsilon."""
359 cases = [(1e-3, 0., False, True), (1e-3, -1., True, True), (0., 1e-3, True, False), (0., -1., False, False)]
360 for membraneDamping, bendingDamping, bendingDamped, membraneDamped in cases:
361 mbs, stiffnessMatrix, dampingMatrix = self.DampingMatrix(membraneDamping, bendingDamping)
362 bendingVelocities = np.zeros(mbs.systemData.ODE2Size())
363 stretchVelocities = np.zeros(mbs.systemData.ODE2Size())
364 for nodeNumber in range(mbs.systemData.NumberOfNodes()):
365 referenceCoordinates = np.array(mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates'))
366 firstIndex = mbs.GetNodeODE2Index(nodeNumber)
367 bendingVelocities[firstIndex + 2] = referenceCoordinates[0]**2 #w = x^2
368 bendingVelocities[firstIndex + 5] = 2 * referenceCoordinates[0] #w_x = 2x
369 stretchVelocities[firstIndex] = referenceCoordinates[0] #u = x
370 stretchVelocities[firstIndex + 3] = 1. #u_x = 1
371 bendingForce = np.linalg.norm(dampingMatrix @ bendingVelocities)
372 stretchForce = np.linalg.norm(dampingMatrix @ stretchVelocities)
373 print(f'\n beta_epsilon = {membraneDamping}, beta_kappa = {bendingDamping}: '
374 f'|D v_bending| = {bendingForce:.3e}, |D v_stretch| = {stretchForce:.3e}', end='')
375 self.assertEqual(bendingForce > 1e-6, bendingDamped)
376 self.assertEqual(stretchForce > 1e-6, membraneDamped)
377
378
379class TestANCFThinPlateOutputs(unittest.TestCase):
380 """Output variables StressLocal, StrainLocal, CurvatureLocal, ForceLocal, TorqueLocal."""
381
382 def SolvedTipLoadStrip(self, alongY):
383 """Clamped 4-element strip with tip load in -z (concave down), solved.
384 @param alongY: strip axis along global y
385 @return: (mbs, built dictionary)"""
386 systemContainer, mbs, built, tipNodes = CreateStrip(numberOfElements=4, alongY=alongY)
387 AddTipLoad(mbs, tipNodes)
388 mbs.Assemble()
389 mbs.SolveStatic(StaticSettings())
390 return mbs, built
391
392 def testOutputVectorSizes(self):
393 """StressLocal and StrainLocal have 6 components, CurvatureLocal, ForceLocal and TorqueLocal 3."""
394 mbs, built = self.SolvedTipLoadStrip(False)
395 expectedSizes = {exu.OutputVariableType.StressLocal: 6, exu.OutputVariableType.StrainLocal: 6,
396 exu.OutputVariableType.CurvatureLocal: 3, exu.OutputVariableType.ForceLocal: 3,
397 exu.OutputVariableType.TorqueLocal: 3}
398 for outputVariableType, expectedSize in expectedSizes.items():
399 value = mbs.GetObjectOutputBody(built['elements'][0], outputVariableType, localPosition=[0, 0, 0])
400 print(f'\n {outputVariableType}: {len(value)} components', end='')
401 self.assertEqual(len(value), expectedSize)
402
403 def testStressStrainAndBendingMoment(self):
404 """Top fiber (zeta = +1) near the clamp in tension for concave-down bending along x and along y;
405 sigma = E epsilon (nu = 0); |sigma| = 6 m/h^2 with m = F (L - s)/b at the element center."""
406 for alongY in [False, True]:
407 mbs, built = self.SolvedTipLoadStrip(alongY)
408 component = 1 if alongY else 0
409 elementNumber = built['elements'][0]
410 stressTop = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.StressLocal, localPosition=[0, 0, 1])[component]
411 strainTop = mbs.GetObjectOutputBody(elementNumber, exu.OutputVariableType.StrainLocal, localPosition=[0, 0, 1])[component]
412 momentPerWidth = tipForce * (stripLength - 0.5 * stripLength / 4) / stripWidth
413 PrintComparison(f'strip along {"y" if alongY else "x"}: top stress', stressTop, 6 * momentPerWidth / stripThickness**2)
414 self.assertGreater(stressTop, 0.)
415 self.assertAlmostEqual(stressTop / (youngsModulus * strainTop), 1., delta=1e-8)
416 self.assertAlmostEqual(stressTop / (6 * momentPerWidth / stripThickness**2), 1., delta=1e-5) #nonlinear static solution
417
418 def testMomentEqualsPlateStiffnessTimesCurvature(self):
419 """TorqueLocal = E h^3/12 CurvatureLocal component-wise (nu = 0), i.e. moment and curvature use the same
420 sign convention, for strips along x and along y."""
421 for alongY in [False, True]:
422 mbs, built = self.SolvedTipLoadStrip(alongY)
423 component = 1 if alongY else 0
424 curvature = mbs.GetObjectOutputBody(built['elements'][0], exu.OutputVariableType.CurvatureLocal, localPosition=[0, 0, 0])
425 moment = mbs.GetObjectOutputBody(built['elements'][0], exu.OutputVariableType.TorqueLocal, localPosition=[0, 0, 0])
426 PrintComparison(f'strip along {"y" if alongY else "x"}: moment/curvature', moment[component] / curvature[component],
427 plateBendingStiffness)
428 self.assertAlmostEqual(moment[component] / curvature[component] / plateBendingStiffness, 1., delta=1e-8)
429
430 def testCurvatureSignSameInBothDirections(self):
431 """For the same concave-down bending, CurvatureLocal component 0 (strip along x) and component 1 (strip
432 along y) have the same sign, the sign of ObjectANCFCable2D: negative (#2864)."""
433 curvatures = []
434 for alongY in [False, True]:
435 mbs, built = self.SolvedTipLoadStrip(alongY)
436 curvature = mbs.GetObjectOutputBody(built['elements'][0], exu.OutputVariableType.CurvatureLocal, localPosition=[0, 0, 0])
437 curvatures.append(curvature[1 if alongY else 0])
438 print(f'\n curvature along x: {curvatures[0]:+.4e}, along y: {curvatures[1]:+.4e}', end='')
439 self.assertEqual(np.sign(curvatures[0]), np.sign(curvatures[1]))
440 self.assertLess(curvatures[0], 0.)
441
442
443class TestShellsModule(unittest.TestCase):
444 """exudyn.shells: ShellMesh, surface maps and thickness functions."""
445
446 def testShellMeshTopology(self):
447 """3 x 2 elements: 12 nodes, 6 elements, edge node lists, node positions on the grid."""
448 systemContainer = exu.SystemContainer()
449 mbs = systemContainer.AddSystem()
450 numberOfElementsX, numberOfElementsY, lengthX, lengthY = 3, 2, 0.6, 0.4
451 origin = np.array([0.1, -0.2, 0.3])
452 plate = ShellMesh(vertices=[origin, origin + [lengthX, 0, 0], origin + [lengthX, lengthY, 0], origin + [0, lengthY, 0]],
453 numberOfElementsX=numberOfElementsX, numberOfElementsY=numberOfElementsY,
454 youngsModulus=youngsModulus, poissonsRatio=0.3, density=density, thickness=stripThickness)
455 plate.CreateANCFThinPlateElements(mbs)
456 print(f'\n nodes {len(plate.nodeNumbers)}, elements {len(plate.elementNumbers)}, edge nodes '
457 f'{ {key: len(value) for key, value in plate.boundaryNodeNumbers.items()} }', end='')
458 self.assertEqual(len(plate.nodeNumbers), (numberOfElementsX + 1) * (numberOfElementsY + 1))
459 self.assertEqual(len(plate.elementNumbers), numberOfElementsX * numberOfElementsY)
460 self.assertEqual(len(plate.boundaryNodeNumbers['left']), numberOfElementsY + 1)
461 self.assertEqual(len(plate.boundaryNodeNumbers['bottom']), numberOfElementsX + 1)
462 for indexY in range(numberOfElementsY + 1):
463 for indexX in range(numberOfElementsX + 1):
464 nodeNumber = plate.nodeNumbers[indexY * (numberOfElementsX + 1) + indexX]
465 position = np.array(mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates'))[0:3]
466 expected = origin + [indexX * lengthX / numberOfElementsX, indexY * lengthY / numberOfElementsY, 0]
467 self.assertLess(np.linalg.norm(position - expected), 1e-12)
468
469 def testHemisphericalMapNodesOnSphere(self):
470 """SurfaceMap.HemisphericalShell: all nodes on the sphere of radius R, both slope vectors tangent to it."""
471 systemContainer = exu.SystemContainer()
472 mbs = systemContainer.AddSystem()
473 radius = 2.0
474 plate = ShellMesh(vertices=[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], numberOfElementsX=4, numberOfElementsY=4,
475 youngsModulus=youngsModulus, poissonsRatio=0.3, density=density, thickness=stripThickness,
476 surfaceMap=SurfaceMap.HemisphericalShell(1., 1., radius, np.pi / 3))
477 plate.CreateANCFThinPlateElements(mbs)
478 radiusError, normalSlopeComponent = 0., 0.
479 for nodeNumber in plate.nodeNumbers:
480 referenceCoordinates = np.array(mbs.GetNodeParameter(nodeNumber, 'referenceCoordinates'))
481 position = referenceCoordinates[0:3]
482 radiusError = max(radiusError, abs(np.linalg.norm(position) - radius))
483 for slope in [referenceCoordinates[3:6], referenceCoordinates[6:9]]:
484 if np.linalg.norm(slope) > 0: #degenerate slope at the pole
485 normalSlopeComponent = max(normalSlopeComponent, abs(slope @ position) / (np.linalg.norm(slope) * radius))
486 print(f'\n radius error {radiusError:.2e}, normal slope component {normalSlopeComponent:.2e}', end='')
487 self.assertLess(radiusError, 1e-12)
488 self.assertLess(normalSlopeComponent, 1e-12)
489
490 def testThicknessFunctionOfARotatedStrip(self):
491 """A strip turned by 30 degrees in the x-y plane with the thickness h0 (1 - s/(2L)) along its axis s, given as a
492 function of the global x and y: the gradients along the node slopes give 12 thickness values per element, and
493 the tip deflection is the one of the tapered cantilever (#2864)."""
494 angle = np.pi / 6
495 axis, across = np.array([np.cos(angle), np.sin(angle), 0.]), np.array([-np.sin(angle), np.cos(angle), 0.])
496 def ThicknessFunction(x, y):
497 return stripThickness * (1 - 0.5 * (x * axis[0] + y * axis[1]) / stripLength)
498 systemContainer = exu.SystemContainer()
499 mbs = systemContainer.AddSystem()
500 plate = ShellMesh(vertices=[[0, 0, 0], stripLength * axis, stripLength * axis + stripWidth * across, stripWidth * across],
501 numberOfElementsX=8, numberOfElementsY=1, youngsModulus=youngsModulus, poissonsRatio=poissonRatio,
502 density=density, thickness=stripThickness, thicknessFunction=ThicknessFunction)
503 plate.CreateANCFThinPlateElements(mbs)
504 self.assertEqual(len(mbs.GetObjectParameter(plate.elementNumbers[0], 'thickness')), 12)
505 for nodeNumber in plate.boundaryNodeNumbers['left']:
506 AddNodeConstraints(mbs, nodeNumber, dofs=list(range(numberOfPlateDOFsPerNode)))
507 tipNodes = plate.boundaryNodeNumbers['right']
508 for nodeNumber in tipNodes:
509 mbs.AddLoad(LoadForceVector(markerNumber=mbs.AddMarker(MarkerNodePosition(nodeNumber=nodeNumber)),
510 loadVector=[0, 0, -tipForce / len(tipNodes)]))
511 mbs.Assemble()
512 mbs.SolveStatic(StaticSettings())
513 positions = np.linspace(0, stripLength, 20001)
514 thickness = stripThickness * (1 - 0.5 * positions / stripLength)
515 integrand = tipForce * (stripLength - positions)**2 / (youngsModulus * stripWidth * thickness**3 / 12)
516 reference = np.sum(0.5 * (integrand[1:] + integrand[:-1]) * np.diff(positions)) #trapezoidal rule
517 PrintComparison('tip deflection, thickness function of a rotated strip', TipDeflection(mbs, tipNodes), reference)
518 self.assertAlmostEqual(TipDeflection(mbs, tipNodes) / reference, 1., delta=1e-3)
519
520
521class TestVisualizationSettings(unittest.TestCase):
522 """The settings of visualizationSettings.bodies.shells and their defaults."""
523
524 def testShellVisualizationDefaults(self):
525 """contourZeta, reducedInterpolation, drawNormal, drawNormalFactor, drawNormalColor, drawNormalLines."""
526 shellSettings = exu.SystemContainer().visualizationSettings.bodies.shells
527 self.assertEqual(shellSettings.contourZeta, 1.)
528 self.assertTrue(shellSettings.reducedInterpolation)
529 self.assertFalse(shellSettings.drawNormal)
530 self.assertEqual(shellSettings.drawNormalFactor, 1.)
531 self.assertTrue(np.allclose(list(shellSettings.drawNormalColor), [0.2, 0.2, 0.2, 1.]))
532 self.assertTrue(shellSettings.drawNormalLines)
533
534
535#run all tests; the result is the number of tests that passed
536testIsActive = exu.sys.get('testIsActive', False)
537result = unittest.TextTestRunner(verbosity=2 if not testIsActive else 0,
538 stream=io.StringIO() if testIsActive else None).run(
539 unittest.TestSuite(unittest.defaultTestLoader.loadTestsFromTestCase(testCase) for testCase in
540 [TestANCFThinPlateElement, TestANCFThinPlateDamping, TestANCFThinPlateOutputs, TestShellsModule,
541 TestVisualizationSettings]))
542for (test, trace) in result.failures + result.errors:
543 exu.Print('FAILED: ' + str(test) + '\n' + trace)
544testResult = result.testsRun - len(result.failures) - len(result.errors) - len(result.unexpectedSuccesses)
545exu.Print('solution of ANCFThinPlateRevisionTest=', testResult)
546exu.sys['testResult'] = testResult