energiesTest.py

You can view and download this file on Github: energiesTest.py

  1#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  2# This is an EXUDYN example
  3#
  4# Details:  Kinetic and potential energy as output variables (#2202): several simple, independent
  5#           mechanisms without gravity, each read through sensors of KineticEnergy and PotentialEnergy.
  6#           The undamped ones keep their total energy, the damped one loses it, and a rigid body with
  7#           an offset center of mass has the kinetic energy of its center of mass plus its rotation.
  8#           (1) a mass point on a Cartesian spring-damper, oscillating in x and y;
  9#           (2) a mass point on a distance spring-damper, swinging and stretching;
 10#           (3) a planar rigid body on a coordinate spring-damper, with a spin;
 11#           (4) a rotor (ObjectRotationalMass1D) on a torsional spring;
 12#           (5) a 1D mass on a coordinate spring-damper with damping - the energy decreases;
 13#           (6) a free rigid body whose reference point is not its center of mass;
 14#           (7) a rigid body on a torsional spring with a constant torque, and (8) one on a linear
 15#           spring-damper with a constant force - the constant load is part of the potential;
 16#           (9) an ObjectGenericODE2 with a constant force vector; (10) a rigid body on a rigid-body
 17#           spring-damper, in small motion; (11) ANCF cables and geometrically exact beams in a rigid
 18#           translation, whose kinetic energy comes from their mass matrix; (12) a system with gravity
 19#           and a constant force, whose total energy SystemEnergy (exudyn.advancedUtilities) records;
 20#           (13) what the energies refuse, with the reason, and how SystemEnergy skips it; (14) two mass
 21#           points attracting each other (ObjectConnectorGravity), on an elliptic orbit.
 22#
 23# Author:   Johannes Gerstmayr
 24# Date:     2026-10-01
 25#
 26# 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.
 27#
 28#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 29
 30import exudyn as exu
 31from exudyn.utilities import * #includes itemInterface and rigidBodyUtilities
 32
 33import numpy as np
 34
 35testIsActive = exu.sys.get('testIsActive', False)
 36
 37SC = exu.SystemContainer()
 38mbs = SC.AddSystem()
 39
 40oGround = mbs.AddObject(ObjectGround())
 41nGround = mbs.AddNode(NodePointGround())
 42mGroundCoordinate = mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=nGround, coordinate=0))
 43KE = exu.OutputVariableType.KineticEnergy
 44PE = exu.OutputVariableType.PotentialEnergy
 45mechanisms = {}     #name: ([kinetic energy sensors], [potential energy sensors])
 46
 47def EnergySensors(name, bodies, connectors):
 48    mechanisms[name] = ([mbs.AddSensor(SensorBody(bodyNumber=b, outputVariableType=KE, storeInternal=True)) for b in bodies],
 49                        [mbs.AddSensor(SensorObject(objectNumber=c, outputVariableType=PE, storeInternal=True)) for c in connectors])
 50
 51#(1) mass point on a Cartesian spring-damper
 52n1 = mbs.AddNode(NodePoint(referenceCoordinates=[0, 0, 0], initialCoordinates=[0.1, 0, 0], initialVelocities=[0, 0.5, 0]))
 53o1 = mbs.AddObject(MassPoint(nodeNumber=n1, mass=2))
 54c1 = mbs.AddObject(CartesianSpringDamper(markerNumbers=[mbs.AddMarker(MarkerBodyPosition(bodyNumber=oGround)),
 55                                                        mbs.AddMarker(MarkerNodePosition(nodeNumber=n1))],
 56                                         stiffness=[200, 100, 300], damping=[0, 0, 0]))
 57EnergySensors('Cartesian spring', [o1], [c1])
 58
 59#(2) mass point on a distance spring-damper, swinging and stretching
 60n2 = mbs.AddNode(NodePoint(referenceCoordinates=[1, 0, 0], initialCoordinates=[0.1, 0, 0], initialVelocities=[0, 1, 0.2]))
 61o2 = mbs.AddObject(MassPoint(nodeNumber=n2, mass=1))
 62c2 = mbs.AddObject(SpringDamper(markerNumbers=[mbs.AddMarker(MarkerBodyPosition(bodyNumber=oGround, localPosition=[2, 0, 0])),
 63                                               mbs.AddMarker(MarkerNodePosition(nodeNumber=n2))],
 64                                referenceLength=1, stiffness=100, damping=0))
 65EnergySensors('distance spring', [o2], [c2])
 66
 67#(3) planar rigid body on a coordinate spring-damper in x, spinning freely
 68n3 = mbs.AddNode(NodeRigidBody2D(referenceCoordinates=[0, 2, 0], initialCoordinates=[0.05, 0, 0], initialVelocities=[0, 0, 2]))
 69o3 = mbs.AddObject(RigidBody2D(nodeNumber=n3, mass=3, inertia=0.2))
 70c3 = mbs.AddObject(CoordinateSpringDamper(markerNumbers=[mGroundCoordinate,
 71                                                         mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=n3, coordinate=0))],
 72                                          stiffness=50, damping=0, offset=0.01))
 73EnergySensors('planar body', [o3], [c3])
 74
 75#(4) rotor on a torsional spring (rotor about the local z-axis)
 76n4 = mbs.AddNode(NodeGenericODE2(referenceCoordinates=[0], initialCoordinates=[0.2], initialCoordinates_t=[0], numberOfODE2Coordinates=1))
 77o4 = mbs.AddObject(ObjectRotationalMass1D(nodeNumber=n4, inertia=0.5, referencePosition=[0, 3, 0]))
 78c4 = mbs.AddObject(CoordinateSpringDamper(markerNumbers=[mGroundCoordinate,
 79                                                         mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=n4, coordinate=0))],
 80                                          stiffness=20, damping=0))
 81EnergySensors('rotor', [o4], [c4])
 82
 83#(5) damped 1D mass: the energy decreases
 84n5 = mbs.AddNode(NodeGenericODE2(referenceCoordinates=[0], initialCoordinates=[0.1], initialCoordinates_t=[0], numberOfODE2Coordinates=1))
 85o5 = mbs.AddObject(ObjectMass1D(nodeNumber=n5, mass=1, referencePosition=[0, 4, 0]))
 86c5 = mbs.AddObject(CoordinateSpringDamper(markerNumbers=[mGroundCoordinate,
 87                                                         mbs.AddMarker(MarkerNodeCoordinate(nodeNumber=n5, coordinate=0))],
 88                                          stiffness=100, damping=1))
 89EnergySensors('damped mass', [o5], [c5])
 90
 91#(6) free rigid body, center of mass away from the reference point: T is the one of the center of mass
 92inertia = InertiaCuboid(density=500, sideLengths=[0.4, 0.2, 0.1]).Translated([0.2, 0.1, 0.05])
 93omega0 = np.array([1., 2., 3.])
 94v0 = np.array([0.3, 0, 0])
 95n6 = mbs.AddNode(NodeRigidBodyEP(referenceCoordinates=[0, 5, 0] + list(eulerParameters0),
 96                                 initialVelocities=list(v0) + list(AngularVelocity2EulerParameters_t(omega0, eulerParameters0))))
 97o6 = mbs.AddObject(ObjectRigidBody(nodeNumber=n6, mass=inertia.Mass(), inertia=inertia.GetInertia6D(),
 98                                   centerOfMass=inertia.COM()))
 99EnergySensors('free body with offset center of mass', [o6], [])
100
101#(7) rigid body on a torsional spring about z, with a constant torque
102n7 = mbs.AddNode(NodeRigidBodyRxyz(referenceCoordinates=[0, 6, 0, 0, 0, 0], initialVelocities=[0, 0, 0, 0, 0, 1]))
103inertia7 = InertiaCuboid(density=500, sideLengths=[0.4, 0.2, 0.1])
104o7 = mbs.AddObject(ObjectRigidBody(nodeNumber=n7, mass=inertia7.Mass(), inertia=inertia7.GetInertia6D()))
105c7 = mbs.AddObject(TorsionalSpringDamper(markerNumbers=[mbs.AddMarker(MarkerBodyRigid(bodyNumber=oGround, localPosition=[0, 6, 0])),
106                                                        mbs.AddMarker(MarkerNodeRigid(nodeNumber=n7))],
107                                         stiffness=2, damping=0, torque=0.1))
108EnergySensors('torsional spring with a constant torque', [o7], [c7])
109
110#(8) rigid body on a linear spring-damper along x, with a constant force
111n8 = mbs.AddNode(NodeRigidBodyEP(referenceCoordinates=[0, 7, 0] + list(eulerParameters0), initialVelocities=[0.4, 0, 0, 0, 0, 0, 0]))
112o8 = mbs.AddObject(ObjectRigidBody(nodeNumber=n8, mass=inertia7.Mass(), inertia=inertia7.GetInertia6D()))
113c8 = mbs.AddObject(ObjectConnectorLinearSpringDamper(markerNumbers=[mbs.AddMarker(MarkerBodyRigid(bodyNumber=oGround, localPosition=[0, 7, 0])),
114                                                                    mbs.AddMarker(MarkerNodeRigid(nodeNumber=n8))],
115                                                     stiffness=30, damping=0, force=0.5))
116EnergySensors('linear spring with a constant force', [o8], [c8])
117
118#(9) two masses on springs as an ObjectGenericODE2: M, K and a constant force vector
119n9 = mbs.AddNode(NodeGenericODE2(referenceCoordinates=[0, 0], initialCoordinates=[0.1, -0.05], initialCoordinates_t=[0, 0.3],
120                                 numberOfODE2Coordinates=2))
121o9 = mbs.AddObject(ObjectGenericODE2(nodeNumbers=[n9], massMatrix=np.diag([1., 2.]),
122                                     stiffnessMatrix=np.array([[200., -100.], [-100., 100.]]), forceVector=[0., 1.]))
123mechanisms['GenericODE2 with a constant force'] = ([mbs.AddSensor(SensorBody(bodyNumber=o9, outputVariableType=KE, storeInternal=True))],
124                                                   [mbs.AddSensor(SensorBody(bodyNumber=o9, outputVariableType=PE, storeInternal=True))])
125
126#(10) rigid body on a rigid-body spring-damper, small motion in all six directions
127n10 = mbs.AddNode(NodeRigidBodyEP(referenceCoordinates=[0, 8, 0] + list(eulerParameters0),
128                                  initialVelocities=[0.02, 0.01, -0.01] + list(AngularVelocity2EulerParameters_t([0.02, -0.01, 0.03], eulerParameters0))))
129o10 = mbs.AddObject(ObjectRigidBody(nodeNumber=n10, mass=inertia7.Mass(), inertia=inertia7.GetInertia6D()))
130c10 = mbs.AddObject(RigidBodySpringDamper(markerNumbers=[mbs.AddMarker(MarkerBodyRigid(bodyNumber=oGround, localPosition=[0, 8, 0])),
131                                                         mbs.AddMarker(MarkerNodeRigid(nodeNumber=n10))],
132                                          stiffness=np.diag([100, 200, 300, 2, 3, 4]), damping=np.zeros((6, 6))))
133EnergySensors('rigid-body spring-damper, small motion', [o10], [c10])
134
135#(11) the kinetic energy from the mass matrix: flexible bodies in a rigid translation, 1/2 m v^2
136v11 = 0.5
137nodes11 = [mbs.AddNode(NodePoint2DSlope1(referenceCoordinates=[i*0.5, 10, 1, 0], initialVelocities=[v11, 0, 0, 0])) for i in range(3)]
138cables11 = [mbs.AddObject(ObjectANCFCable2D(nodeNumbers=[nodes11[i], nodes11[i+1]], length=0.5, massPerLength=2,
139                                            bendingStiffness=1, axialStiffness=100)) for i in range(2)]
140section12 = exu.BeamSection()
141section12.stiffnessMatrix = np.diag([100, 100, 100, 1, 1, 1]); section12.inertia = np.diag([2e-3, 1e-3, 1e-3]); section12.massPerLength = 2
142nodes12 = [mbs.AddNode(NodeRigidBodyEP(referenceCoordinates=[i*0.5, 11, 0] + list(eulerParameters0),
143                                       initialVelocities=[0, v11, 0, 0, 0, 0, 0])) for i in range(3)]
144beams12 = [mbs.AddObject(ObjectBeamGeometricallyExact(nodeNumbers=[nodes12[i], nodes12[i+1]], length=0.5, sectionData=section12))
145           for i in range(2)]
146sRigidMotion = [mbs.AddSensor(SensorBody(bodyNumber=b, outputVariableType=KE, storeInternal=True)) for b in cables11 + beams12]
147
148#(14) two mass points attracting each other (ObjectConnectorGravity) on an elliptic orbit about their center of mass
149n14 = [mbs.AddNode(NodePoint(referenceCoordinates=[0, 13, 0], initialVelocities=[0, -0.3, 0])),
150       mbs.AddNode(NodePoint(referenceCoordinates=[1, 13, 0], initialVelocities=[0, 0.3, 0]))]
151o14 = [mbs.AddObject(MassPoint(nodeNumber=n, mass=1)) for n in n14]
152c14 = mbs.AddObject(ObjectConnectorGravity(markerNumbers=[mbs.AddMarker(MarkerNodePosition(nodeNumber=n)) for n in n14],
153                                           gravitationalConstant=0.2, mass0=1, mass1=1, minDistanceRegularization=0.05))
154EnergySensors('two masses attracting each other', o14, [c14])
155
156mbs.Assemble()
157
158simulationSettings = exu.SimulationSettings()
159simulationSettings.timeIntegration.newton.useModifiedNewton = False #Just for the test; modified Newton is usually faster
160simulationSettings.timeIntegration.numberOfSteps = 2000
161simulationSettings.timeIntegration.endTime = 2
162simulationSettings.timeIntegration.generalizedAlpha.spectralRadius = 1
163simulationSettings.timeIntegration.verboseMode = 0
164simulationSettings.solution.file.write = False
165simulationSettings.solution.sensors.writePeriod = 0.01
166mbs.SolveDynamic(simulationSettings)
167
168testResult = 0
169for (name, (kinetic, potential)) in mechanisms.items():
170    total = sum(mbs.GetSensorStoredData(s)[:, 1] for s in kinetic + potential)
171    exu.Print('%-40s total energy %.8f -> %.8f, relative change %.2e' % (name, total[0], total[-1], (total[-1]-total[0])/total[0]))
172    testResult += total[-1]
173
174#(6) against the kinetic energy of the center of mass and the rotation about it, at the start
175comVelocity = v0 + np.cross(omega0, inertia.COM())
176Jcom = inertia.Translated(-inertia.COM()).Inertia()
177T6 = 0.5*inertia.Mass()*comVelocity @ comVelocity + 0.5*omega0 @ Jcom @ omega0
178exu.Print('free body: T from the output variable', mbs.GetSensorStoredData(mechanisms['free body with offset center of mass'][0][0])[0, 1],
179          ', from the center of mass', T6)
180
181#(11) at the start: each element of mass 2*0.5 = 1 moving with 0.5 has T = 0.125
182rigidMotion = [mbs.GetSensorStoredData(s)[0, 1] for s in sRigidMotion]
183exu.Print('flexible bodies in a rigid translation: T =', rigidMotion, '(exact: 0.125 each)')
184testResult += sum(rigidMotion)
185
186#(12) the energy of a whole system with loads, SystemEnergy: a rigid body with an offset center of mass under
187#gravity and a constant force, on a spring; the total of kinetic, elastic and load potential energy is kept
188from exudyn.advancedUtilities import SystemEnergy
189SC2 = exu.SystemContainer()
190mbs2 = SC2.AddSystem()
191oGround2 = mbs2.AddObject(ObjectGround())
192inertia12 = InertiaCuboid(density=500, sideLengths=[0.4, 0.2, 0.1]).Translated([0.1, 0, 0])
193n12 = mbs2.AddNode(NodeRigidBodyEP(referenceCoordinates=[1, 0, 0] + list(eulerParameters0),
194                                   initialVelocities=[0, 0.5, 0] + list(AngularVelocity2EulerParameters_t([0, 0, 1], eulerParameters0))))
195o12 = mbs2.AddObject(ObjectRigidBody(nodeNumber=n12, mass=inertia12.Mass(), inertia=inertia12.GetInertia6D(),
196                                     centerOfMass=inertia12.COM()))
197mbs2.AddObject(SpringDamper(markerNumbers=[mbs2.AddMarker(MarkerBodyPosition(bodyNumber=oGround2)),
198                                           mbs2.AddMarker(MarkerBodyPosition(bodyNumber=o12))],
199                            referenceLength=0.8, stiffness=50, damping=0))
200mbs2.AddLoad(LoadMassProportional(markerNumber=mbs2.AddMarker(MarkerBodyMass(bodyNumber=o12)), loadVector=[0, -9.81, 0]))
201mbs2.AddLoad(LoadForceVector(markerNumber=mbs2.AddMarker(MarkerBodyPosition(bodyNumber=o12, localPosition=[0.2, 0, 0])),
202                             loadVector=[0.5, 0, 0]))
203mbs2.Assemble()
204systemEnergy = SystemEnergy(mbs2)
205sEnergy = systemEnergy.AddSensor()
206mbs2.Assemble()
207exu.Print('SystemEnergy: kinetic', len(systemEnergy.kineticObjects), 'potential', len(systemEnergy.potentialObjects),
208          'loads', len(systemEnergy.loads), 'unavailable', systemEnergy.unavailable)
209mbs2.SolveDynamic(simulationSettings)
210energies = mbs2.GetSensorStoredData(sEnergy)
211exu.Print('system with loads: total energy %.8f -> %.8f, relative change %.2e' % (energies[0, 4], energies[-1, 4],
212          (energies[-1, 4]-energies[0, 4])/energies[0, 4]))
213testResult += energies[-1, 4]
214
215#(13) what the energies refuse, with the reason: a local position other than [0,0,0] (the energy is one value of the
216#whole item), and a spring-damper whose force is a user function (its potential is unknown); SystemEnergy lists such
217#an item as unavailable and skips it, or raises with skipUnavailable=False
218refused = 0
219try:
220    mbs.GetObjectOutputBody(o1, KE, localPosition=[0.1, 0, 0])
221except ValueError as error:
222    exu.Print('refused:', str(error).split(' [Python file')[0])
223    refused += 1
224
225SC3 = exu.SystemContainer()
226mbs3 = SC3.AddSystem()
227oGround3 = mbs3.AddObject(ObjectGround())
228n13 = mbs3.AddNode(NodePoint(referenceCoordinates=[1, 0, 0], initialVelocities=[0, 2, 0]))
229o13 = mbs3.AddObject(MassPoint(nodeNumber=n13, mass=3))
230c13 = mbs3.AddObject(SpringDamper(markerNumbers=[mbs3.AddMarker(MarkerBodyPosition(bodyNumber=oGround3)),
231                                                 mbs3.AddMarker(MarkerNodePosition(nodeNumber=n13))],
232                                  referenceLength=0.5, stiffness=10,
233                                  springForceUserFunction=lambda mbs, t, itemNumber, u, v, k, d, f: k*u))
234mbs3.Assemble()
235try:
236    mbs3.GetObjectOutput(c13, PE)
237except NotImplementedError as error:
238    exu.Print('refused:', str(error).split(' [Python file')[0])
239    refused += 1
240systemEnergy3 = SystemEnergy(mbs3)
241exu.Print('SystemEnergy with a user function: unavailable', systemEnergy3.unavailable)
242try:
243    SystemEnergy(mbs3, skipUnavailable=False)
244except NotImplementedError as error:
245    exu.Print('refused:', str(error).split(' [Python file')[0])
246    refused += 1
247T3, V3, loads3, total3 = systemEnergy3.ComputeSystemEnergies()
248exu.Print('the mass point alone: T =', T3, '(exact 6), total =', total3)
249testResult += refused + total3
250
251exu.Print('solution of energiesTest=', testResult)
252exu.sys['testResult'] = testResult